Improving a query using a lot of inner joins to wp_postmeta, a key/value table Improving a query using a lot of inner joins to wp_postmeta, a key/value table wordpress wordpress

Improving a query using a lot of inner joins to wp_postmeta, a key/value table


It seems you are trying to obtain a result set with one row per post of type car. It seems you want to display various attributes of each car in the post, and those are stashed away in postmeta.

Pro tip: Never use SELECT * in software unless you absolutely know why you're doing it. Especially with queries containing lots of JOIN operations, SELECT * returns lots of pointless and redundant columns.

There's a query design trick to know for the WordPress postmeta table. If you want to get a particular attribute, do this:

 SELECT p.ID, p.post_title,        color.meta_value AS color   FROM wp_posts AS p   LEFT JOIN wp_postmeta AS color ON p.ID = color.post_id AND 'color' = color.meta_key  WHERE p.post_status = 'publish'    AND /* etc etc */

It's super-important to understand this pattern when doing what you're trying to do. This pattern is required because postmeta is a peculiar type of table called a or store. What's going on here? A few things:

  1. Using this pattern uou get one row for each post, with some columns from the posts table and a particular attribute from the postmeta table.
  2. You are LEFT JOINing the postmeta table so you still get a row if the attribute is missing.
  3. You are using an alias name for the postmeta table. Here it's postmeta AS color.
  4. You are including the selector for meta_key (here it's 'color' = color.meta_key) in the ON condition of the join.
  5. You are using an alias in your SELECT clause to present the postmeta.meta_value item with an appropriate column name. Here it's color.meta_value AS color.

Once you get used to employing this pattern, you can stack it up, with a cascade of LEFT JOIN operations, to get lots of different attributes, like so.

     SELECT wp_posts.ID, wp_posts.post_title, wp_posts.whatever,            color.meta_value        AS color,            transmission.meta_value AS transmission,            model.meta_value        AS model,            brand.meta_value        AS brand       FROM wp_posts  LEFT JOIN wp_postmeta  AS color          ON wp_posts.ID = color.post_id        AND color.meta_key='color'  LEFT JOIN wp_postmeta  AS transmission         ON wp_posts.ID = transmission.post_id AND transmission.meta_key='transmission'  LEFT JOIN wp_postmeta  AS model         ON wp_posts.ID = model.post_id        AND model.meta_key='model'  LEFT JOIN wp_postmeta  AS  brand         ON wp_posts.ID = brand.post_id        AND brand.meta_key='brand'      WHERE wp_posts.post_status = 'publish'        AND wp_posts.post_type = 'car'   ORDER BY wp_posts.post_title

I've done a bunch of indenting on this query to make it easier to see the pattern. You may prefer a different indenting style.

It's hard to know why you were having performance problems with the query in your question. It's possibly because you were getting a combinatorial explosion with all the INNER JOIN operations that was then filtered. But at any rate the query you showed was probably returning no rows.

If you are still having performance trouble, try creating a compound index on postmeta on the (post_id, meta_key, meta_value) columns. If you're creating a WordPress plugin, that's probably a job to do at plugin installation time.


This is a Wordpress database, and you might be reluctant to make extensive changes to the schema, because it could break other parts of the application or complicate upgrades in the future.

The difficulty of this query shows one of the downsides to the design. That design is flexible in that it allows for new attributes to be created at runtime, but it makes a lot of queries against such data more complex than they would be with a conventional table.

The schema for Wordpress has not been optimized well. There are some naive indexing mistakes, even in the most current version 4.0.

For this particular query, the following two indexes help:

CREATE INDEX `bk1` ON wp_postmeta (`post_id`,`meta_key`,`meta_value`(255));CREATE INDEX `bk2` ON wp_posts (`post_type`,`post_status`,`post_title`(255));

The bk1 index helps to look up exactly the right meta key and value.

The bk2 index helps to avoid the filesort.

These indexes can't be covering indexes, because post_title and meta_value are TEXT columns, and these are too long to be fully indexed. You'd have to change them to VARCHAR(255). But that risks breaking the application, if it's depending on storing longer strings in that table.

+----+-------------+--------------+------------+------+---------------+------+---------+----------------------------+------+----------+-----------------------+| id | select_type | table        | partitions | type | possible_keys | key  | key_len | ref                        | rows | filtered | Extra                 |+----+-------------+--------------+------------+------+---------------+------+---------+----------------------------+------+----------+-----------------------+|  1 | SIMPLE      | wp_posts     | NULL       | ref  | bk2           | bk2  | 124     | const,const                |    1 |   100.00 | Using index condition ||  1 | SIMPLE      | color        | NULL       | ref  | bk1           | bk1  | 1542    | wp.wp_posts.ID,const,const |    1 |   100.00 | Using index           ||  1 | SIMPLE      | transmission | NULL       | ref  | bk1           | bk1  | 1542    | wp.wp_posts.ID,const,const |    1 |   100.00 | Using index           ||  1 | SIMPLE      | model        | NULL       | ref  | bk1           | bk1  | 1542    | wp.wp_posts.ID,const,const |    1 |   100.00 | Using index           ||  1 | SIMPLE      | brand        | NULL       | ref  | bk1           | bk1  | 1542    | wp.wp_posts.ID,const,const |    1 |   100.00 | Using index           |+----+-------------+--------------+------------+------+---------------+------+---------+----------------------------+------+----------+-----------------------+


To resolve performance issue with 10+ joins SQL queries on innodb tables using utf8 charset, create a new index on postmeta :

Backup database first. Reduce [wp_]postmeta.meta_key length to 191 to avoid "Specified key was too long; max key length is 767 bytes" error.

ALTER TABLE wp_postmeta MODIFY meta_key VARCHAR(191);

Create index

CREATE INDEX wpm_ix ON wp_postmeta (post_id, meta_key);