Chapter 10 · Optimizer, EXPLAIN, Statistics, Histograms, and Query Tuning
Join Algorithms, Subquery Optimizations, Derived Tables, Semijoins, and Materialization
Connect MariaDB join algorithms, semijoin strategies, subquery transformations and derived-table merge/materialization to observable row-flow and optimizer decisions.
Learning outcomes
Two SQL statements return the same set of customers: one uses
EXISTS, the other joins to a distinct list of
work-order customer IDs. A developer assumes the first must
execute the subquery once per customer because it is written as
a subquery. MariaDB’s optimizer can transform qualifying
IN/EXISTS forms into semijoins and
choose among several strategies. Likewise, a derived table can
be merged into the outer query or materialized as a temporary
result depending on semantics and estimated cost.
Explain ordinary nested-loop row flow and MariaDB block-based join variants without assuming one algorithm is always superior.
Recognize semijoin strategies such as table pullout, FirstMatch, LooseScan, Materialization and DuplicateWeedout.
Distinguish derived-table merge, condition pushdown, split materialization and full materialization.
Use EXPLAIN/ANALYZE and optimizer trace to verify selected transformations rather than inferring them from SQL text.
Run optimizer_switch experiments only at session scope and restore them immediately.
MariaDB has long supported block-based join algorithms such as BNL/BNLH/BKA/BKAH, controlled by join-cache settings and optimizer switches. MariaDB 12.0 added new-style BKA/BNL hints. The mandatory lab does not require forcing a specific algorithm; it teaches how to observe whatever the 12.3.2 optimizer chooses.
1. Nested-loop reasoning remains the baseline mental model
In a nested loop, MariaDB obtains rows from an outer input and finds matching rows in an inner input. If the inner side has a useful index, repeated key lookups can be efficient. If it does not, repeated scans can be catastrophic. Block-based algorithms buffer outer rows so they can process the inner side in batches; Batch Key Access (BKA) batches index lookups, while hashed variants can accelerate equality matching under applicable settings.
USE servicehub_optimizer_lab;SELECT @@join_cache_level,@@join_buffer_size,@@join_buffer_space_limit,@@optimizer_switch\GANALYZE FORMAT=JSONSELECT c.customer_id,c.region_code,w.work_order_id,w.statusFROM customers cJOIN work_orders w ON w.customer_id=c.customer_idWHERE c.region_code='TBZ' AND w.status='open';
Do not tune join_buffer_size by folklore. It is
per-query/per-join working memory with concurrency implications,
and index design often matters more. The plan and runtime row
flow should tell you whether the inner lookup pattern is the
problem before you allocate more join memory.
2. Semijoin means existence without duplicate multiplication
EXPLAIN FORMAT=JSONSELECT c.customer_id,c.tierFROM customers cWHERE EXISTS ( SELECT 1 FROM work_orders w WHERE w.customer_id=c.customer_id AND w.status='escalated');EXPLAIN FORMAT=JSONSELECT c.customer_id,c.tierFROM customers cWHERE c.customer_id IN ( SELECT w.customer_id FROM work_orders w WHERE w.status='escalated');
A semijoin asks whether at least one inner match exists; it does not need to emit one outer row per matching inner row. MariaDB documents five semijoin strategies: table pullout, FirstMatch, Materialization, LooseScan and DuplicateWeedout. The optimizer chooses among applicable strategies based on query shape and cost. The strategy names are less important than their row-flow implication: avoid redundant work when only existence matters.
3. Materialization is a trade: build once, reuse many times
Materialization computes an intermediate result and stores it in a temporary structure. That adds startup work and memory/disk risk, but it can avoid repeatedly executing an expensive subquery or derived expression. Derived-table merge does the opposite: when semantics permit, MariaDB can fold a derived table into the outer query so predicates and indexes participate in one optimization problem.
EXPLAIN FORMAT=JSONSELECT d.customer_id,d.open_valueFROM ( SELECT customer_id,SUM(total_cents) AS open_value FROM work_orders WHERE status='open' GROUP BY customer_id) AS dWHERE d.open_value > 100000;SET optimizer_trace='enabled=on';SELECT d.customer_id,d.open_valueFROM ( SELECT customer_id,SUM(total_cents) AS open_value FROM work_orders WHERE status='open' GROUP BY customer_id) AS dWHERE d.open_value > 100000;SELECT TRACE FROM information_schema.OPTIMIZER_TRACE\GSET optimizer_trace='enabled=off';
Aggregation often creates a natural materialization boundary because the grouped result has different cardinality and semantics. MariaDB also supports condition pushdown into eligible derived tables and split-materialized optimization. Optimizer trace can show which transformations were considered and why. Do not assume a derived table is always a physical temporary table merely because SQL text contains parentheses.
4. Controlled optimizer_switch experiment
SET @saved_optimizer_switch=@@optimizer_switch;SET SESSION optimizer_switch='semijoin=off';EXPLAIN FORMAT=JSONSELECT c.customer_idFROM customers cWHERE c.customer_id IN ( SELECT w.customer_id FROM work_orders w WHERE w.status='escalated');SET SESSION optimizer_switch=@saved_optimizer_switch;SELECT @@optimizer_switch\G
This is a diagnostic experiment, not a recommended production setting. The goal is to compare plan shape and runtime when a family of transformations is unavailable. Restore the session value immediately. If disabling a transformation improves one query, ask why the default estimate preferred the other plan: statistics, correlation, indexes, parameter distribution and version behavior remain the durable causes to investigate.
5. Deliberately wrong: memorize “hash join beats nested loop”
Join algorithms are workload-dependent. An indexed nested loop
can be excellent when the outer input is small and inner lookups
are selective. A block/hash strategy can help when repeated
scanning dominates, but it consumes join-buffer memory and has
applicability constraints. MariaDB’s block algorithms are also
controlled by join_cache_level and switches such as
join_cache_bka, join_cache_hashed and
join_cache_incremental. The default
join_cache_level is 2, so merely seeing the
switches ON does not mean every block algorithm is eligible.
Repair the reasoning by measuring row flow and candidate costs. Ask: how many outer rows, how many inner probes, is there a supporting index, how much duplication does existence semantics avoid, does materialization reduce repetition, and what temporary-memory/I/O work appears? Then verify with runtime evidence.
6. Join memory and algorithm choice are concurrency decisions
Block-based joins use join buffers, which means a query-level
improvement can become a server-level memory problem when
multiplied by concurrent sessions.
join_buffer_size is therefore not analogous to a
single shared cache. A workload with many concurrent joins can
allocate substantial working memory, and larger buffers do not
guarantee a better plan. MariaDB also exposes
join_buffer_space_limit to bound total
join-buffer space for a query.
Batch Key Access is useful to understand because it changes the order and batching of index lookups rather than changing relational semantics. It can reduce random access by grouping lookup work, but it relies on applicable indexes and Multi-Range Read behavior. A hash/block strategy can help an equijoin without a useful indexed lookup path, but its build/probe work and memory still depend on input cardinality. These are reasons to inspect runtime rows before changing join-cache policy.
| Situation | Mechanism to consider | Evidence required |
|---|---|---|
| Small selective outer input + indexed inner key | Ordinary indexed nested loop | Low outer rows and cheap inner probes. |
| Many inner key lookups | BKA/MRR where applicable | Repeated indexed probes dominate and batching is eligible. |
| Existence-only subquery | Semijoin strategy | Duplicate inner matches need not multiply output. |
| Reusable expensive sub-result | Materialization | One-time build is cheaper than repeated execution. |
| Derived query with pushable filter | Merge/condition pushdown | Predicate can reduce rows before materialization/aggregation boundaries. |
7. Semijoin names are not tuning goals
FirstMatch, LooseScan, Materialization, DuplicateWeedout and table pullout describe execution strategies for qualifying subqueries. Do not write an operational objective such as “make every IN query use LooseScan.” The right strategy depends on duplicates, available indexes, outer/inner cardinality, grouping properties and cost. Instead, define the row-flow problem and let the optimizer choose from accurate inputs; use trace or a scoped switch only when you need to understand or test a specific alternative.
This distinction matters during upgrades. A newer MariaDB version may select a different semijoin strategy because the cost model or transformation logic changed. A different strategy name is not a regression by itself. Compare correctness, runtime rows, latency and resource use under the same workload.
8. Production judgment and bridge
- Write the relational intent first: ordinary join, existence test, aggregation boundary or reusable derived result.
- Use indexes/statistics so the optimizer has a good candidate set.
- Inspect EXPLAIN/ANALYZE for actual strategy and row flow.
- Use optimizer trace when you need to understand a rejected transformation.
- Run switch/hint experiments at session/query scope, never as unexplained global policy.
- Re-test after server upgrades because optimizer behavior is version-sensitive.
Check your understanding
- What semantic property distinguishes a semijoin from an ordinary inner join?
- Name three MariaDB semijoin strategies.
- Why can materialization be beneficial despite creating a temporary result?
- Why does optimizer_switch=... belong in a scoped experiment?
- Why does join_cache_bka=on not prove BKA is currently usable?
Review the answers
A semijoin returns an outer row based on existence and does not multiply it by every inner match. MariaDB strategies include Table Pullout, FirstMatch, LooseScan, Materialization and DuplicateWeedout. Materialization can pay a one-time build cost to avoid repeated inner work. optimizer_switch changes can affect many plan decisions, so experiments should be scoped and restored. BKA eligibility also depends on join_cache_level and other conditions, not only the switch flag.
Lesson 5 closes the chapter by turning all these tools into a regression-control workflow and showing when hints or ignored indexes are appropriate safeguards rather than permanent substitutes for understanding.