Parameters
optimizer_max_subgraph_pairs cap.Join-order search animation
Example query this animation executes
-- Seven-table join: the planner must pick an order before execution SELECT u.name, SUM(li.qty * li.unit_price) AS revenue FROM users u JOIN orders o ON o.user_id = u.id JOIN line_items li ON li.order_id = o.id JOIN products p ON p.id = li.product_id JOIN suppliers s ON s.id = p.supplier_id JOIN categories c ON c.id = p.category_id JOIN warehouses w ON w.id = li.warehouse_id WHERE u.country = 'US' AND s.region = 'EU' GROUP BY u.name;
7! = 5,040 possible left-deep orders. EXPLAIN shows the one MySQL picked; this lesson shows the search that picked it.
What you'll see in the animation
- Top: the N tables as candidate roots — every search starts by picking one of them as the outermost loop.
- Center: the search tree of partial plans. Each node is a (partial-order, cost) pair the planner evaluated.
- Yellow pulses = a partial plan is being costed by best_extension_by_limited_search.
- Grey-out = optimizer_prune_level=1 dropped this branch because its partial cost already exceeded the best complete plan found so far.
- Bottom strip: the chosen final order — what EXPLAIN would print.
- Counter (top-right of stage): how many partial plans were actually evaluated. Watch it explode when you raise search_depth and turn pruning off.
Cost readout (updates live)
N (tables) ?Number of non-eq_ref tables plus eq_ref groups. const and eq_ref tables are pre-placed by the planner and do not enter greedy_search.
—
effective_search_depth ?min(optimizer_search_depth, N). When equal to N the search reduces to full exhaustive enumeration.
—
Plans evaluated ?Concrete count for this N and depth. The number of times best_extension_by_limited_search is called.
—
Worst-case complexity ?From sql_planner.cc line 2311: when search_depth >= N, greedy_search is O(N!). For smaller depth d: O(N * N^d / d).
—
Pruning ratio ?Fraction of partial plans that optimizer_prune_level=1 dropped before evaluating their full subtree. 0% means pruning is off; real OLTP plans see 60-90%.
—
Planning time estimate ?At ~1 microsecond per partial plan on modern hardware. Compare to query execution: if planning approaches execution cost, raise optimizer_prune_level or lower optimizer_search_depth.
—
Plans evaluated vs N (log–log)
Learn more — what the source actually says about greedy_search
The algorithm is Optimize_table_order::greedy_search
in sql/sql_planner.cc. The doxygen comment above its
body documents the complexity directly:
procedure greedy_search
input: remaining_tables
output: pplan;
{
pplan = <>;
do {
(t, a) = best_extension(pplan, remaining_tables);
pplan = concat(pplan, (t, a));
remaining_tables = remaining_tables - t;
} while (remaining_tables != {})
return pplan;
}
"the worst-case complexity of this algorithm is
<= O(N * N^search_depth / search_depth).
When search_depth >= N, then the complexity
of greedy_search is O(N!)."
Why N!, intuitively? A left-deep plan over N
tables is an ordering — there are N! orderings. With
search_depth ≥ N and pruning off, every ordering is
costed. With a depth limit d, only the next
d tables are enumerated exhaustively before the search
commits to a partial prefix, so the bound drops to
O(N · Nd / d).
Defaults you might tune (MySQL 8.4 / MariaDB 11.4):
optimizer_search_depth= 62 (MAX_TABLES + 1). Setting it to0tells the server to pick a value automatically. Session-tunable, so you can drop it per-query if planning is a bottleneck.optimizer_prune_level= 1 (cost-based pruning enabled). Set0only when you need to compare what the planner would have chosen without heuristics — never in production.optimizer_max_subgraph_pairs= 100000. Only the hypergraph (DPhyp) optimizer respects this; it's the modern set-based optimizer enabled per-query viaSET optimizer_switch='hypergraph_optimizer=on'on MySQL 8.0+.
N is not always the table count in your FROM clause.
The source comment is precise: "N is the number of non-eq_ref
tables + eq_ref groups, which normally are considerably less than
total numbers of tables in the query." const and
eq_ref chains are placed before greedy_search
runs, so a 10-table join can have N=3 if seven of the joins are
primary-key equi-joins.
How is this different from the join algorithms?
Hash join, BNL, nested loop are execution strategies — they
decide how a single join *runs* once the planner has fixed its place
in the tree. greedy_search is the layer above: given a
multi-way join, which pair joins first? Which result becomes the
outer side of the next join? EXPLAIN's table-order column is the
output of this search.
Sources: MySQL 8.4 Reference Manual §10.9.3 (Controlling
Switchable Optimizations); sql/sql_planner.cc lines
2275-2334 in mysql-server master. MariaDB Knowledge Base
"Optimizer Switch".