diff --git a/src/wp-includes/class-wp-meta-query.php b/src/wp-includes/class-wp-meta-query.php
index 0b3ffab033d..f57e1361379 100644
--- a/src/wp-includes/class-wp-meta-query.php
+++ b/src/wp-includes/class-wp-meta-query.php
@@ -102,6 +102,7 @@ class WP_Meta_Query {
 	 * @since 5.1.0 Introduced $compare_key clause parameter, which enables LIKE key matches.
 	 * @since 5.3.0 Increased the number of operators available to $compare_key. Introduced $type_key,
 	 *              which enables the $key to be cast to a new data type for comparisons.
+	 * @since 5.9.0 Introduced $compare_key_like_mode and $compare_like_mode to LIKE queries
 	 *
 	 * @param array $meta_query {
 	 *     Array of meta query clauses. When first-order clauses or sub-clauses use strings as
@@ -112,24 +113,28 @@ class WP_Meta_Query {
 	 *     @type array  ...$0 {
 	 *         Optional. An array of first-order clause parameters, or another fully-formed meta query.
 	 *
-	 *         @type string $key         Meta key to filter by.
-	 *         @type string $compare_key MySQL operator used for comparing the $key. Accepts '=', '!='
-	 *                                   'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'REGEXP', 'NOT REGEXP', 'RLIKE',
-	 *                                   'EXISTS' (alias of '=') or 'NOT EXISTS' (alias of '!=').
-	 *                                   Default is 'IN' when `$key` is an array, '=' otherwise.
-	 *         @type string $type_key    MySQL data type that the meta_key column will be CAST to for
-	 *                                   comparisons. Accepts 'BINARY' for case-sensitive regular expression
-	 *                                   comparisons. Default is ''.
-	 *         @type string $value       Meta value to filter by.
-	 *         @type string $compare     MySQL operator used for comparing the $value. Accepts '=',
-	 *                                   '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE',
-	 *                                   'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP',
-	 *                                   'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'.
-	 *                                   Default is 'IN' when `$value` is an array, '=' otherwise.
-	 *         @type string $type        MySQL data type that the meta_value column will be CAST to for
-	 *                                   comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE',
-	 *                                   'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'.
-	 *                                   Default is 'CHAR'.
+	 *         @type string $key                    Meta key to filter by.
+	 *         @type string $compare_key            MySQL operator used for comparing the $key. Accepts '=', '!='
+	 *                                              'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'REGEXP', 'NOT REGEXP', 'RLIKE',
+	 *                                              'EXISTS' (alias of '=') or 'NOT EXISTS' (alias of '!=').
+	 *                                              Default is 'IN' when `$key` is an array, '=' otherwise.
+	 *         @type string $compare_key_like_mode  Search mode for LIKE compares. Accepts 'contains', 'startswith' or
+	 *                                              'endswith'. Default is 'contains'.
+	 *         @type string $type_key               MySQL data type that the meta_key column will be CAST to for
+	 *                                              comparisons. Accepts 'BINARY' for case-sensitive regular expression
+	 *                                              comparisons. Default is ''.
+	 *         @type string $value                  Meta value to filter by.
+	 *         @type string $compare                MySQL operator used for comparing the $value. Accepts '=',
+	 *                                              '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE',
+	 *                                              'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP',
+	 *                                              'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'.
+	 *                                              Default is 'IN' when `$value` is an array, '=' otherwise.
+	 *         @type string $compare_like_mode      Search mode for LIKE compares. Accepts 'contains', 'startswith' or
+	 *                                              'endswith'. Default is 'contains'.
+	 *         @type string $type                   MySQL data type that the meta_value column will be CAST to for
+	 *                                              comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE',
+	 *                                              'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'.
+	 *                                              Default is 'CHAR'.
 	 *     }
 	 * }
 	 */
@@ -246,7 +251,7 @@ public function parse_query_vars( $qv ) {
 		 * the rest of the meta_query).
 		 */
 		$primary_meta_query = array();
-		foreach ( array( 'key', 'compare', 'type', 'compare_key', 'type_key' ) as $key ) {
+		foreach ( array( 'key', 'compare_key_like_mode', 'compare', 'compare_like_mode', 'type', 'compare_key', 'type_key' ) as $key ) {
 			if ( ! empty( $qv[ "meta_$key" ] ) ) {
 				$primary_meta_query[ $key ] = $qv[ "meta_$key" ];
 			}
@@ -545,6 +550,27 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' )
 		$meta_compare     = $clause['compare'];
 		$meta_compare_key = $clause['compare_key'];
 
+		// LIKE and NOT LIKE supports three search modes. Startswith, Endswith and Contains.
+		// Default to 'contains' for backward compat
+		if ( 'LIKE' === $meta_compare_key || 'NOT LIKE' === $meta_compare_key ) {
+			// Key compares
+			if ( empty( $clause['compare_key_like_mode'] ) ) {
+				$clause['compare_key_like_mode'] = 'contains';
+			}
+			$meta_compare_key_like_mode = $clause['compare_key_like_mode'];
+		}
+		if ( 'LIKE' === $meta_compare || 'NOT LIKE' === $meta_compare ) {
+			// Value compares
+			if ( empty( $clause['compare_like_mode'] ) ) {
+				$clause['compare_like_mode'] = 'contains';
+			}
+			$meta_compare_like_mode = $clause['compare_like_mode'];
+		}
+		// Query templates for LIKE / NOT LIKE queries
+		$meta_compare_like_value_tpl['startswith'] = '%s%%';
+		$meta_compare_like_value_tpl['endswith']   = '%%%s';
+		$meta_compare_like_value_tpl['contains']   = '%%%s%%';
+
 		// First build the JOIN clause, if one is required.
 		$join = '';
 
@@ -632,7 +658,7 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' )
 						$where = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 						break;
 					case 'LIKE':
-						$meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
+						$meta_compare_value = sprintf( $meta_compare_like_value_tpl[ $meta_compare_key_like_mode ], $wpdb->esc_like( trim( $clause['key'] ) ) );
 						$where              = $wpdb->prepare( "$alias.meta_key LIKE %s", $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 						break;
 					case 'IN':
@@ -657,9 +683,8 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' )
 						break;
 					case 'NOT LIKE':
 						$meta_compare_string = $meta_compare_string_start . "AND $subquery_alias.meta_key LIKE %s " . $meta_compare_string_end;
-
-						$meta_compare_value = '%' . $wpdb->esc_like( trim( $clause['key'] ) ) . '%';
-						$where              = $wpdb->prepare( $meta_compare_string, $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+						$meta_compare_value  = sprintf( $meta_compare_like_value_tpl[ $meta_compare_key_like_mode ], $wpdb->esc_like( trim( $clause['key'] ) ) );
+						$where               = $wpdb->prepare( $meta_compare_string, $meta_compare_value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
 						break;
 					case 'NOT IN':
 						$array_subclause     = '(' . substr( str_repeat( ',%s', count( $clause['key'] ) ), 1 ) . ') ';
@@ -709,7 +734,7 @@ public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' )
 
 				case 'LIKE':
 				case 'NOT LIKE':
-					$meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
+					$meta_value = sprintf( $meta_compare_like_value_tpl[ $meta_compare_like_mode ], $wpdb->esc_like( $meta_value ) );
 					$where      = $wpdb->prepare( '%s', $meta_value );
 					break;
 
diff --git a/tests/phpunit/tests/query/metaQuery.php b/tests/phpunit/tests/query/metaQuery.php
index c9e73ec8a50..312cb9fee35 100644
--- a/tests/phpunit/tests/query/metaQuery.php
+++ b/tests/phpunit/tests/query/metaQuery.php
@@ -271,6 +271,142 @@ public function test_meta_query_single_query_compare_not_like() {
 		$this->assertSameSets( $expected, $query->posts );
 	}
 
+	public function test_meta_query_single_query_compare_startswith() {
+		$p1 = self::factory()->post->create();
+		$p2 = self::factory()->post->create();
+		$p3 = self::factory()->post->create();
+		$p4 = self::factory()->post->create();
+		$p5 = self::factory()->post->create();
+		$p6 = self::factory()->post->create();
+
+		add_post_meta( $p1, 'foo', 'barSTARTSWITH' );
+		add_post_meta( $p2, 'foo', 'ENDSWITHbar' );
+		add_post_meta( $p3, 'foo', 'CONTAINSbarCONTAINS' );
+		add_post_meta( $p4, 'foo', 'nomatch' );
+		add_post_meta( $p5, 'foo', 'barbar' );
+
+		$query = new WP_Query(
+			array(
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+				'fields'                 => 'ids',
+				'meta_query'             => array(
+					array(
+						'key'               => 'foo',
+						'value'             => 'bar',
+						'compare'           => 'LIKE',
+						'compare_like_mode' => 'startswith',
+					),
+				),
+			)
+		);
+
+		$expected = array( $p1, $p5 );
+		$this->assertSameSets( $expected, $query->posts );
+	}
+
+	public function test_meta_query_single_query_compare_not_startswith() {
+		$p1 = self::factory()->post->create();
+		$p2 = self::factory()->post->create();
+		$p3 = self::factory()->post->create();
+		$p4 = self::factory()->post->create();
+		$p5 = self::factory()->post->create();
+		$p6 = self::factory()->post->create();
+
+		add_post_meta( $p1, 'foo', 'barSTARTSWITH' );
+		add_post_meta( $p2, 'foo', 'ENDSWITHbar' );
+		add_post_meta( $p3, 'foo', 'CONTAINSbarCONTAINS' );
+		add_post_meta( $p4, 'foo', 'nomatch' );
+		add_post_meta( $p5, 'foo', 'barbar' );
+
+		$query = new WP_Query(
+			array(
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+				'fields'                 => 'ids',
+				'meta_query'             => array(
+					array(
+						'key'               => 'foo',
+						'value'             => 'bar',
+						'compare'           => 'NOT LIKE',
+						'compare_like_mode' => 'startswith',
+					),
+				),
+			)
+		);
+
+		$expected = array( $p2, $p3, $p4 );
+		$this->assertSameSets( $expected, $query->posts );
+	}
+
+	public function test_meta_query_single_query_compare_endswith() {
+		$p1 = self::factory()->post->create();
+		$p2 = self::factory()->post->create();
+		$p3 = self::factory()->post->create();
+		$p4 = self::factory()->post->create();
+		$p5 = self::factory()->post->create();
+		$p6 = self::factory()->post->create();
+
+		add_post_meta( $p1, 'foo', 'barSTARTSWITH' );
+		add_post_meta( $p2, 'foo', 'ENDSWITHbar' );
+		add_post_meta( $p3, 'foo', 'CONTAINSbarCONTAINS' );
+		add_post_meta( $p4, 'foo', 'nomatch' );
+		add_post_meta( $p5, 'foo', 'barbar' );
+
+		$query = new WP_Query(
+			array(
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+				'fields'                 => 'ids',
+				'meta_query'             => array(
+					array(
+						'key'               => 'foo',
+						'value'             => 'ar',
+						'compare'           => 'LIKE',
+						'compare_like_mode' => 'endswith',
+					),
+				),
+			)
+		);
+
+		$expected = array( $p2, $p5 );
+		$this->assertSameSets( $expected, $query->posts );
+	}
+
+	public function test_meta_query_single_query_compare_not_endswith() {
+		$p1 = self::factory()->post->create();
+		$p2 = self::factory()->post->create();
+		$p3 = self::factory()->post->create();
+		$p4 = self::factory()->post->create();
+		$p5 = self::factory()->post->create();
+		$p6 = self::factory()->post->create();
+
+		add_post_meta( $p1, 'foo', 'barSTARTSWITH' );
+		add_post_meta( $p2, 'foo', 'ENDSWITHbar' );
+		add_post_meta( $p3, 'foo', 'CONTAINSbarCONTAINS' );
+		add_post_meta( $p4, 'foo', 'nomatch' );
+		add_post_meta( $p5, 'foo', 'barbar' );
+
+		$query = new WP_Query(
+			array(
+				'update_post_meta_cache' => false,
+				'update_post_term_cache' => false,
+				'fields'                 => 'ids',
+				'meta_query'             => array(
+					array(
+						'key'               => 'foo',
+						'value'             => 'bar',
+						'compare'           => 'NOT LIKE',
+						'compare_like_mode' => 'endswith',
+					),
+				),
+			)
+		);
+
+		$expected = array( $p1, $p3, $p4 );
+		$this->assertSameSets( $expected, $query->posts );
+	}
+
 	public function test_meta_query_single_query_compare_between_not_between() {
 		$p1 = self::factory()->post->create();
 		$p2 = self::factory()->post->create();
@@ -1322,10 +1458,12 @@ public function test_meta_query_decimal_results() {
 	public function test_meta_vars_should_be_converted_to_meta_query() {
 		$q = new WP_Query(
 			array(
-				'meta_key'     => 'foo',
-				'meta_value'   => '5',
-				'meta_compare' => '>',
-				'meta_type'    => 'SIGNED',
+				'meta_key'                   => 'foo',
+				'meta_value'                 => '5',
+				'meta_compare'               => '>',
+				'meta_type'                  => 'SIGNED',
+				'meta_compare_key_like_mode' => 'endswith',
+				'meta_compare_like_mode'     => 'startswith',
 			)
 		);
 
@@ -1333,6 +1471,8 @@ public function test_meta_vars_should_be_converted_to_meta_query() {
 		$this->assertSame( '5', $q->meta_query->queries[0]['value'] );
 		$this->assertSame( '>', $q->meta_query->queries[0]['compare'] );
 		$this->assertSame( 'SIGNED', $q->meta_query->queries[0]['type'] );
+		$this->assertSame( 'startswith', $q->meta_query->queries[0]['compare_like_mode'] );
+		$this->assertSame( 'endswith', $q->meta_query->queries[0]['compare_key_like_mode'] );
 	}
 
 	/**
@@ -1874,6 +2014,101 @@ public function test_compare_key_like() {
 		$this->assertSameSets( array( $posts[0], $posts[2] ), $q->posts );
 	}
 
+	public function test_compare_key_like_startswith() {
+		$posts = self::factory()->post->create_many( 3 );
+
+		add_post_meta( $posts[0], 'aaa_foo_aaa', 'abc' );
+		add_post_meta( $posts[1], 'aaa_bar_aaa', 'abc' );
+		add_post_meta( $posts[2], 'aaa_foo_bbb', 'abc' );
+
+		$q = new WP_Query(
+			array(
+				'meta_query' => array(
+					array(
+						'compare_key'           => 'LIKE',
+						'compare_key_like_mode' => 'startswith',
+						'key'                   => 'aaa_foo',
+					),
+				),
+				'fields'     => 'ids',
+			)
+		);
+
+		$this->assertSameSets( array( $posts[0], $posts[2] ), $q->posts );
+	}
+
+	public function test_compare_key_like_endswith() {
+		$posts = self::factory()->post->create_many( 3 );
+
+		add_post_meta( $posts[0], 'aaa_foo_aaa', 'abc' );
+		add_post_meta( $posts[1], 'aaa_bar_aaa', 'abc' );
+		add_post_meta( $posts[2], 'aaa_foo_bbb', 'abc' );
+
+		$q = new WP_Query(
+			array(
+				'meta_query' => array(
+					array(
+						'compare_key'           => 'LIKE',
+						'compare_key_like_mode' => 'endswith',
+						'key'                   => 'foo_bbb',
+					),
+				),
+				'fields'     => 'ids',
+			)
+		);
+
+		$this->assertSameSets( array( $posts[2] ), $q->posts );
+	}
+
+	public function test_compare_key_like_explicit_contains() {
+		$posts = self::factory()->post->create_many( 3 );
+
+		add_post_meta( $posts[0], 'aaa_foo_aaa', 'abc' );
+		add_post_meta( $posts[1], 'aaa_bar_aaa', 'abc' );
+		add_post_meta( $posts[2], 'aaa_foo_bbb', 'abc' );
+
+		$q = new WP_Query(
+			array(
+				'meta_query' => array(
+					array(
+						'compare_key'           => 'LIKE',
+						'compare_key_like_mode' => 'contains',
+						'key'                   => 'a_foo_',
+					),
+				),
+				'fields'     => 'ids',
+			)
+		);
+
+		$this->assertSameSets( array( $posts[0], $posts[2] ), $q->posts );
+	}
+
+	public function test_compare_key_like_and_value_like() {
+		$posts = self::factory()->post->create_many( 3 );
+
+		add_post_meta( $posts[0], 'aaa_foo_aaa', 'abc' );
+		add_post_meta( $posts[1], 'aaa_bar_aaa', 'def' );
+		add_post_meta( $posts[2], 'aaa_foo_bbb', 'abc' );
+
+		$q = new WP_Query(
+			array(
+				'meta_query' => array(
+					array(
+						'compare_key'           => 'LIKE',
+						'compare_key_like_mode' => 'endswith',
+						'key'                   => 'aaa',
+						'compare'               => 'LIKE',
+						'compare_like_mode'     => 'startswith',
+						'value'                 => 'de',
+					),
+				),
+				'fields'     => 'ids',
+			)
+		);
+
+		$this->assertSameSets( array( $posts[1] ), $q->posts );
+	}
+
 	/**
 	 * @ticket 42409
 	 */
