Index: src/wp-includes/class-wp-site-query.php
===================================================================
--- src/wp-includes/class-wp-site-query.php	(revision 42373)
+++ src/wp-includes/class-wp-site-query.php	(working copy)
@@ -240,16 +240,7 @@
 		 */
 		do_action_ref_array( 'pre_get_sites', array( &$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 ) );
-
-		// Ignore the $fields argument as the queried result will be the same regardless.
-		unset( $_args['fields'] );
-
-		$key          = md5( serialize( $_args ) );
-		$last_changed = wp_cache_get_last_changed( 'sites' );
-
-		$cache_key   = "get_sites:$key:$last_changed";
+		$cache_key   = $this->get_cache_key( $this->query_vars );
 		$cache_value = wp_cache_get( $cache_key, 'sites' );
 
 		if ( false === $cache_value ) {
@@ -698,4 +689,74 @@
 			return 'DESC';
 		}
 	}
+
+	/**
+	 * Generates the cache key to lookup query results for a specific set of arguments.
+	 *
+	 * @since 5.0.0
+	 *
+	 * @param array $query_vars Array of WP_Site_Query arguments. See WP_Site_Query::__construct().
+	 * @return string Cache key to use for the lookup.
+	 */
+	protected function get_cache_key( $query_vars ) {
+		// $args can include anything. Only use the args defined in the query_var_defaults to compute the key.
+		$_args = wp_array_slice_assoc( $query_vars, array_keys( $this->query_var_defaults ) );
+
+		// Ignore the $fields argument as the queried result will be the same regardless.
+		unset( $_args['fields'] );
+
+		// Ignore the $update_site_cache as it does not affect the query.
+		unset( $_args['update_site_cache'] );
+
+		// Use the same cache key for array lookups with one element and single value lookups.
+		$single_multi_mappings = array(
+			'ID'         => 'site__in',
+			'network_id' => 'network__in',
+			'domain'     => 'domain__in',
+			'path'       => 'path__in',
+			'lang_id'    => 'lang__in',
+		);
+		foreach ( $single_multi_mappings as $single_var => $multi_var ) {
+			if ( empty( $_args[ $single_var ] ) && isset( $_args[ $multi_var ] ) && is_array( $_args[ $multi_var ] ) && count( $_args[ $multi_var ] ) === 1 ) {
+				$_args[ $single_var ] = array_pop( $_args[ $multi_var ] );
+				$_args[ $multi_var ] = '';
+			}
+		}
+
+		$key = md5( serialize( $_args ) );
+
+		/* The following part of the code checks some common combinations of query vars.
+		 * If all non-empty values that actually affect the last_changed date for the query
+		 * belong to one of the special argument combinations, a more specific last_changed
+		 * cache key is used instead of the global one. */
+
+		// Only consider arguments that are not empty and have thus been passed to the query.
+		$used_args = array_filter( $_args );
+
+		// The following arguments affect the query result, but not its last_changed value.
+		$ignore_args = array( 'number', 'offset', 'no_found_rows', 'orderby', 'order', 'count' );
+		$used_args   = array_diff_key( $used_args, array_flip( $ignore_args ) );
+
+		// The following arguments are relevant for a special 'last_changed' prefix.
+		$last_changed_keys = array( 'domain', 'path', 'network_id' );
+
+		$last_changed_args = array();
+		foreach ( $last_changed_keys as $last_changed_key ) {
+			if ( isset( $used_args[ $last_changed_key ] ) ) {
+				$last_changed_args[ $last_changed_key ] = (string) $used_args[ $last_changed_key ];
+				unset( $used_args[ $last_changed_key ] );
+			}
+		}
+
+		// Only use a special 'last_changed' prefix if no other arguments are present.
+		if ( ! empty( $last_changed_args ) && empty( $used_args ) ) {
+			$last_changed_prefix = md5( serialize( $last_changed_args ) ) . '_';
+		} else {
+			$last_changed_prefix = '';
+		}
+
+		$last_changed = wp_cache_get_last_changed( 'sites', $last_changed_prefix );
+
+		return "get_sites:$key:$last_changed";
+	}
 }
Index: src/wp-includes/functions.php
===================================================================
--- src/wp-includes/functions.php	(revision 42373)
+++ src/wp-includes/functions.php	(working copy)
@@ -5952,17 +5952,21 @@
  * Get last changed date for the specified cache group.
  *
  * @since 4.7.0
+ * @since 5.0.0 Now supports the $prefix parameter.
  *
- * @param string $group Where the cache contents are grouped.
+ * @param string $group  Where the cache contents are grouped.
+ * @param string $prefix Optional. Prefix to look for a specific last changed key. Default empty string.
  *
  * @return string $last_changed UNIX timestamp with microseconds representing when the group was last changed.
  */
-function wp_cache_get_last_changed( $group ) {
-	$last_changed = wp_cache_get( 'last_changed', $group );
+function wp_cache_get_last_changed( $group, $prefix = '' ) {
+	$key = $prefix . 'last_changed';
+
+	$last_changed = wp_cache_get( $key, $group );
 
 	if ( ! $last_changed ) {
 		$last_changed = microtime();
-		wp_cache_set( 'last_changed', $last_changed, $group );
+		wp_cache_set( $key, $last_changed, $group );
 	}
 
 	return $last_changed;
Index: src/wp-includes/ms-blogs.php
===================================================================
--- src/wp-includes/ms-blogs.php	(revision 42373)
+++ src/wp-includes/ms-blogs.php	(working copy)
@@ -459,6 +459,7 @@
 				'blog_id' => $blog_id,
 				'domain'  => null,
 				'path'    => null,
+				'site_id' => get_current_network_id(),
 			)
 		);
 	}
@@ -486,7 +487,33 @@
 	 */
 	do_action( 'clean_site_cache', $blog_id, $blog, $domain_path_key );
 
-	wp_cache_set( 'last_changed', microtime(), 'sites' );
+	$current_time = microtime();
+
+	// The following arguments are relevant for a special 'last_changed' prefix.
+	$last_changed_args = array(
+		'domain'     => $blog->domain,
+		'path'       => $blog->path,
+		'network_id' => $blog->site_id,
+	);
+
+	$last_changed_prefixes = array( '' );
+
+	// Generate all possible variations.
+	$combinations = array( array() );
+	foreach ( $last_changed_args as $key => $value ) {
+		$value = (string) $value;
+
+		foreach ( $combinations as $combination ) {
+			$current_args = array_merge( $combination, array( $key => $value ) );
+			$last_changed_prefixes[] = md5( serialize( $current_args ) ) . '_';
+
+			$combinations[] = $current_args;
+		}
+	}
+
+	foreach ( $last_changed_prefixes as $last_changed_prefix ) {
+		wp_cache_set( $last_changed_prefix . 'last_changed', $current_time, 'sites' );
+	}
 
 	/**
 	 * Fires after the blog details cache is cleared.
Index: tests/phpunit/tests/multisite/siteQuery.php
===================================================================
--- tests/phpunit/tests/multisite/siteQuery.php	(revision 42373)
+++ tests/phpunit/tests/multisite/siteQuery.php	(working copy)
@@ -63,6 +63,21 @@
 					'path'    => '/foo/bar/',
 					'site_id' => self::$network_ids['wordpress.org/'],
 				),
+				'wordpress.org/differentnetwork/'      => array(
+					'domain'  => 'wordpress.org',
+					'path'    => '/differentnetwork/',
+					'site_id' => self::$network_ids['make.wordpress.org/'],
+				),
+				'test.wordpress.org/'     => array(
+					'domain'  => 'test.wordpress.org',
+					'path'    => '/',
+					'site_id' => self::$network_ids['wordpress.org/'],
+				),
+				'test.wordpress.org/foo/' => array(
+					'domain'  => 'test.wordpress.org',
+					'path'    => '/foo/',
+					'site_id' => self::$network_ids['wordpress.org/'],
+				),
 				'make.wordpress.org/'     => array(
 					'domain'  => 'make.wordpress.org',
 					'path'    => '/',
@@ -228,7 +243,7 @@
 				array(
 					'fields'     => 'ids',
 					'network_id' => self::$network_ids['wordpress.org/'],
-					'number'     => 3,
+					'number'     => 5,
 					'order'      => 'ASC',
 				)
 			);
@@ -237,6 +252,8 @@
 				self::$site_ids['wordpress.org/'],
 				self::$site_ids['wordpress.org/foo/'],
 				self::$site_ids['wordpress.org/foo/bar/'],
+				self::$site_ids['test.wordpress.org/'],
+				self::$site_ids['test.wordpress.org/foo/'],
 			);
 
 			$this->assertEquals( $expected, $found );
@@ -245,7 +262,7 @@
 				array(
 					'fields'     => 'ids',
 					'network_id' => self::$network_ids['wordpress.org/'],
-					'number'     => 3,
+					'number'     => 5,
 					'order'      => 'DESC',
 				)
 			);
@@ -265,6 +282,7 @@
 			$expected = array(
 				self::$site_ids['make.wordpress.org/'],
 				self::$site_ids['make.wordpress.org/foo/'],
+				self::$site_ids['wordpress.org/differentnetwork/'],
 			);
 
 			$this->assertEqualSets( $expected, $found );
@@ -369,6 +387,7 @@
 				self::$site_ids['wordpress.org/'],
 				self::$site_ids['wordpress.org/foo/'],
 				self::$site_ids['wordpress.org/foo/bar/'],
+				self::$site_ids['wordpress.org/differentnetwork/'],
 				self::$site_ids['make.wordpress.org/'],
 				self::$site_ids['make.wordpress.org/foo/'],
 			);
@@ -390,6 +409,9 @@
 				self::$site_ids['wordpress.org/'],
 				self::$site_ids['wordpress.org/foo/'],
 				self::$site_ids['wordpress.org/foo/bar/'],
+				self::$site_ids['wordpress.org/differentnetwork/'],
+				self::$site_ids['test.wordpress.org/'],
+				self::$site_ids['test.wordpress.org/foo/'],
 				self::$site_ids['make.wordpress.org/'],
 				self::$site_ids['make.wordpress.org/foo/'],
 			);
@@ -408,6 +430,8 @@
 
 			$expected = array(
 				get_current_blog_id(), // Account for the initial site added by the test suite.
+				self::$site_ids['test.wordpress.org/'],
+				self::$site_ids['test.wordpress.org/foo/'],
 				self::$site_ids['make.wordpress.org/'],
 				self::$site_ids['make.wordpress.org/foo/'],
 			);
@@ -657,6 +681,7 @@
 			$expected = array(
 				self::$site_ids['wordpress.org/foo/'],
 				self::$site_ids['wordpress.org/foo/bar/'],
+				self::$site_ids['test.wordpress.org/foo/'],
 				self::$site_ids['make.wordpress.org/foo/'],
 				self::$site_ids['www.w.org/foo/'],
 				self::$site_ids['www.w.org/foo/bar/'],
@@ -878,6 +903,434 @@
 			);
 			$this->assertEquals( $number_of_queries + 1, $wpdb->num_queries );
 		}
+
+		/**
+		 * @ticket 42252
+		 * @dataProvider data_wp_site_query_granular_cache
+		 */
+		public function test_wp_site_query_granular_cache( $query_vars, $site_id, $update_data, $add ) {
+			global $wpdb;
+
+			if ( is_string( $site_id ) ) {
+				$site_id = self::$site_ids[ $site_id ];
+			}
+
+			if ( isset( $query_vars['network_id'] ) && is_string( $query_vars['network_id'] ) ) {
+				$query_vars['network_id'] = self::$network_ids[ $query_vars['network_id'] ];
+			}
+
+			$this->set_last_changed( get_site( $site_id ), current_time( 'timestamp' ) - HOUR_IN_SECONDS );
+
+			$q = new WP_Site_Query();
+
+			$query_1 = $q->query( $query_vars );
+
+			update_blog_details( $site_id, $update_data );
+
+			$number_of_queries = $wpdb->num_queries;
+
+			$query_2 = $q->query( $query_vars );
+
+			$expected = $add ? $number_of_queries + 1 : $number_of_queries;
+
+			$this->assertEquals( $expected, $wpdb->num_queries );
+		}
+
+		public function data_wp_site_query_granular_cache() {
+			$base_domain     = 'wordpress.org';
+			$base_path       = '/';
+			$base_network_id = $base_domain . $base_path;
+
+			$all_different = 'make.wordpress.org/foo/';
+
+			$same_domain     = 'wordpress.org/differentnetwork/';
+			$same_path       = 'make.wordpress.org/';
+			$same_network_id = 'test.wordpress.org/foo/';
+
+			// A combination of $same_domain_path (with different network ID) does not exist.
+			$same_domain_network_id = 'wordpress.org/foo/';
+			$same_path_network_id   = 'test.wordpress.org/';
+
+			$same_domain_path_network_id = 'wordpress.org/';
+
+			$base_data_sets = array(
+
+				// Update for a random site field.
+				array(
+					'query_vars' => array(
+						'fields' => 'ids',
+					),
+					'update_data' => array(
+						'mature' => 1,
+					),
+				),
+
+				// Update for a site field that is sorted by and may change the order.
+				array(
+					'query_vars' => array(
+						'fields'  => 'ids',
+						'orderby' => 'registered',
+						'order'   => 'DESC',
+					),
+					'update_data' => array(
+						'registered' => '0000-00-00 00:00:00',
+					),
+				),
+			);
+
+			$test_data = array();
+			foreach ( $base_data_sets as $base_data_set ) {
+				$comparison_sets = array(
+
+					// General query without any special field.
+					array(
+						'query_vars' => array(),
+						'comparisons' => array(
+							array(
+								'site_id' => $all_different,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_domain,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_path,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_domain_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_path_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_domain_path_network_id,
+								'add'     => true,
+							),
+						),
+					),
+
+					// Query by domain.
+					array(
+						'query_vars' => array(
+							'domain' => $base_domain,
+						),
+						'comparisons' => array(
+							array(
+								'site_id' => $all_different,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_path,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_path_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_path_network_id,
+								'add'     => true,
+							),
+						),
+					),
+
+					// Query by path.
+					array(
+						'query_vars' => array(
+							'path' => $base_path,
+						),
+						'comparisons' => array(
+							array(
+								'site_id' => $all_different,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_domain_path_network_id,
+								'add'     => true,
+							),
+						),
+					),
+
+					// Query by network ID.
+					array(
+						'query_vars' => array(
+							'network_id' => $base_network_id,
+						),
+						'comparisons' => array(
+							array(
+								'site_id' => $all_different,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_domain_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_path_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_domain_path_network_id,
+								'add'     => true,
+							),
+						),
+					),
+
+					// Query by domain and path.
+					array(
+						'query_vars' => array(
+							'domain' => $base_domain,
+							'path'   => $base_path,
+						),
+						'comparisons' => array(
+							array(
+								'site_id' => $all_different,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_path_network_id,
+								'add'     => true,
+							),
+						),
+					),
+
+					// Query by domain and network ID.
+					array(
+						'query_vars' => array(
+							'domain'     => $base_domain,
+							'network_id' => $base_network_id,
+						),
+						'comparisons' => array(
+							array(
+								'site_id' => $all_different,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_path_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_path_network_id,
+								'add'     => true,
+							),
+						),
+					),
+
+					// Query by path and network ID.
+					array(
+						'query_vars' => array(
+							'path'       => $base_path,
+							'network_id' => $base_network_id,
+						),
+						'comparisons' => array(
+							array(
+								'site_id' => $all_different,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path_network_id,
+								'add'     => true,
+							),
+							array(
+								'site_id' => $same_domain_path_network_id,
+								'add'     => true,
+							),
+						),
+					),
+
+					// Query by domain, path and network ID.
+					array(
+						'query_vars' => array(
+							'domain'     => $base_domain,
+							'path'       => $base_path,
+							'network_id' => $base_network_id,
+						),
+						'comparisons' => array(
+							array(
+								'site_id' => $all_different,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_path_network_id,
+								'add'     => false,
+							),
+							array(
+								'site_id' => $same_domain_path_network_id,
+								'add'     => true,
+							),
+						),
+					),
+				);
+
+				foreach ( $comparison_sets as $comparison_set ) {
+					$query_vars = array_merge( $base_data_set['query_vars'], $comparison_set['query_vars'] );
+
+					foreach ( $comparison_set['comparisons'] as $comparison ) {
+						$test_data[] = array(
+							$query_vars,
+							$comparison['site_id'],
+							$base_data_set['update_data'],
+							$comparison['add'],
+						);
+					}
+				}
+			}
+
+			return $test_data;
+		}
+
+		private function set_last_changed( $blog, $current_time ) {
+			/*
+			This function is essentially a copy of the respective code part in `clean_blog_cache()`.
+			It allows to set the last_changed values for a site to a specific time.
+			*/
+
+			// The following arguments are relevant for a special 'last_changed' prefix.
+			$last_changed_args = array(
+				'domain'     => $blog->domain,
+				'path'       => $blog->path,
+				'network_id' => $blog->site_id,
+			);
+
+			$last_changed_prefixes = array( '' );
+
+			// Generate all possible variations.
+			$combinations = array( array() );
+			foreach ( $last_changed_args as $key => $value ) {
+				$value = (string) $value;
+
+				foreach ( $combinations as $combination ) {
+					$current_args = array_merge( $combination, array( $key => $value ) );
+					$last_changed_prefixes[] = md5( serialize( $current_args ) ) . '_';
+
+					$combinations[] = $current_args;
+				}
+			}
+
+			foreach ( $last_changed_prefixes as $last_changed_prefix ) {
+				wp_cache_set( $last_changed_prefix . 'last_changed', $current_time, 'sites' );
+			}
+		}
 	}
 
 endif;
