Skip to content

Add paragraph on the performance of meta_queries using EXISTS/NOT_EXISTS  #355

@moraleida

Description

@moraleida

Is your enhancement related to a problem? Please describe.

It is very common that developers introduce new meta_keys without setting default values for posts that already existed in the database, or that they delete meta_keys when the value is not needed instead of setting it to a default or false value.

This becomes an issue on larger database tables when users need to find some information by filtering not only by the meta_key and value but also sometimes by checking if that meta_key does not exist at all. This use case is covered in WP_Meta_Query, allowing for the use of compare => 'NOT EXISTS'

This solution should, however, be discouraged, as it generates very expensive queries that can really hammer a database performance.

Example:

  • Database with 253k rows in wp_posts and 5 million rows in wp_postmeta
  • There are 51,406 rows with the _publicize_pending meta_key in wp_postmeta
/**
 * Using NOT_EXISTS in a Meta Query generates a LEFT JOIN clause
 * with an IS NULL check in WHERE
 *
 * Rows searched: 268,358 (all rows in the join)
 */
$query = new \WP_Query(
	array(
		'suppress_filters' => true,
		'meta_query'       => array(
			array(
				'key'     => '_publicize_pending',
				'compare' => 'NOT EXISTS',
			),
		),
	) );

Generates:

MySQL [wordpress]> EXPLAIN SELECT SQL_CALC_FOUND_ROWS  wp_posts.ID FROM wp_posts  
    -> LEFT JOIN wp_postmeta ON (wp_posts.ID = wp_postmeta.post_id AND wp_postmeta.meta_key = 'some_key' ) 
    -> WHERE 1=1  
    -> AND ( wp_postmeta.post_id IS NULL ) 
    -> GROUP BY wp_posts.ID ORDER BY wp_posts.post_date DESC LIMIT 0, 10;
+----+-------------+-------------+------------+-------+-----------------------------------------------------------------+---------+---------+-----------------------+--------+----------+---------------------------------+
| id | select_type | table       | partitions | type  | possible_keys                                                   | key     | key_len | ref                   | rows   | filtered | Extra                           |
+----+-------------+-------------+------------+-------+-----------------------------------------------------------------+---------+---------+-----------------------+--------+----------+---------------------------------+
|  1 | SIMPLE      | wp_posts    | NULL       | index | PRIMARY,post_name,type_status_date,post_parent,post_author,guid | PRIMARY | 8       | NULL                  | 268339 |   100.00 | Using temporary; Using filesort |
|  1 | SIMPLE      | wp_postmeta | NULL       | ref   | post_id,meta_key,meta_key_meta_value                            | post_id | 8       | wordpress.wp_posts.ID |     19 |   100.00 | Using where; Not exists         |
+----+-------------+-------------+------------+-------+-----------------------------------------------------------------+---------+---------+-----------------------+--------+----------+---------------------------------+

In contrast, this query checking for a specific value in the declared meta_key only needs to read 89,970 rows, or roughly 70% fewer rows.

/**
 * Querying only for the meta_key and value generates an INNER JOIN clause
 * with a key/value match in WHERE
 *
 * Rows searched: 89,970
 */
$query = new \WP_Query(
	array(
		'suppress_filters' => true,
		'meta_query'       => array(
			array(
				'key'     => '_publicize_pending',
				'value'     => '1',
				'compare' => '!=',
			),
		),
	) );

Generates:

MySQL [wordpress]> EXPLAIN SELECT wp_posts.ID
    -> FROM wp_posts
    -> INNER JOIN wp_postmeta
    -> ON ( wp_posts.ID = wp_postmeta.post_id )
    -> WHERE 1=1
    -> AND ( ( wp_postmeta.meta_key = '_publicize_pending'
    -> AND wp_postmeta.meta_value != '1' ) );
+----+-------------+-------------+------------+--------+--------------------------------------+---------------------+---------+-------------------------------+-------+----------+-------------+
| id | select_type | table       | partitions | type   | possible_keys                        | key                 | key_len | ref                           | rows  | filtered | Extra       |
+----+-------------+-------------+------------+--------+--------------------------------------+---------------------+---------+-------------------------------+-------+----------+-------------+
|  1 | SIMPLE      | wp_postmeta | NULL       | range  | post_id,meta_key,meta_key_meta_value | meta_key_meta_value | 1534    | NULL                          | 89970 |   100.00 | Using where |
|  1 | SIMPLE      | wp_posts    | NULL       | eq_ref | PRIMARY                              | PRIMARY             | 8       | wordpress.wp_postmeta.post_id |     1 |   100.00 | Using index |
+----+-------------+-------------+------------+--------+--------------------------------------+---------------------+---------+-------------------------------+-------+----------+-------------+
2 rows in set, 1 warning (0.001 sec)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions