2 operators·0.08 ms total·generated 2026-04-25T12:51:29Z
Summary
Query runs 2 operators; returns 100 rows in 0.08 ms. No warnings.
Total time
84 µs
Rows returned
100
Rows examined
100
Operators
2
SQL
select testdb.ss.g AS g,testdb.ss.v AS v
from testdb.ss
where (testdb.ss.v > 4900)
Execution plan (flamegraph)
Click a plan operator to inspect details.
myteach — interactive algorithm catalog
Every operator shown above has a hands-on lesson with a cost-model slider and an animated walk-through. The catalog is the central index — bookmark it once, revisit per query:
{
"$schema": "https://myflames.dev/schemas/sidecar-v1.json",
"schema_version": "1.3",
"generated_at": "2026-04-25T12:51:29Z",
"myflames_version": "1.5.0",
"source": {
"type": "file"
},
"plan_summary": {
"total_time_ms": 0.084,
"rows_sent": 100,
"rows_examined_estimate": 100,
"operator_count": 2,
"max_depth": 2
},
"optimizer_switches": [
{
"name": "skip_scan",
"value": "on",
"explanation": "Uses a composite index even though the query does not filter on the leading column, by probing each distinct leading-column value. Only pays off when the leading column has very low cardinality.",
"node_labels": [
"Covering index [ss.g]"
]
},
{
"name": "use_index_extensions",
"value": "on",
"explanation": "The optimizer treats the primary-key columns appended to every secondary index as additional key parts — enables covering reads that would otherwise need a row fetch.",
"node_labels": [
"Covering index [ss.g]"
]
}
],
"warnings": [],
"suggestions": [],
"executive_summary": "Query runs 2 operators; returns 100 rows in 0.08 ms. No warnings.",
"plan_tree": {
"node_id": "n:73b39ab87252",
"short_label": "Filter: ((ss.v > 4900))",
"folded_label": "FILTER ((ss.v > 4900)) starts=1 rows=100",
"children": [
{
"node_id": "n:df672e70bc82",
"short_label": "Covering index [ss.g]",
"folded_label": "COVERING INDEX [ss.g] starts=1 rows=100",
"children": []
}
]
},
"query": {
"raw": "/* select#1 */ select `testdb`.`ss`.`g` AS `g`,`testdb`.`ss`.`v` AS `v` from `testdb`.`ss` where (`testdb`.`ss`.`v` > 4900)",
"beautified": "select testdb.ss.g AS g,testdb.ss.v AS v\nfrom testdb.ss\nwhere (testdb.ss.v > 4900)"
},
"teach_hooks": [
{
"lesson": "filter",
"match": {
"folded_label": "FILTER ((ss.v > 4900)) starts=1 rows=100",
"short_label": "Filter: ((ss.v > 4900))"
},
"controls": {
"input_rows": 100,
"selectivity": 6.0
},
"note": "Filter: (ss.v > 4900)",
"query_sql": "/* select#1 */ select `testdb`.`ss`.`g` AS `g`,`testdb`.`ss`.`v` AS `v` from `testdb`.`ss` where (`testdb`.`ss`.`v` > 4900)"
},
{
"lesson": "skip_scan",
"match": {
"folded_label": "COVERING INDEX [ss.g] starts=1 rows=100",
"short_label": "Covering index [ss.g]"
},
"controls": {
"table_rows": 100,
"ndv_leading": 5,
"selectivity": 10
},
"note": "Covering index skip scan on ss using g over 4900 < v (ss)",
"query_sql": "/* select#1 */ select `testdb`.`ss`.`g` AS `g`,`testdb`.`ss`.`v` AS `v` from `testdb`.`ss` where (`testdb`.`ss`.`v` > 4900)"
}
],
"operator_complexities": [
{
"node_id": "n:df672e70bc82",
"folded_label": "COVERING INDEX [ss.g] starts=1 rows=100",
"short_label": "Covering index [ss.g]",
"complexity": {
"big_o": "O(n)",
"short": "n",
"severity": "good",
"rationale": "Covering index scan: walks the whole index in key order but never touches the clustered tree — all selected columns fit on the index leaves.",
"confidence": "exact",
"learn_more": "covering_index"
}
}
]
}
Filter operator — WHERE predicate row-by-row
Filter operator — WHERE predicate row-by-rowMySQL 8.4 • MariaDB 11.4
Understand the internal execution loop of a Filter stage: evaluate every input row, then keep or discard.
Lesson familyFilter Operator
Parameters (filter stage)
Rows entering filter: 1000000
Rows kept by WHERE: 5.0%
Example query this animation executes
SELECT order_id, total FROM orders WHERE total > 500;
EXPLAIN Filter node behavior: child operator emits rows, then this stage evaluates WHERE total > 500 row-by-row before passing rows upward.
What you'll see in the animation
Left stream is child output rows entering the filter stage.
Middle Filter operator evaluates predicate logic for every incoming row.
Green rows satisfy the predicate and continue to parent operators.
Red rows are discarded, but CPU work already happened for them.
Ready — press Play
0.0s0.0s
Cost readout (filter operator)
Input rows ?Rows entering the filter stage from its child operator.
—
Output rows ?Rows that satisfy the WHERE predicate and continue upward in the plan.
—
Dropped rows ?Rows evaluated but rejected by the predicate.
—
Selectivity ?Percentage of input rows that survive the predicate.
—
Rows evaluated vs rows returned (log–log)
Learn more — why filter nodes still matter
A Filter node is not always bad — it is normal in many plans. The key question is how many rows arrive at this stage.
If a selective predicate is pushed down into an index access path, far fewer rows reach this filter stage.
When you see large input rows and tiny output rows, consider adding/adjusting indexes so filtering happens earlier.
Skip Scan — range access without the leading index column
Skip Scan — range access without the leading index columnMySQL 8.4
How MySQL turns a full table scan into N small range scans by iterating over distinct values of the leading index column.
Parameters (Skip Scan)
Total number of rows in the table.
Number of distinct values in column A (e.g. gender). Lower = better for Skip Scan.
Percentage of rows matching WHERE condition on trailing column B.
Example query this animation executes
-- Index on (gender, age) but no equality on gender
SELECT * FROM employees
WHERE age BETWEEN 25 AND 30;
Skip Scan iterates over each distinct gender value, doing a range scan on age within each group — avoids reading the whole table.
What you'll see in the animation
A composite index on (gender, age) is shown as colored groups.
The cursor pill jumps to the first distinct gender value.
Within each group, a range scan checks age BETWEEN 25 AND 30.
Matching rows are highlighted green; non-matching rows are greyed out.
The cursor hops to the next group and repeats until all groups are scanned.
Ready — press Play
0.0s0.0s
Cost readout (Skip Scan)
Distinct values (leading col) ?Number of distinct values in the leading index column. Skip Scan does one sub-range-scan per distinct value.
—
Rows per group ?table_rows / NDV of leading column. Each group is scanned separately.
—
Matching rows per group ?Rows per group that satisfy the WHERE condition on the trailing column.
—
Total matching rows ?Sum of matching rows across all groups.
—
Skip scan reads ?Total reads: for each distinct value, one B+tree seek plus a range read of matching rows.
—
Full scan reads ?A full table scan reads every row — the baseline without Skip Scan.
—
Savings ?Reads saved compared to a full table scan: full_scan_reads minus skip_scan_reads.
—
Reads vs table size (log–log)
Learn more — when does the optimizer choose Skip Scan?
Skip Scan was introduced in MySQL 8.0.13 and is
controlled by optimizer_switch='skip_scan=on' (on by default).
When it helps:
1. The leading column of a composite index has low NDV
(number of distinct values) — e.g. gender, status, boolean flags.
2. The trailing column has a selective range condition
(e.g. age BETWEEN 25 AND 30).
3. The optimizer estimates that doing N sub-range-scans (one per distinct
leading value) is cheaper than a full table scan or a full index scan.
When it does NOT help:
1. The leading column has high NDV (thousands of distinct
values) — too many sub-scans make it slower than a full scan.
2. The range on the trailing column is not selective
(most rows match) — you end up reading almost everything anyway.
3. A better single-column index on the trailing column exists.
Note: Skip Scan is a MySQL-only
optimization. MariaDB does not implement it as of 11.x. In EXPLAIN output
you will see Using index for skip scan in the Extra column.
Source: MySQL 8.4 Reference Manual §10.2.1.2
“Range Optimization — Skip Scan Range Access Method”.