5

I'm trying to analyze why the following query is slower with LIMIT 0,1 than LIMIT 0,100

I've added SQL_NO_CACHE for testing purposes.

Query:

 SELECT 
  SQL_NO_CACHE  SQL_CALC_FOUND_ROWS wp_posts.*, 
  low_stock_amount_meta.meta_value AS low_stock_amount
FROM 
  wp_posts 
  LEFT JOIN wp_wc_product_meta_lookup wc_product_meta_lookup ON wp_posts.ID = wc_product_meta_lookup.product_id 
  LEFT JOIN wp_postmeta AS low_stock_amount_meta ON wp_posts.ID = low_stock_amount_meta.post_id 
  AND low_stock_amount_meta.meta_key = '_low_stock_amount' 
WHERE 
  1 = 1 
  AND wp_posts.post_type IN ('product', 'product_variation') 
  AND (
    (wp_posts.post_status = 'publish')
  ) 
  AND wc_product_meta_lookup.stock_quantity IS NOT NULL 
  AND wc_product_meta_lookup.stock_status IN('instock', 'outofstock') 
  AND (
    (
      low_stock_amount_meta.meta_value > '' 
      AND wc_product_meta_lookup.stock_quantity <= CAST(
        low_stock_amount_meta.meta_value AS SIGNED
      )
    ) 
    OR (
      (
        low_stock_amount_meta.meta_value IS NULL 
        OR low_stock_amount_meta.meta_value <= ''
      ) 
      AND wc_product_meta_lookup.stock_quantity <= 2
    )
  ) 

ORDER BY 
  wp_posts.ID DESC 
LIMIT 
  0, 1

Explains shows the exact same output

1   SIMPLE  wp_posts    index   PRIMARY,type_status_date    PRIMARY 8   NULL    27071   Using where
1   SIMPLE  low_stock_amount_meta   ref post_id,meta_key    meta_key    767 const   1   Using where
1   SIMPLE  wc_product_meta_lookup  eq_ref  PRIMARY,stock_status,stock_quantity,product_id  PRIMARY 8   woocommerce-admin.wp_posts.ID   1   Using where

The average query time is 350ms with LIMIT 0,1

The average query time is 7ms with LIMIT 0,100

The query performance gets faster starting with LIMIT 0,17

I've added another column to the order by clause as suggested in this question, but that triggers Using filesort in the explain output

Order by wp_posts.post_date, wp_posts.ID desc

1   SIMPLE  wp_posts    ALL PRIMARY,type_status_date    NULL    NULL    NULL    27071   Using where; Using filesort
1   SIMPLE  low_stock_amount_meta   ref post_id,meta_key    meta_key    767 const   1   Using where
1   SIMPLE  wc_product_meta_lookup  eq_ref  PRIMARY,stock_status,stock_quantity,product_id  PRIMARY 8   woocommerce-admin.wp_posts.ID   1   Using where

Is there a way to work around it without altering indices and why is this happening?

It's also interesting that the query time improves starting with LIMIT 0,17. I'm not sure why 17 is a magic number here.

Update 1: I just tried adding FORCE INDEX(PRIMARY) and now LIMIT 0,100 has same performance as LIMIT 0,1 smh

O. Jones
  • 103,626
  • 17
  • 118
  • 172
Moon
  • 22,195
  • 68
  • 188
  • 269
  • Usually the second time is best. The _first_ time includes fetching things from disk. – Rick James Jul 16 '21 at 23:02
  • @RickJames but I have SQL_NO_CACHE and the difference is 350ms vs 7ms and running LIMIT 0,17 is fast even for the first time. – Moon Jul 16 '21 at 23:05
  • The buffer pool is also a cache. On a fresh system, none of posts and postmeta is loaded until after the first query. So, if the first query test took several seconds, the "average" would be abnormally high, even if the rest were very low. (And, yes, it was good to avoid the "Query cache".) – Rick James Jul 16 '21 at 23:09
  • @RickJames I get what you're saying, but it still does not explain the question of why LIMIT 0, 17 is vastly faster than 0,1 even for the first query. I've even tried to reboot the system to make the system cold. – Moon Jul 16 '21 at 23:20
  • Please verify again that your execution plans are actually identical, as this should only happen if they are not. Also, you mentioned that you added "force index(primary)" (where exactly?), are the queries then equally fast or equally slow? What happens if you force all 3 indexes shown in your execution plan? – Solarflare Jul 17 '21 at 01:01
  • @Solarflare I've triple confirmed already. When I add the index, they are equally slow. – Moon Jul 17 '21 at 01:12
  • @Moon, some questions for you. Does this query come from someplace in WooCommerce, or is it your own (pllugin / snippet) code? How many rows (order of magnitude) in wp_posts, wp_postmeta, and wp_wc_product_meta_lookup? – O. Jones Jul 18 '21 at 11:46
  • @O.Jones Thank you. Yes, it's from WooCommerce. I'm gonna update the original post, but the performance is the same in MySQL 5.7 or MariaDB. Limit 0,1 execution time is at around 150ms, which is the same as Limit 0,17. My local setup with MySQL 5.6 only shows this weird behavior where Limit 0,17+ is vastly faster SMH – Moon Jul 19 '21 at 00:29
  • @O.Jones my postmeta has 979392 rows, posts table has 31995, wp_wc_product_meta_lookup has 31995 rows. – Moon Jul 19 '21 at 00:30
  • A million rows in postmeta for Woo? Use the plugin. Seriously. It *will* make your store faster. And, with respect don't waste too much effort trying to figure out performance oddities in old versions of megalines-of-code software like MySQL. – O. Jones Jul 19 '21 at 09:10
  • I think that `varchar <= ''`` will _always_ be `false`, even for `NULL` and negative numbers. `varchar <= 0`` is a different matter. – Rick James Jul 19 '21 at 17:34

2 Answers2

1

wp_postmeta has sloppy indexes; this slows down most queries involving it.

O. Jones and I have made a WordPress plugin to improve the indexing of postmeta. We detect all sorts of stuff like the presence of the Barracuda version of the InnoDB storage engine, and other MySQL arcana, and do the right thing.

The may speed up all three averages. It is likely to change the EXPLAINs.

Rick James
  • 135,179
  • 13
  • 127
  • 222
  • Thanks, Rick, but I'm not sure what the plugin does. My `wp_postmeta` table already has index for meta_id, post_id, meta_key, and meta_value. It looks like plugins adds the same index to the table, which I already have. – Moon Jul 16 '21 at 23:28
  • Single column indexes is _not_ the same as "composite" (multi-column) indexes. – Rick James Jul 17 '21 at 05:51
  • Rick James // Ah! I see what you're trying to do. Unfortunately, it did not help in my case. – Moon Jul 17 '21 at 16:07
  • Did the `EXPLAINs` change? – Rick James Jul 17 '21 at 17:48
  • Rick James // No, it did not. As you see in my original post, the output of EXPLAIN is pretty good without any warning signs such as `using temporary` or `using filesort`. – Moon Jul 18 '21 at 01:11
  • Interesting. And weird. Maybe the plugin needs to put a compound indes or two on that `wp_wc_product_meta_lookup` table; it has one row per WooCommerce SKU. – O. Jones Jul 18 '21 at 11:43
  • Moon, would you be willing to run our plugin's indexing operation again and say "yes" to uploading the diagnostic metadata so we can see it? @RickJames and I are committed (volunteers) trying to help site owners resolve these kinds ofproblems. – O. Jones Jul 18 '21 at 11:49
0

Analyzing this query. I confess I don't understand the performance change from LIMIT 1 to LIMIT 17. Still, the problem for your store's customers (or managers) is the slowness on LIMIT 1. So let's address that.

The question you linked was for postgreSQL, not MySQL. postgreSQL has a more sophisticated way of handling ORDER BY ... LIMIT 1 than MySQL does. And, the resolution to that problem was the adding of an appropriate compound index for the required lookup.

It looks to me like the purpose of your query is to find the low-stock or out-of-stock WooCommerce product with the largest wp_posts.ID

The LEFT JOIN to the wp_wc_product_meta_lookup table should be, and is, straightforward: the ON-condition column mentioned is its primary key. This table is, basically, WooCommerce's materialized view of numeric values like stock_quantity stored in wp_postmeta. Numeric values in wp_postmeta can't be indexed because that table stores them as text strings. Yeah. I know.

The LEFT JOIN between wp_posts and wp_postmeta follows the very common ON-condition pattern ON posts.ID = meta.post_id AND meta.meta_key = 'constant'. That ON condition is notorious for poor support by WordPress's standard indexes. More or less the entire purpose of Rick and my Index WP MySQL For Speed plugin is to provide good compound indexes in wp_postmeta to work around that problem.

How so? This is the DDL it runs to add the indexes. The most important lines for this purpose: ((There's more to it, read the linked article.)

ALTER TABLE wp_postmeta ADD PRIMARY KEY (post_id, meta_key, meta_id);
ALTER TABLE wp_postmeta ADD KEY meta_key (meta_key, post_id);

These two indexes support the ON-condition pattern in the query. I am pretty sure that adding theses keys to postmeta will make your query more predictable and faster in performance.

If the ORDER BY post.ID DESC is a very common use case, an index could be added for that.

You could try refactoring the query (if you have control over its source) to defer the retrieval of details from the wp_posts table. Like this.

SELECT wp_posts.*, postid.low_stock_amount
 FROM (
   wp_posts.ID, low_stock_amount_meta.meta_value AS low_stock_amount
FROM 
  wp_posts 
  LEFT JOIN wp_wc_product_meta_lookup wc_product_meta_lookup ON wp_posts.ID = wc_product_meta_lookup.product_id 
  LEFT JOIN wp_postmeta AS low_stock_amount_meta ON wp_posts.ID = low_stock_amount_meta.post_id 
  AND low_stock_amount_meta.meta_key = '_low_stock_amount' 
WHERE 
  1 = 1 
  AND wp_posts.post_type IN ('product', 'product_variation') 
  AND (
    (wp_posts.post_status = 'publish')
  ) 
  AND wc_product_meta_lookup.stock_quantity IS NOT NULL 
  AND wc_product_meta_lookup.stock_status IN('instock', 'outofstock') 
  AND (
    (
      low_stock_amount_meta.meta_value > '' 
      AND wc_product_meta_lookup.stock_quantity <= CAST(
        low_stock_amount_meta.meta_value AS SIGNED
      )
    ) 
    OR (
      (
        low_stock_amount_meta.meta_value IS NULL 
        OR low_stock_amount_meta.meta_value <= ''
      ) 
      AND wc_product_meta_lookup.stock_quantity <= 2
    )
  ) 

ORDER BY 
  wp_posts.ID DESC 
LIMIT 
  0, 1
) postid
LEFT JOIN wp_posts ON wp_posts.ID = postid.ID

This refactoring makes your complex query sort only the wp_posts.ID value and then retrieves the posts data once it has the appropriate value in hand. Lots of WordPress core code does something similar: retrieves a list of post ID values in one query, then retrieves the post data in a second query.

And, by the way, MySQL 8 ignores SQL_NO_CACHE.

Rick James
  • 135,179
  • 13
  • 127
  • 222
O. Jones
  • 103,626
  • 17
  • 118
  • 172