Index: src/wp-includes/class-wp-site-query.php
===================================================================
--- src/wp-includes/class-wp-site-query.php	(revision 42907)
+++ src/wp-includes/class-wp-site-query.php	(working copy)
@@ -40,6 +40,22 @@
 	);
 
 	/**
+	 * Metadata query container.
+	 *
+	 * @since 5.0.0
+	 * @var WP_Meta_Query
+	 */
+	public $meta_query = false;
+
+	/**
+	 * Metadata query clauses.
+	 *
+	 * @since 5.0.0
+	 * @var array
+	 */
+	protected $meta_query_clauses;
+
+	/**
 	 * Date query container.
 	 *
 	 * @since 4.6.0
@@ -92,7 +108,8 @@
 	 *
 	 * @since 4.6.0
 	 * @since 4.8.0 Introduced the 'lang_id', 'lang__in', and 'lang__not_in' parameters.
-	 * @since 5.0.0 Introduced the 'update_site_meta_cache' parameter.
+	 * @since 5.0.0 Introduced the 'update_site_meta_cache', 'meta_query', 'meta_key',
+	 *              'meta_value', 'meta_type' and 'meta_compare' parameters.
 	 *
 	 * @param string|array $query {
 	 *     Optional. Array or query string of site query parameters. Default empty.
@@ -139,6 +156,15 @@
 	 *                                                Default empty array.
 	 *     @type bool         $update_site_cache      Whether to prime the cache for found sites. Default true.
 	 *     @type bool         $update_site_meta_cache Whether to prime the metadata cache for found sites. Default true.
+	 *     @type array        $meta_query             Meta query clauses to limit retrieved sites by. See `WP_Meta_Query`.
+	 *                                                Default empty.
+	 *     @type string       $meta_key               Limit sites to those matching a specific metadata key.
+	 *                                                Can be used in conjunction with `$meta_value`. Default empty.
+	 *     @type string       $meta_value             Limit sites to those matching a specific metadata value.
+	 *                                                Usually used in conjunction with `$meta_key`. Default empty.
+	 *     @type string       $meta_type              Data type that the `$meta_value` column will be CAST to for
+	 *                                                comparisons. Default empty.
+	 *     @type string       $meta_compare           Comparison operator to test the `$meta_value`. Default empty.
 	 * }
 	 */
 	public function __construct( $query = '' ) {
@@ -175,6 +201,11 @@
 			'date_query'             => null, // See WP_Date_Query
 			'update_site_cache'      => true,
 			'update_site_meta_cache' => true,
+			'meta_query'             => '',
+			'meta_key'               => '',
+			'meta_value'             => '',
+			'meta_type'              => '',
+			'meta_compare'           => '',
 		);
 
 		if ( ! empty( $query ) ) {
@@ -228,12 +259,20 @@
 	 *
 	 * @since 4.6.0
 	 *
+	 * @global wpdb $wpdb WordPress database abstraction object.
+	 *
 	 * @return array|int List of WP_Site objects, a list of site ids when 'fields' is set to 'ids',
 	 *                   or the number of sites when 'count' is passed as a query var.
 	 */
 	public function get_sites() {
+		global $wpdb;
+
 		$this->parse_query();
 
+		// Parse meta query.
+		$this->meta_query = new WP_Meta_Query();
+		$this->meta_query->parse_query_vars( $this->query_vars );
+
 		/**
 		 * Fires before sites are retrieved.
 		 *
@@ -243,6 +282,12 @@
 		 */
 		do_action_ref_array( 'pre_get_sites', array( &$this ) );
 
+		// Reparse query vars, in case they were modified in a 'pre_get_sites' callback.
+		$this->meta_query->parse_query_vars( $this->query_vars );
+		if ( ! empty( $this->meta_query->queries ) ) {
+			$this->meta_query_clauses = $this->meta_query->get_sql( 'blog', $wpdb->blogs, 'blog_id', $this );
+		}
+
 		// $args can include anything. Only use the args defined in the query_var_defaults to compute the key.
 		$_args = wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) );
 
@@ -370,7 +415,7 @@
 
 			$orderby = implode( ', ', $orderby_array );
 		} else {
-			$orderby = "blog_id $order";
+			$orderby = "{$wpdb->blogs}.blog_id $order";
 		}
 
 		$number = absint( $this->query_vars['number'] );
@@ -387,23 +432,23 @@
 		if ( $this->query_vars['count'] ) {
 			$fields = 'COUNT(*)';
 		} else {
-			$fields = 'blog_id';
+			$fields = "{$wpdb->blogs}.blog_id";
 		}
 
 		// Parse site IDs for an IN clause.
 		$site_id = absint( $this->query_vars['ID'] );
 		if ( ! empty( $site_id ) ) {
-			$this->sql_clauses['where']['ID'] = $wpdb->prepare( 'blog_id = %d', $site_id );
+			$this->sql_clauses['where']['ID'] = $wpdb->prepare( "{$wpdb->blogs}.blog_id = %d", $site_id );
 		}
 
 		// Parse site IDs for an IN clause.
 		if ( ! empty( $this->query_vars['site__in'] ) ) {
-			$this->sql_clauses['where']['site__in'] = 'blog_id IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['site__in'] ) ) . ' )';
+			$this->sql_clauses['where']['site__in'] = "{$wpdb->blogs}.blog_id IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['site__in'] ) ) . ' )';
 		}
 
 		// Parse site IDs for a NOT IN clause.
 		if ( ! empty( $this->query_vars['site__not_in'] ) ) {
-			$this->sql_clauses['where']['site__not_in'] = 'blog_id NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['site__not_in'] ) ) . ' )';
+			$this->sql_clauses['where']['site__not_in'] = "{$wpdb->blogs}.blog_id NOT IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['site__not_in'] ) ) . ' )';
 		}
 
 		$network_id = absint( $this->query_vars['network_id'] );
@@ -526,6 +571,17 @@
 
 		$join = '';
 
+		if ( ! empty( $this->meta_query_clauses ) ) {
+			$join .= $this->meta_query_clauses['join'];
+
+			// Strip leading 'AND'.
+			$this->sql_clauses['where']['meta_query'] = preg_replace( '/^\s*AND\s*/', '', $this->meta_query_clauses['where'] );
+
+			if ( ! $this->query_vars['count'] ) {
+				$groupby = "{$wpdb->blogs}.blog_id";
+			}
+		}
+
 		$where = implode( ' AND ', $this->sql_clauses['where'] );
 
 		$pieces = array( 'fields', 'join', 'where', 'orderby', 'limits', 'groupby' );
@@ -675,10 +731,42 @@
 				$parsed = 'CHAR_LENGTH(path)';
 				break;
 			case 'id':
-				$parsed = 'blog_id';
+				$parsed = "{$wpdb->blogs}.blog_id";
 				break;
 		}
 
+		if ( ! empty( $parsed ) || empty( $this->meta_query_clauses ) ) {
+			return $parsed;
+		}
+
+		$meta_clauses = $this->meta_query->get_clauses();
+		if ( empty( $meta_clauses ) ) {
+			return $parsed;
+		}
+
+		$primary_meta_query = reset( $meta_clauses );
+		if ( ! empty( $primary_meta_query['key'] ) && $primary_meta_query['key'] === $orderby ) {
+			$orderby = 'meta_value';
+		}
+
+		switch ( $orderby ) {
+			case 'meta_value':
+				if ( ! empty( $primary_meta_query['type'] ) ) {
+					$parsed = "CAST({$primary_meta_query['alias']}.meta_value AS {$primary_meta_query['cast']})";
+				} else {
+					$parsed = "{$primary_meta_query['alias']}.meta_value";
+				}
+				break;
+			case 'meta_value_num':
+				$parsed = "{$primary_meta_query['alias']}.meta_value+0";
+				break;
+			default:
+				if ( isset( $meta_clauses[ $orderby ] ) ) {
+					$meta_clause = $meta_clauses[ $orderby ];
+					$parsed      = "CAST({$meta_clause['alias']}.meta_value AS {$meta_clause['cast']})";
+				}
+		}
+
 		return $parsed;
 	}
 
Index: src/wp-settings.php
===================================================================
--- src/wp-settings.php	(revision 42907)
+++ src/wp-settings.php	(working copy)
@@ -98,6 +98,7 @@
 require( ABSPATH . WPINC . '/formatting.php' );
 require( ABSPATH . WPINC . '/meta.php' );
 require( ABSPATH . WPINC . '/functions.php' );
+require( ABSPATH . WPINC . '/class-wp-meta-query.php' );
 require( ABSPATH . WPINC . '/class-wp-matchesmapregex.php' );
 require( ABSPATH . WPINC . '/class-wp.php' );
 require( ABSPATH . WPINC . '/class-wp-error.php' );
@@ -159,7 +160,6 @@
 require( ABSPATH . WPINC . '/class-wp-user-query.php' );
 require( ABSPATH . WPINC . '/class-wp-session-tokens.php' );
 require( ABSPATH . WPINC . '/class-wp-user-meta-session-tokens.php' );
-require( ABSPATH . WPINC . '/class-wp-meta-query.php' );
 require( ABSPATH . WPINC . '/class-wp-metadata-lazyloader.php' );
 require( ABSPATH . WPINC . '/general-template.php' );
 require( ABSPATH . WPINC . '/link-template.php' );
Index: tests/phpunit/tests/multisite/siteMeta.php
===================================================================
--- tests/phpunit/tests/multisite/siteMeta.php	(revision 42907)
+++ tests/phpunit/tests/multisite/siteMeta.php	(working copy)
@@ -256,6 +256,122 @@
 		get_site_meta( self::$site_id, 'foo', true );
 		$this->assertSame( $num_queries + 1, $wpdb->num_queries);
 	}
+
+	/**
+	 * @ticket 40229
+	 */
+	public function test_add_site_meta_should_bust_get_sites_cache() {
+		if ( ! is_site_meta_supported() ) {
+			$this->markTestSkipped( 'Tests only runs with the blogmeta database table installed' );
+		}
+
+		add_site_meta( self::$site_id, 'foo', 'bar' );
+
+		// Prime cache.
+		$found = get_sites( array(
+			'fields' => 'ids',
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$this->assertEqualSets( array( self::$site_id ), $found );
+
+		add_site_meta( self::$site_id2, 'foo', 'bar' );
+
+		$found = get_sites( array(
+			'fields' => 'ids',
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$this->assertEqualSets( array( self::$site_id, self::$site_id2 ), $found );
+	}
+
+	/**
+	 * @ticket 40229
+	 */
+	public function test_update_site_meta_should_bust_get_sites_cache() {
+		if ( ! is_site_meta_supported() ) {
+			$this->markTestSkipped( 'Tests only runs with the blogmeta database table installed' );
+		}
+
+		add_site_meta( self::$site_id, 'foo', 'bar' );
+		add_site_meta( self::$site_id2, 'foo', 'baz' );
+
+		// Prime cache.
+		$found = get_sites( array(
+			'fields' => 'ids',
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$this->assertEqualSets( array( self::$site_id ), $found );
+
+		update_site_meta( self::$site_id2, 'foo', 'bar' );
+
+		$found = get_sites( array(
+			'fields' => 'ids',
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$this->assertEqualSets( array( self::$site_id, self::$site_id2 ), $found );
+	}
+
+	/**
+	 * @ticket 40229
+	 */
+	public function test_delete_site_meta_should_bust_get_sites_cache() {
+		if ( ! is_site_meta_supported() ) {
+			$this->markTestSkipped( 'Tests only runs with the blogmeta database table installed' );
+		}
+
+		add_site_meta( self::$site_id, 'foo', 'bar' );
+		add_site_meta( self::$site_id2, 'foo', 'bar' );
+
+		// Prime cache.
+		$found = get_sites( array(
+			'fields' => 'ids',
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$this->assertEqualSets( array( self::$site_id, self::$site_id2 ), $found );
+
+		delete_site_meta( self::$site_id2, 'foo', 'bar' );
+
+		$found = get_sites( array(
+			'fields' => 'ids',
+			'meta_query' => array(
+				array(
+					'key' => 'foo',
+					'value' => 'bar',
+				),
+			),
+		) );
+
+		$this->assertEqualSets( array( self::$site_id ), $found );
+	}
 }
 
 endif;
Index: tests/phpunit/tests/multisite/siteQuery.php
===================================================================
--- tests/phpunit/tests/multisite/siteQuery.php	(revision 42907)
+++ tests/phpunit/tests/multisite/siteQuery.php	(working copy)
@@ -878,6 +878,193 @@
 			);
 			$this->assertEquals( $number_of_queries + 1, $wpdb->num_queries );
 		}
+
+		/**
+		 * @ticket 40229
+		 * @dataProvider data_wp_site_query_meta_query
+		 */
+		public function test_wp_site_query_meta_query( $query, $expected, $strict ) {
+			if ( ! is_site_meta_supported() ) {
+				$this->markTestSkipped( 'Tests only runs with the blogmeta database table installed' );
+			}
+
+			add_site_meta( self::$site_ids['wordpress.org/'], 'foo', 'foo' );
+			add_site_meta( self::$site_ids['wordpress.org/foo/'], 'foo', 'bar' );
+			add_site_meta( self::$site_ids['wordpress.org/foo/bar/'], 'foo', 'baz' );
+			add_site_meta( self::$site_ids['make.wordpress.org/'], 'bar', 'baz' );
+			add_site_meta( self::$site_ids['wordpress.org/'], 'numberfoo', 1 );
+			add_site_meta( self::$site_ids['wordpress.org/foo/'], 'numberfoo', 2 );
+
+			$query['fields'] = 'ids';
+
+			$q     = new WP_Site_Query();
+			$found = $q->query( $query );
+
+			foreach ( $expected as $index => $domain_path ) {
+				$expected[ $index ] = self::$site_ids[ $domain_path ];
+			}
+
+			if ( $strict ) {
+				$this->assertEquals( $expected, $found );
+			} else {
+				$this->assertEqualSets( $expected, $found );
+			}
+		}
+
+		public function data_wp_site_query_meta_query() {
+			return array(
+				array(
+					array(
+						'meta_key' => 'foo',
+					),
+					array(
+						'wordpress.org/',
+						'wordpress.org/foo/',
+						'wordpress.org/foo/bar/',
+					),
+					false,
+				),
+				array(
+					array(
+						'meta_key'   => 'foo',
+						'meta_value' => 'bar',
+					),
+					array(
+						'wordpress.org/foo/',
+					),
+					false,
+				),
+				array(
+					array(
+						'meta_key'     => 'foo',
+						'meta_value'   => array( 'bar', 'baz' ),
+						'meta_compare' => 'IN',
+					),
+					array(
+						'wordpress.org/foo/',
+						'wordpress.org/foo/bar/',
+					),
+					false,
+				),
+				array(
+					array(
+						'meta_query' => array(
+							array(
+								'key'   => 'foo',
+								'value' => 'bar',
+							),
+							array(
+								'key'   => 'numberfoo',
+								'value' => 2,
+								'type'  => 'NUMERIC',
+							),
+						),
+					),
+					array(
+						'wordpress.org/foo/',
+					),
+					false,
+				),
+				array(
+					array(
+						'meta_key' => 'foo',
+						'orderby'  => 'meta_value',
+						'order'    => 'ASC',
+					),
+					array(
+						'wordpress.org/foo/',
+						'wordpress.org/foo/bar/',
+						'wordpress.org/',
+					),
+					true,
+				),
+				array(
+					array(
+						'meta_key' => 'foo',
+						'orderby'  => 'foo',
+						'order'    => 'ASC',
+					),
+					array(
+						'wordpress.org/foo/',
+						'wordpress.org/foo/bar/',
+						'wordpress.org/',
+					),
+					true,
+				),
+				array(
+					array(
+						'meta_key' => 'numberfoo',
+						'orderby'  => 'meta_value_num',
+						'order'    => 'DESC',
+					),
+					array(
+						'wordpress.org/foo/',
+						'wordpress.org/',
+					),
+					true,
+				),
+				array(
+					array(
+						'meta_query' => array(
+							array(
+								'key'     => 'foo',
+								'value'   => array( 'foo', 'bar' ),
+								'compare' => 'IN',
+							),
+							array(
+								'key' => 'numberfoo',
+							),
+						),
+						'orderby'    => array( 'meta_value' => 'ASC' ),
+					),
+					array(
+						'wordpress.org/foo/',
+						'wordpress.org/',
+					),
+					true,
+				),
+				array(
+					array(
+						'meta_query' => array(
+							array(
+								'key'     => 'foo',
+								'value'   => array( 'foo', 'bar' ),
+								'compare' => 'IN',
+							),
+							array(
+								'key' => 'numberfoo',
+							),
+						),
+						'orderby'    => array( 'foo' => 'ASC' ),
+					),
+					array(
+						'wordpress.org/foo/',
+						'wordpress.org/',
+					),
+					true,
+				),
+				array(
+					array(
+						'meta_query' => array(
+							array(
+								'key'     => 'foo',
+								'value'   => array( 'foo', 'bar' ),
+								'compare' => 'IN',
+							),
+							'my_subquery' => array(
+								'key' => 'numberfoo',
+							),
+						),
+						'orderby'    => array( 'my_subquery' => 'DESC' ),
+					),
+					array(
+						'wordpress.org/foo/',
+						'wordpress.org/',
+					),
+					true,
+				),
+			);
+		}
 	}
 
 endif;
