MySQL’s optimiser hints (available since 5.7, extended in 8.0) let you override the query planner’s decisions without changing session variables or schema — they are embedded directly in the query with /*+ HINT */ syntax and affect only that specific statement. For WordPress performance work, hints are invaluable when the optimiser consistently makes a bad plan choice that cannot be fixed with index changes alone.
Problem: The MySQL query optimiser makes poor index choices on complex WordPress queries — choosing a full table scan over an available index, or merging indexes in a suboptimal way — and there is no obvious way to override the optimiser without changing the query.
Solution: Use MySQL optimiser hints directly in the SQL query: SELECT /*+ INDEX(wp_posts idx_post_date) */ ... forces index selection, /*+ NO_RANGE_OPTIMIZATION(...) */ disables range scans on a specific index, and /*+ SEMIJOIN() */ controls subquery strategy. Hints are ignored gracefully on older MySQL versions. Use EXPLAIN FORMAT=TREE to verify the chosen plan.
The examples below demonstrate index selection hints on wp_posts, disabling index merge for a query that performs better with a single index, the SET_VAR hint for statement-level variable overrides, and the JOIN_ORDER hint for complex WooCommerce meta joins.
-- ── 1. Force a specific index ─────────────────────────────────────────────
-- Without hint: optimiser may choose a suboptimal index on wp_posts
SELECT /*+ INDEX(p type_status_date) */ ID, post_title
FROM wp_posts p
WHERE post_type = 'product'
AND post_status = 'publish'
ORDER BY post_date DESC
LIMIT 20;
-- ── 2. Ignore a specific index (prevent bad index choice) ────────────────
SELECT /*+ NO_INDEX(pm PRIMARY) */ p.ID, p.post_title
FROM wp_posts p
JOIN wp_postmeta pm ON pm.post_id = p.ID
WHERE pm.meta_key = '_price'
AND pm.meta_value > '100'
AND p.post_status = 'publish';
-- ── 3. Disable index merge (when ALL reads perform better than bitwise AND)
SELECT /*+ NO_INDEX_MERGE(p) */ ID
FROM wp_posts p
WHERE post_type = 'product' OR post_status = 'draft';
-- ── 4. SET_VAR hint: override a session variable for one query only ───────
-- Increase sort buffer for a large ORDER BY without changing the session
SELECT /*+ SET_VAR(sort_buffer_size = 4194304) */
p.ID,
p.post_title,
MAX( CAST(pm.meta_value AS DECIMAL(10,2)) ) AS price
FROM wp_posts p
JOIN wp_postmeta pm ON pm.post_id = p.ID AND pm.meta_key = '_price'
WHERE p.post_type = 'product'
GROUP BY p.ID
ORDER BY price DESC
LIMIT 100;
-- ── 5. JOIN_ORDER hint: force a specific join order ───────────────────────
-- When WooCommerce joins order_items → order_itemmeta → posts in bad order
SELECT /*+ JOIN_ORDER(oi, oim, p) */
oi.order_id,
oim.meta_value AS product_qty,
p.post_title AS product_name
FROM wp_woocommerce_order_items oi
JOIN wp_woocommerce_order_itemmeta oim ON oim.order_item_id = oi.order_item_id
JOIN wp_posts p ON p.ID = oim.meta_value
WHERE oi.order_item_type = 'line_item'
AND oim.meta_key = '_product_id';
-- ── 6. Verify the hint was accepted ───────────────────────────────────────
EXPLAIN FORMAT=JSON
SELECT /*+ INDEX(p type_status_date) */ ID FROM wp_posts p
WHERE post_type='post' AND post_status='publish';
-- Look for "index_name": "type_status_date" in the JSON output
NOTE: Optimiser hints bypass the query planner's statistics — if your table data changes significantly (e.g., a bulk import doubles the row count), a hardcoded hint may force a plan that was optimal before the import but is now suboptimal; audit hinted queries after major data changes with EXPLAIN ANALYZE to verify the forced plan is still the best choice.