Chapter 10 · Optimizer, EXPLAIN, Statistics, Histograms, and Query Tuning

Table/Index Statistics, Histograms, Cardinality, and Misestimate Diagnosis

Diagnose MariaDB cardinality misestimates with InnoDB persistent statistics, engine-independent statistics, targeted histograms and controlled ANALYZE TABLE workflows.

Advanced100–120 minutesSkew + histogram/cardinality labMariaDB Community 12.3.2 baselineInnoDB statistics · free local serverLast reviewed: August 2026

Learning outcomes

ServiceHub has an index on status, yet a query for the rare escalated value is occasionally costed like a much broader predicate. Another query filters two individually common columns whose combination is rare. These are cardinality-estimation problems. The optimizer cannot inspect every row for every candidate plan; it relies on statistical summaries. In MariaDB/InnoDB there are multiple statistical surfaces, and they have different refresh behavior and different operating costs.

01

Distinguish InnoDB persistent index statistics from MariaDB engine-independent statistics and histograms.

02

Inspect mysql.innodb_table_stats, mysql.innodb_index_stats and mysql.column_stats safely.

03

Use ANALYZE TABLE and ANALYZE TABLE ... PERSISTENT FOR ... deliberately rather than reflexively.

04

Build a skewed/correlated workload and compare estimated versus observed cardinality before and after statistics collection.

05

Explain why stale, sampled or independent-column statistics cannot perfectly model every real data relationship.

Operational cost

Current MariaDB uses InnoDB statistics by default and can auto-recalculate them after significant change. Engine-independent statistics/histograms are richer but are not automatically collected by the default policy; collecting them can require full table/index scans. Run them as a planned maintenance action on the columns/indexes that justify the cost.

1. Three statistical layers you should not conflate

Layer Stored where Refresh behavior / use
InnoDB persistent table/index stats mysql.innodb_table_stats / mysql.innodb_index_stats Persist across restart; sampled; ANALYZE TABLE recalculates; auto-recalc can update after significant change.
Engine-independent table/index stats mysql.table_stats / mysql.index_stats Collected explicitly or by configured policy; can complement/prefer engine stats.
Engine-independent column histograms mysql.column_stats Collected with PERSISTENT FOR; describe value distribution and can improve selectivity estimates.
sql · inspect what exists before changing anything
USE servicehub_optimizer_lab;SELECT * FROM mysql.innodb_table_statsWHERE database_name='servicehub_optimizer_lab' AND table_name='work_orders'\GSELECT index_name,stat_name,stat_value,sample_sizeFROM mysql.innodb_index_statsWHERE database_name='servicehub_optimizer_lab' AND table_name='work_orders'ORDER BY index_name,stat_name;SELECT db_name,table_name,column_name,nulls_ratio,avg_frequency,hist_size,hist_typeFROM mysql.column_statsWHERE db_name='servicehub_optimizer_lab' AND table_name='work_orders';

An empty mysql.column_stats result is not an error. With the default use_stat_tables=preferably_for_queries, MariaDB can use engine-independent statistics when present without automatically collecting them during an ordinary ANALYZE. This is an intentional separation between “use richer statistics” and “pay to collect richer statistics.”

2. Build an estimate-versus-reality checkpoint

sql · measure the actual skew and the optimizer estimate
SELECT status,COUNT(*) AS actual_rowsFROM work_orders GROUP BY status ORDER BY actual_rows DESC;EXPLAIN FORMAT=JSONSELECT * FROM work_orders WHERE status='escalated';ANALYZE FORMAT=JSONSELECT * FROM work_orders WHERE status='escalated';EXPLAIN FORMAT=JSONSELECT * FROM work_ordersWHERE status='escalated' AND region_code='TBZ';ANALYZE FORMAT=JSONSELECT * FROM work_ordersWHERE status='escalated' AND region_code='TBZ';

The first query establishes ground truth for the teaching dataset. The EXPLAIN/ANALYZE pair then compares estimated and observed row flow. If the estimate is already close, do not force a histogram simply because the feature exists. Statistics are justified when an important predicate is misestimated enough to change plan quality.

3. Refresh InnoDB statistics first when that is the likely problem

sql · standard engine statistics refresh
SELECT @@innodb_stats_persistent,       @@innodb_stats_auto_recalc,       @@innodb_stats_persistent_sample_pages;ANALYZE TABLE work_orders;SELECT * FROM mysql.innodb_table_statsWHERE database_name='servicehub_optimizer_lab' AND table_name='work_orders'\G

InnoDB persistent statistics are sampled, not exact census data. The default sample-page count is a compromise between ANALYZE cost and estimate quality. Increasing sampling globally because one table is skewed can make maintenance more expensive across the server. MariaDB also supports per-table statistics settings, which are preferable when a specific table justifies a different policy. Any sample-size experiment should be staged and measured rather than copied from a tuning blog.

4. Collect targeted engine-independent histograms when needed

sql · collect only the statistics that support the investigation
SELECT @@use_stat_tables,@@optimizer_use_condition_selectivity;ANALYZE TABLE work_ordersPERSISTENT FOR COLUMNS(status,region_code,priority) INDEXES(idx_status_region_sched);SELECT db_name,table_name,column_name,       min_value,max_value,nulls_ratio,avg_frequency,hist_size,hist_typeFROM mysql.column_statsWHERE db_name='servicehub_optimizer_lab' AND table_name='work_orders'ORDER BY column_name;ANALYZE FORMAT=JSONSELECT * FROM work_ordersWHERE status='escalated' AND region_code='TBZ';

MariaDB documents that engine-independent collection can perform full table/index scans and should usually be targeted rather than applied to every column and index. Histograms help with value-distribution selectivity, including non-indexed columns, but they do not magically model every cross-column dependency. If region_code and status are strongly correlated, independent summaries can still misestimate their conjunction.

5. Deliberately wrong: ANALYZE PERSISTENT FOR ALL everywhere, every hour

This converts a diagnosis tool into recurring full-scan load. It may compete with user queries for I/O and cache, increase replication activity because ANALYZE is binlogged by default, and collect expensive statistics on columns that never influence plans. It also creates false confidence: fresh histograms cannot compensate for a missing index, non-sargable predicate, unstable parameter distribution or cross-column correlation that the summary model does not capture.

sql · safer targeted workflow
-- 1. Identify an important misestimate with EXPLAIN/ANALYZE.-- 2. Collect only the relevant columns/indexes.ANALYZE LOCAL TABLE work_ordersPERSISTENT FOR COLUMNS(status,region_code) INDEXES(idx_status_region_sched);-- 3. Re-check plan/runtime evidence.ANALYZE FORMAT=JSONSELECT * FROM work_ordersWHERE status='escalated' AND region_code='TBZ';

LOCAL / NO_WRITE_TO_BINLOG prevents the ANALYZE statement from being written to the binary log when that is appropriate for your topology. Do not apply it mechanically: decide whether statistics should be collected independently on replicas/primaries and document that operational policy.

6. Statistics can be fresh and still wrong enough

Consider customer_id and region_code: in a well-designed model, customer region and work-order region may be correlated by business rules. Marginal distributions do not capture the full joint distribution. The optimizer may also face expressions, ranges, LIKE prefixes, NULL behavior, rapidly changing queues or parameter classes whose selectivity is hard to summarize. “We ran ANALYZE” is therefore not the end of diagnosis.

When estimates remain poor, verify data correctness, consider a better composite index or generated-column representation, simplify non-sargable predicates, split radically different parameter classes, or use a narrowly scoped hint only after proving the better alternative. Editing system statistics tables by hand is possible in MariaDB but belongs to controlled expert diagnostics, not routine application tuning.

7. Correlation, parameter classes, and why “fresh statistics” can still mislead

A histogram on status can describe the distribution of status values, and another on region_code can describe regions, but the optimizer does not thereby obtain a perfect joint probability model for status='escalated' AND region_code='TBZ'. Real production data often contains business correlation: one product tier appears mostly in one geography, one incident type belongs to one device family, or recent rows have very different status distributions from historical rows. Fresh single-column statistics can therefore remain insufficient.

Separate parameter classes are equally important. A prepared statement may receive a rare tenant ID in one request and a dominant tenant in another. If one plan is expected to serve both, test both classes explicitly. If the classes have radically different optimal access paths, the durable solution may be schema/query redesign, partitioning of workloads, or explicit application routing—not endlessly increasing histogram size.

Estimate problem Evidence Candidate remedy
Stale row/index distribution Stats timestamps and estimate/runtime gap ANALYZE TABLE or targeted persistent collection.
Strong skew in one column Actual grouped counts differ from uniform assumption Targeted histogram if the predicate matters.
Cross-column correlation Marginal stats look reasonable but conjunction is wrong Composite index, generated key, query redesign, or scoped control.
Rapidly changing queue Distribution changes faster than refresh cadence Operationally appropriate refresh policy or different access strategy.
Expression hides indexed value Predicate is non-sargable despite good base-column stats Rewrite or generated-column/index pattern.

The goal is not maximum statistical detail. The goal is enough accurate information for important plan decisions at acceptable maintenance cost. Collecting everything can itself become a production workload.

8. A statistics change needs rollback evidence too

Before collecting expensive engine-independent statistics on a large production table, record the current plan, query latency distribution, statistics rows, server load, and replication policy. After collection, compare the same query classes. If plans regress, you need a documented way to stop preferring those statistics or restore the prior operational state. Treat statistics as optimizer inputs with lifecycle and ownership, not invisible metadata that can never cause regressions.

Finally, treat statistics maintenance as part of the change record: note who collected it, why, on which server/version, for which columns/indexes, and what query evidence justified the cost. That operational history is essential when a later plan change must be explained or reversed.

9. Verification and bridge

Check your understanding

  1. Why can mysql.column_stats legitimately be empty after an ordinary ANALYZE TABLE?
  2. What is the main tradeoff of innodb_stats_persistent_sample_pages?
  3. Why should PERSISTENT FOR target selected columns/indexes instead of always using FOR ALL?
  4. Can a histogram fully model correlation between two columns?
  5. Why can ANALYZE TABLE affect replication operations?
Review the answers

The default policy can use engine-independent stats when present without collecting them automatically. More InnoDB sample pages can improve estimates but increase ANALYZE I/O/work. Targeted PERSISTENT collection avoids full-scan statistics work on irrelevant columns/indexes. Single-column histograms do not fully model arbitrary cross-column correlation. ANALYZE TABLE is written to the binary log by default unless LOCAL/NO_WRITE_TO_BINLOG or read-only behavior prevents it.

Lesson 4 now studies how these estimates feed join algorithms, semijoin strategies, derived-table merge/materialization and subquery transformations.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.