Using Optimizer Hints Deliberately, Without Outsmarting the Optimizer
AI generated
InnoDB
SQL
MySQL · Optimizer · Query Tuning · Best Practices
Using Optimizer Hints Deliberately
without outsmarting the optimizer

Optimizer hints such as USE INDEX, FORCE INDEX, and STRAIGHT_JOIN intervene directly in the query optimizer's decisions and can solve real problems with stale statistics, but they can also create new ones when applied carelessly. This article shows the syntax, the effect, and the clear line between legitimate optimization and risky code smell.

19 min read USE INDEX · FORCE INDEX · STRAIGHT_JOIN · /*+ ... */ MySQL 5.7 · MySQL 8.0

1. What the optimizer does by default and when it errs

The MySQL query optimizer automatically decides for each query which execution plan is likely to be most efficient, based on table statistics, index cardinality, and estimated costs for different access paths. This decision is, as a rule, made correctly by the optimizer, because modern cost models account for factors like I/O cost, CPU cost, and the selectivity of filter conditions. In the vast majority of cases, manual intervention via optimizer hints is neither necessary nor advisable.

The optimizer does, however, err systematically in certain recurring situations. Stale table statistics after large bulk inserts or deletes without a subsequent ANALYZE TABLE lead to incorrect cardinality estimates. Heavily skewed data distributions, where one index value covers ninety percent of all rows and another covers only one percent, can overwhelm the default histograms in some versions. Complex queries with many JOINs can also end up choosing a suboptimal join order, because the search space for the perfect order grows exponentially and the optimizer stops after a bounded number of permutations.

Optimizer hints exist precisely for these exceptional cases, targeted instructions to the optimizer to enforce or restrict a specific decision instead of fully trusting the automatic cost estimate. Importantly, the order of measures matters: a hint should only ever be applied after a confirmed misjudgment by the optimizer, proven through EXPLAIN, not preemptively out of uncertainty.


-- Check what the optimizer chose and why, before reaching for a hint
EXPLAIN SELECT * FROM orders
WHERE customer_id = 42 AND status = 'pending';
-- +----+-------+---------------+------+---------+------+
-- | id | table | possible_keys | key  | key_len | rows |
-- +----+-------+---------------+------+---------+------+
-- |  1 | orders| idx_customer  | NULL | NULL    | 84021|
-- +----+-------+---------------+------+---------+------+
-- possible_keys shows idx_customer, but key is NULL:
-- the optimizer chose a full table scan instead

2. USE INDEX, FORCE INDEX, and IGNORE INDEX

The classic index hints, USE INDEX, FORCE INDEX, and IGNORE INDEX, have existed since very early MySQL versions and are written directly after the table name in the FROM clause. USE INDEX(idx_name) restricts the optimizer's selection to the named indexes but still leaves it to decide whether an index or a table scan is chosen at all. FORCE INDEX(idx_name) goes a step further and forces the optimizer to use the named index, even if its own cost estimate considers a table scan cheaper.

IGNORE INDEX(idx_name) explicitly excludes a specific index from consideration, useful in situations where the optimizer incorrectly favors an unsuitable index while another, unnamed index or a table scan would actually be faster. All three hints additionally accept a context specification, such as FORCE INDEX FOR JOIN or FORCE INDEX FOR ORDER BY, to make the hint apply only to a specific part of query processing.


-- USE INDEX: narrows the candidate set, optimizer still decides
SELECT * FROM orders USE INDEX (idx_customer_status)
WHERE customer_id = 42 AND status = 'pending';

-- FORCE INDEX: overrides the optimizer's own cost estimate
SELECT * FROM orders FORCE INDEX (idx_customer_status)
WHERE customer_id = 42 AND status = 'pending';

-- IGNORE INDEX: excludes a specific index from consideration
SELECT * FROM orders IGNORE INDEX (idx_status)
WHERE customer_id = 42 AND status = 'pending';

-- Scoped to a specific phase of query processing
SELECT * FROM orders FORCE INDEX FOR ORDER BY (idx_created_at)
WHERE customer_id = 42 ORDER BY created_at DESC;

3. STRAIGHT_JOIN and join order

STRAIGHT_JOIN forces the optimizer to join the tables in exactly the order given in the SQL statement, instead of computing what it considers a better order itself. The optimizer normally chooses join order based on estimated intermediate result sizes, to minimize the total number of row combinations to be processed. For complex queries with more than five or six tables, however, the search space for all possible join orders grows so large that the optimizer, for performance reasons, only actually evaluates a subset of the permutations.

In such cases, a join order manually chosen by an experienced developer, combined with STRAIGHT_JOIN, can indeed be more efficient than the automatically chosen one, particularly when the developer can judge the actual selectivity of the filter conditions better than the possibly stale statistics used by the optimizer. Importantly, STRAIGHT_JOIN applies to the entire query and therefore requires the table order in the statement to be deliberately chosen from small to large, or from the most restrictive to the least restrictive filter.


-- Optimizer picks the join order automatically
SELECT o.id, c.name, p.title
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN products p ON p.id = o.product_id
WHERE c.country = 'DE';

-- STRAIGHT_JOIN forces the exact order written: customers first
-- (useful when country='DE' is highly selective and the optimizer
-- misjudges it due to stale statistics)
SELECT STRAIGHT_JOIN c.name, o.id, p.title
FROM customers c
JOIN orders o ON o.customer_id = c.id
JOIN products p ON p.id = o.product_id
WHERE c.country = 'DE';

4. The optimizer hint syntax since MySQL 8

MySQL 8.0 introduced a significantly more fine grained and modern hint syntax, written as a comment directly after the SELECT keyword in the form /*+ HINT_NAME(args) */. This syntax does not replace the classic index hints but adds considerably more control, such as JOIN_ORDER for explicitly specifying join order without physically reordering the SQL statement, INDEX and NO_INDEX as more modern alternatives to FORCE INDEX and IGNORE INDEX, as well as MAX_EXECUTION_TIME for capping the maximum execution time of a single query.

The decisive advantage of this new syntax over the classic hints is that it is parsed as a comment and is therefore ignored by older MySQL versions or other database systems instead of being rejected as a syntax error, which makes portable codebases easier. Multiple hints can also be combined in a single comment block, allowing more complex, targeted interventions in a single, clearly readable line.


-- Modern hint syntax (MySQL 8.0+): comment-based, ignored by older parsers
SELECT /*+ INDEX(orders idx_customer_status) */ *
FROM orders
WHERE customer_id = 42 AND status = 'pending';

-- Explicit join order without rewriting table order in FROM
SELECT /*+ JOIN_ORDER(c, o, p) */ c.name, o.id, p.title
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN products p ON p.id = o.product_id
WHERE c.country = 'DE';

-- Bounding a potentially runaway query in an admin tool
SELECT /*+ MAX_EXECUTION_TIME(2000) */ * FROM orders
WHERE YEAR(created_at) = 2024;

5. Index hints versus refreshed statistics

Before even considering an optimizer hint, the most obvious cause for the optimizer's wrong decisions should be ruled out: stale table statistics. ANALYZE TABLE refreshes the cardinality estimates on which the optimizer builds its cost calculation, and in practice fixes a significant share of cases where developers reach for a hint prematurely. After large data imports, bulk deletes, or significant shifts in data distribution, a manual or automated ANALYZE TABLE run is often the simpler and more robust solution than a hint permanently embedded in the code.

The difference in maintainability is significant: refreshed statistics automatically adapt to future data distributions, while a FORCE INDEX hint hardcoded into SQL must be manually re-verified after every schema change or shift in data distribution. The rule of thumb is therefore to first try ANALYZE TABLE and verify the effect with EXPLAIN before adding a hint as a permanent fixture in the code.

Measure Effect Maintenance burden Recommendation
ANALYZE TABLE Refreshes statistics, optimizer decides anew Low, adapts automatically First step before any hint
USE INDEX Narrows candidates, optimizer decides Medium Good with several similarly good indexes
FORCE INDEX Forces the index, overrides cost estimate High, risk on schema change Only after EXPLAIN evidence
STRAIGHT_JOIN Forces the entire join order High, re-check after data growth Only for complex multi-table joins

6. When a hint is a code smell

An optimizer hint becomes a code smell as soon as it is applied without prior diagnosis using EXPLAIN, essentially as a reflexive reaction to a query that feels slow. Equally problematic is a hint that papers over a fundamentally flawed schema design, such as a missing composite index, instead of simply adding it. A FORCE INDEX pointing at a suboptimal index in such cases is only a patch over a deeper structural weakness that will resurface sooner or later once the data distribution changes.

Particularly risky are hints that remain in the code without documentation and without an accompanying comment. A later developer trying to optimize the query sees the hint but does not understand which specific misjudgment of the optimizer it was originally meant to correct, and therefore cannot judge whether the hint is still needed after an upgrade or a schema change. Every optimizer hint in production code should therefore always be accompanied by a comment documenting the optimizer's original misjudgment and the date of the EXPLAIN analysis.

7. When a hint is legitimate

An optimizer hint is legitimate when three conditions are met simultaneously: first, the optimizer's misjudgment has been concretely demonstrated through EXPLAIN or EXPLAIN ANALYZE, not merely suspected. Second, simpler solutions such as refreshed statistics or a newly created index have already been tried and found insufficient. Third, the hint is documented, with a comment explaining why it is needed and when it was last reviewed.

Typical legitimate use cases are reporting queries with heavily skewed data distributions, where the optimizer systematically misestimates due to insufficient histogram granularity, as well as complex multi-table joins in batch processes where the developer knows the data distribution better than the statistics available at query time. MAX_EXECUTION_TIME as a defensive measure against runaway queries in admin tools is also a clearly legitimate, low risk use of a hint, because it does not override optimization logic but merely sets an upper bound.

Mironsoft

Query optimization and index strategy for MySQL

FORCE INDEX in the code, but nobody remembers why?

We review existing optimizer hints for necessity, document the underlying EXPLAIN findings, and remove hints that have become obsolete thanks to refreshed statistics or better indexes.

Hint audit

Reviewing existing hints in the codebase for necessity and currency

Index strategy

Composite indexes instead of risky FORCE INDEX crutches

EXPLAIN reviews

Solid diagnosis before any manual intervention in the optimizer

8. Hints and maintainability under schema changes

A central risk of optimizer hints lies in their fragility with respect to schema changes. If an index referenced by a FORCE INDEX is renamed or removed, the affected query fails with a clear error, which at first glance seems safer than a silent performance loss, but in practice can cause unplanned outages after otherwise harmless maintenance work. A team that drops a seemingly unused index without knowing it is referenced in a FORCE INDEX hint unintentionally produces a production outage.

The modern hint syntax with /*+ ... */ partially mitigates this risk, because invalid or no longer applicable hints are, in many cases, treated as a warning instead of an error, allowing the query to keep running, albeit possibly with a suboptimal plan. Regardless of the chosen syntax, however, the rule holds: any migration that affects an index referenced by a hint must adapt the affected application code as part of the same change, not as a follow up task.


-- Documented hint: the only acceptable way to keep one in production code
-- Reason: optimizer chose a full scan on skewed status distribution
-- Verified with EXPLAIN ANALYZE on 2026-06-14, MySQL 8.0.36
SELECT /*+ INDEX(orders idx_status_created) */ id, total
FROM orders
WHERE status = 'refunded'
ORDER BY created_at DESC
LIMIT 100;

-- Review checklist before removing a legacy FORCE INDEX hint:
-- 1. Run ANALYZE TABLE orders;
-- 2. Compare EXPLAIN with and without the hint
-- 3. If plans match or the hint-free plan is equal/better, remove it

9. Validating hints regularly instead of forgetting them

An optimizer hint that is correct today may become unnecessary or even counterproductive a year from now due to grown data volume, improved optimizer versions, or changed access patterns. The MySQL optimizer is developed further with every major version, so a hint that corrected a misjudgment in MySQL 5.7 may, after an upgrade to MySQL 8.0, end up blocking a by then better automatic decision.

A periodic review process is recommended, for example once a quarter or after every major upgrade, in which all optimizer hints present in the code are systematically listed and compared against the current state without the hint using a fresh EXPLAIN ANALYZE. If it turns out that the optimizer now independently reaches the same or a better decision, the hint should be removed, keeping the codebase lean and giving the optimizer back its full decision making freedom.

10. Summary

Optimizer hints such as USE INDEX, FORCE INDEX, STRAIGHT_JOIN, and the modern /*+ ... */ syntax since MySQL 8 are powerful tools, but no substitute for clean schema design and current table statistics. The optimizer gets the vast majority of its decisions right, which is why a hint should only ever be considered after a concrete misjudgment demonstrated with EXPLAIN, never preemptively or reflexively.

Legitimate uses are characterized by three traits: a demonstrated misjudgment, already exhausted simpler alternatives such as ANALYZE TABLE, and complete documentation in the code. Without this discipline, optimizer hints quickly become a code smell that papers over deeper schema problems and turns into a trap during future migrations. A regular review process ensures that old hints do not remain permanently in the code long after the optimizer itself has become better.

Using optimizer hints deliberately: the essentials at a glance

Diagnose first

Always use EXPLAIN before a hint, never react reflexively to a slow query.

Simpler fix first

Check ANALYZE TABLE and missing indexes before choosing a hint as the fix.

Always document

Every hint needs a comment with the reason and date of the last EXPLAIN review.

Validate periodically

Check after major upgrades and regularly whether the hint is still needed.

11. FAQ: Using Optimizer Hints Deliberately

1USE INDEX vs. FORCE INDEX?
USE INDEX narrows the selection, the optimizer decides. FORCE INDEX forces the index even against the cost estimate.
2When to use STRAIGHT_JOIN?
In complex multi-table joins when a manual order is demonstrably more efficient.
3Modern hint syntax since MySQL 8?
Comment form /*+ HINT_NAME(args) */ after SELECT, fine grained and backward compatible.
4Try ANALYZE TABLE first?
Yes, stale statistics are one of the most common causes of misjudgments, often fixable without a hint.
5When is a hint a code smell?
When applied without EXPLAIN diagnosis, papers over flawed schema design, or is undocumented.
6When is a hint legitimate?
With a demonstrated misjudgment, exhausted alternatives, and complete documentation.
7Index referenced by FORCE INDEX dropped?
The query fails. Migrations must update the referencing code.
8What does MAX_EXECUTION_TIME do?
Caps a query's maximum execution time, a defensive measure against runaway queries.
9Why validate regularly?
The optimizer keeps evolving, a once necessary hint can later become counterproductive.
10Are hints portable?
No, MySQL specific. The comment syntax is ignored by other systems instead of rejected.