Summary

Query scans 2 tables, materializes 2 temp tables and sorts the result; examines ~4,008 rows to return 24 in 3.8 ms. Main finding: full scan of employees (2,000 rows).

Total time
3.79 ms
Rows returned
24
Rows examined
4.01K
Operators
15
Fix first

Primary recommendation

Add indexes on filter/join columns to avoid full table scans

Why does this help?

An index turns a full-table scan into a direct lookup — MySQL jumps straight to the matching rows instead of reading every row and discarding the ones that don't match. For joins, the benefit compounds: a nested loop over N outer rows that had to scan the inner table becomes N fast lookups.

SQL

with dept_avg as (/*
select#2 */
select demodb.employees.dept_id AS dept_id,avg(demodb.employees.salary) AS avg_sal,count(0) AS n
from demodb.employees
group by demodb.employees.dept_id), top_paid as (/*
select#3 */
select e.id AS id,e.name AS name,e.dept_id AS dept_id,e.salary AS salary,row_number() OVER (PARTITION BY demodb.e.dept_id
ORDER BY demodb.e.salary desc ) AS rn
from demodb.employees e) /*
select#1 */
select d.name AS dept,da.avg_sal AS avg_sal,tp.name AS top_earner,tp.salary AS salary
from demodb.departments d
  join dept_avg da
  join top_paid tp
where ((tp.dept_id = d.id) and (da.dept_id = d.id) and (tp.rn <= 3))
order by da.avg_sal desc

Execution plan (bargraph)

Click a plan operator to inspect details.

MySQL Query Plan Self-time per operation · Total: 4 ms OPERATION LOOPS SELF-TIME Search Hover a bar for details; click to pin · Ctrl+F to search Index scan [employees.idx_dept] 1 1 ms (31.7%) Materialize 1 1 ms (27.9%) Sort 1 0.364 ms (9.6%) Table scan [employees] 1 0.353 ms (9.3%) Window aggregate: row_number() OVER (PARTITION BY… 1 0.327 ms (8.6%) Index lookup [tp.<auto_key0>] 8 0.232 ms (6.1%) Group 1 0.139 ms (3.7%) Filter: ((tp.rn <= 3)) 8 0.055 ms (1.5%) Table scan [departments] 1 0.015 ms (0.4%) Sort 1 0.013 ms (0.3%) Materialize 1 0.011 ms (0.3%) Index lookup [da.<auto_key0>] 24 0.011 ms (0.3%) Stream results 1 0.006 ms (0.2%) Nested loop inner join 1 0.005 ms (0.1%) Nested loop inner join 1 0.003 ms (0.1%)

Warnings (3)

  1. WARN

    Full table scan: departments (8 rows), employees (2000 rows)

    Labeled: Table scan [departments] Table scan [employees]

  2. WARN

    2 temp tableLearn →(s) (Materialize) — up to 2000 rows; may spill to disk

    Labeled: Materialize

  3. WARN

    2 sort operationLearn →(s) — 2000 rows; may use disk-based filesortLearn →

    Labeled: Sort

Suggestions (3)

  1. HIGH

    Add indexes on filter/join columns to avoid full table scans

  2. MEDIUM

    Increase tmp_table_size / max_heap_table_size to keep temp tables in memory

  3. MEDIUM

    Increase sort_buffer_size or add an ordered index to avoid filesortLearn →

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:

Open the full catalog →

Lessons relevant to this plan

Raw sidecar (JSON)

{
  "$schema": "https://myflames.dev/schemas/sidecar-v1.json",
  "schema_version": "1.3",
  "generated_at": "2026-04-25T12:51:25Z",
  "myflames_version": "1.5.0",
  "source": {
    "type": "file"
  },
  "plan_summary": {
    "total_time_ms": 3.79,
    "rows_sent": 24,
    "rows_examined_estimate": 4008,
    "operator_count": 15,
    "max_depth": 10
  },
  "optimizer_switches": [
    {
      "name": "materialization",
      "value": "on",
      "explanation": "A subquery or derived table is executed once and its result stored in a temporary table for reuse. Watch tmp_table_size / max_heap_table_size — it may spill to disk.",
      "node_labels": [
        "Materialize"
      ]
    }
  ],
  "warnings": [
    {
      "severity": "warn",
      "category": "full_scan",
      "text": "Full table scan: departments (8 rows), employees (2000 rows)",
      "source": "plan",
      "node_labels": [
        "Table scan [departments]",
        "Table scan [employees]"
      ]
    },
    {
      "severity": "warn",
      "category": "temp_table",
      "text": "2 temp table(s) (Materialize) — up to 2000 rows; may spill to disk",
      "source": "plan",
      "node_labels": [
        "Materialize"
      ]
    },
    {
      "severity": "warn",
      "category": "filesort",
      "text": "2 sort operation(s) — 2000 rows; may use disk-based filesort",
      "source": "plan",
      "node_labels": [
        "Sort"
      ]
    }
  ],
  "suggestions": [
    {
      "severity": "high",
      "category": "index",
      "action": "Add indexes on filter/join columns to avoid full table scans",
      "source": "plan"
    },
    {
      "severity": "medium",
      "category": "tuning_variable",
      "action": "Increase tmp_table_size / max_heap_table_size to keep temp tables in memory",
      "source": "plan",
      "target_variable": "tmp_table_size"
    },
    {
      "severity": "medium",
      "category": "tuning_variable",
      "action": "Increase sort_buffer_size or add an ordered index to avoid filesort",
      "source": "plan",
      "target_variable": "sort_buffer_size"
    }
  ],
  "executive_summary": "Query scans 2 tables, materializes 2 temp tables and sorts the result; examines ~4,008 rows to return 24 in 3.8 ms. Main finding: full scan of employees (2,000 rows).",
  "plan_tree": {
    "node_id": "n:1f1e4988985d",
    "short_label": "Sort",
    "folded_label": "SORT starts=1 rows=24",
    "children": [
      {
        "node_id": "n:8415bc449e4d",
        "short_label": "Stream results",
        "folded_label": "STREAM starts=1 rows=24",
        "children": [
          {
            "node_id": "n:e09d45a7f9ab",
            "short_label": "Nested loop inner join",
            "folded_label": "NESTED LOOP INNER starts=1 rows=24",
            "children": [
              {
                "node_id": "n:e93f3abd5175",
                "short_label": "Nested loop inner join",
                "folded_label": "NESTED LOOP INNER starts=1 rows=24",
                "children": [
                  {
                    "node_id": "n:b953fadc011c",
                    "short_label": "Table scan [departments]",
                    "folded_label": "TABLE SCAN [departments] starts=1 rows=8",
                    "children": []
                  },
                  {
                    "node_id": "n:28b9826d22a5",
                    "short_label": "Filter: ((tp.rn <= 3))",
                    "folded_label": "FILTER ((tp.rn <= 3)) starts=8 rows=3",
                    "children": [
                      {
                        "node_id": "n:60bd34bb1b68",
                        "short_label": "Index lookup [tp.<auto_key0>]",
                        "folded_label": "INDEX LOOKUP [tp.<auto_key0>] starts=8 rows=250",
                        "children": [
                          {
                            "node_id": "n:3d82bcbcdc50",
                            "short_label": "Materialize",
                            "folded_label": "MATERIALIZE starts=1 rows=2000",
                            "children": [
                              {
                                "node_id": "n:ba04ee36d04e",
                                "short_label": "Window aggregate: row_number() OVER (PARTITION BY ...",
                                "folded_label": "Window aggregate: row_number() OVER (PARTITION BY e.dept_id .. starts=1 rows=2000",
                                "children": [
                                  {
                                    "node_id": "n:4fab1d622697",
                                    "short_label": "Sort",
                                    "folded_label": "SORT starts=1 rows=2000",
                                    "children": [
                                      {
                                        "node_id": "n:98ed0d4e2f31",
                                        "short_label": "Table scan [employees]",
                                        "folded_label": "TABLE SCAN [employees] starts=1 rows=2000",
                                        "children": []
                                      }
                                    ]
                                  }
                                ]
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  }
                ]
              },
              {
                "node_id": "n:c390875b730f",
                "short_label": "Index lookup [da.<auto_key0>]",
                "folded_label": "INDEX LOOKUP [da.<auto_key0>] starts=24 rows=1",
                "children": [
                  {
                    "node_id": "n:b55f5202484b",
                    "short_label": "Materialize",
                    "folded_label": "MATERIALIZE starts=1 rows=8",
                    "children": [
                      {
                        "node_id": "n:dff6f45b7a36",
                        "short_label": "Group",
                        "folded_label": "GROUP starts=1 rows=8",
                        "children": [
                          {
                            "node_id": "n:90028dbcb362",
                            "short_label": "Index scan [employees.idx_dept]",
                            "folded_label": "INDEX SCAN [employees.idx_dept] starts=1 rows=2000",
                            "children": []
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  },
  "query": {
    "raw": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc",
    "beautified": "with dept_avg as (/*\nselect#2 */\nselect demodb.employees.dept_id AS dept_id,avg(demodb.employees.salary) AS avg_sal,count(0) AS n\nfrom demodb.employees\ngroup by demodb.employees.dept_id), top_paid as (/*\nselect#3 */\nselect e.id AS id,e.name AS name,e.dept_id AS dept_id,e.salary AS salary,row_number() OVER (PARTITION BY demodb.e.dept_id\nORDER BY demodb.e.salary desc ) AS rn\nfrom demodb.employees e) /*\nselect#1 */\nselect d.name AS dept,da.avg_sal AS avg_sal,tp.name AS top_earner,tp.salary AS salary\nfrom demodb.departments d\n  join dept_avg da\n  join top_paid tp\nwhere ((tp.dept_id = d.id) and (da.dept_id = d.id) and (tp.rn <= 3))\norder by da.avg_sal desc"
  },
  "primary_action": {
    "ref": "suggestions[0]"
  },
  "teach_hooks": [
    {
      "lesson": "filesort",
      "match": {
        "folded_label": "SORT starts=1 rows=24",
        "short_label": "Sort"
      },
      "controls": {
        "rows": 24,
        "row_size": 200
      },
      "note": "Sort: da.avg_sal DESC",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "nested_loop",
      "match": {
        "folded_label": "NESTED LOOP INNER starts=1 rows=24",
        "short_label": "Nested loop inner join"
      },
      "controls": {
        "outer_rows": 24,
        "inner_rows": 24,
        "row_size": 200
      },
      "note": "Nested loop inner join",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "full_scan",
      "match": {
        "folded_label": "TABLE SCAN [departments] starts=1 rows=8",
        "short_label": "Table scan [departments]"
      },
      "controls": {
        "rows": 8,
        "row_size": 200,
        "selectivity": 100.0
      },
      "note": "Table scan on d (departments)",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "filter",
      "match": {
        "folded_label": "FILTER ((tp.rn <= 3)) starts=8 rows=3",
        "short_label": "Filter: ((tp.rn <= 3))"
      },
      "controls": {
        "input_rows": 24,
        "selectivity": 42.9
      },
      "note": "Filter: (tp.rn <= 3)",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "non_unique_lookup",
      "match": {
        "folded_label": "INDEX LOOKUP [tp.<auto_key0>] starts=8 rows=250",
        "short_label": "Index lookup [tp.<auto_key0>]"
      },
      "controls": {
        "rows": 2000,
        "selectivity": 40.0,
        "covering": false
      },
      "note": "Index lookup on tp using <auto_key0> (dept_id=d.id) (tp)",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "tmp",
      "match": {
        "folded_label": "MATERIALIZE starts=1 rows=2000",
        "short_label": "Materialize"
      },
      "controls": {
        "rows": 2000,
        "row_size": 200
      },
      "note": "Materialize CTE top_paid",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "filesort",
      "match": {
        "folded_label": "SORT starts=1 rows=2000",
        "short_label": "Sort"
      },
      "controls": {
        "rows": 2000,
        "row_size": 200
      },
      "note": "Sort: e.dept_id, e.salary DESC",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "full_scan",
      "match": {
        "folded_label": "TABLE SCAN [employees] starts=1 rows=2000",
        "short_label": "Table scan [employees]"
      },
      "controls": {
        "rows": 2000,
        "row_size": 200,
        "selectivity": 100.0
      },
      "note": "Table scan on e (employees)",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "non_unique_lookup",
      "match": {
        "folded_label": "INDEX LOOKUP [da.<auto_key0>] starts=24 rows=1",
        "short_label": "Index lookup [da.<auto_key0>]"
      },
      "controls": {
        "rows": 24,
        "selectivity": 5.0,
        "covering": false
      },
      "note": "Index lookup on da using <auto_key0> (dept_id=d.id) (da)",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "tmp",
      "match": {
        "folded_label": "MATERIALIZE starts=1 rows=8",
        "short_label": "Materialize"
      },
      "controls": {
        "rows": 8,
        "row_size": 200
      },
      "note": "Materialize CTE dept_avg",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    },
    {
      "lesson": "btree",
      "match": {
        "folded_label": "INDEX SCAN [employees.idx_dept] starts=1 rows=2000",
        "short_label": "Index scan [employees.idx_dept]"
      },
      "controls": {
        "rows": 2000,
        "key_size": 16,
        "page_size": 16384,
        "key_type": "secondary_noncovering"
      },
      "note": "Index scan on employees using idx_dept (employees)",
      "query_sql": "with `dept_avg` as (/* select#2 */ select `demodb`.`employees`.`dept_id` AS `dept_id`,avg(`demodb`.`employees`.`salary`) AS `avg_sal`,count(0) AS `n` from `demodb`.`employees` group by `demodb`.`employees`.`dept_id`), `top_paid` as (/* select#3 */ select `e`.`id` AS `id`,`e`.`name` AS `name`,`e`.`dept_id` AS `dept_id`,`e`.`salary` AS `salary`,row_number() OVER (PARTITION BY `demodb`.`e`.`dept_id` ORDER BY `demodb`.`e`.`salary` desc )  AS `rn` from `demodb`.`employees` `e`) /* select#1 */ select `d`.`name` AS `dept`,`da`.`avg_sal` AS `avg_sal`,`tp`.`name` AS `top_earner`,`tp`.`salary` AS `salary` from `demodb`.`departments` `d` join `dept_avg` `da` join `top_paid` `tp` where ((`tp`.`dept_id` = `d`.`id`) and (`da`.`dept_id` = `d`.`id`) and (`tp`.`rn` <= 3)) order by `da`.`avg_sal` desc"
    }
  ],
  "operator_complexities": [
    {
      "node_id": "n:1f1e4988985d",
      "folded_label": "SORT starts=1 rows=24",
      "short_label": "Sort",
      "complexity": {
        "big_o": "O(n log n)",
        "short": "n log n",
        "severity": "medium",
        "rationale": "Filesort: MySQL sorts the output in the server layer. Priority-queue filesort (LIMIT) is not detected today — we report the upper bound.",
        "confidence": "worst_case",
        "learn_more": "filesort"
      }
    },
    {
      "node_id": "n:e09d45a7f9ab",
      "folded_label": "NESTED LOOP INNER starts=1 rows=24",
      "short_label": "Nested loop inner join",
      "complexity": {
        "big_o": "O(n · log m)",
        "short": "n · log m",
        "severity": "medium",
        "rationale": "Indexed nested loop: each outer row probes the inner table via an index descent.",
        "confidence": "exact",
        "learn_more": "nested_loop_join"
      }
    },
    {
      "node_id": "n:b953fadc011c",
      "folded_label": "TABLE SCAN [departments] starts=1 rows=8",
      "short_label": "Table scan [departments]",
      "complexity": {
        "big_o": "O(n)",
        "short": "n",
        "severity": "medium",
        "rationale": "Full table scan: the storage engine returns every row; cost scales with the table size.",
        "confidence": "exact",
        "learn_more": "full_table_scan"
      }
    },
    {
      "node_id": "n:60bd34bb1b68",
      "folded_label": "INDEX LOOKUP [tp.<auto_key0>] starts=8 rows=250",
      "short_label": "Index lookup [tp.<auto_key0>]",
      "complexity": {
        "big_o": "O(log n + k)",
        "short": "log n + k",
        "severity": "good",
        "rationale": "Index lookup (ref): one B+tree descent then a sequential walk over k matching index entries. k = matching rows for the indexed predicate.",
        "confidence": "exact",
        "learn_more": "index_lookup"
      }
    },
    {
      "node_id": "n:3d82bcbcdc50",
      "folded_label": "MATERIALIZE starts=1 rows=2000",
      "short_label": "Materialize",
      "complexity": {
        "big_o": "build: O(m)  •  scan: O(1) per probe, O(rows) to fetch all",
        "short": "build+scan",
        "severity": "medium",
        "rationale": "Materialize is a two-phase operator. The build pays the inner subquery's cost ONCE; each scan of the temp table is effectively constant time. Don't compound build cost by loops.",
        "confidence": "typical",
        "learn_more": "materialization",
        "build_complexity": {
          "big_o": "O(m)",
          "short": "m",
          "severity": "medium",
          "rationale": "Materialize build: cost = cost of the source subquery over m rows.",
          "confidence": "typical",
          "learn_more": "materialization"
        },
        "scan_complexity": {
          "big_o": "O(1) per probe, O(rows) to fetch all",
          "short": "O(1)/probe",
          "severity": "good",
          "rationale": "Materialize scan: reads the pre-computed temp table. Each probe is constant time once the table is in memory.",
          "confidence": "typical",
          "learn_more": "materialization"
        }
      }
    },
    {
      "node_id": "n:4fab1d622697",
      "folded_label": "SORT starts=1 rows=2000",
      "short_label": "Sort",
      "complexity": {
        "big_o": "O(n log n)",
        "short": "n log n",
        "severity": "medium",
        "rationale": "Filesort: MySQL sorts the output in the server layer. Priority-queue filesort (LIMIT) is not detected today — we report the upper bound.",
        "confidence": "worst_case",
        "learn_more": "filesort"
      }
    },
    {
      "node_id": "n:98ed0d4e2f31",
      "folded_label": "TABLE SCAN [employees] starts=1 rows=2000",
      "short_label": "Table scan [employees]",
      "complexity": {
        "big_o": "O(n)",
        "short": "n",
        "severity": "medium",
        "rationale": "Full table scan: the storage engine returns every row; cost scales with the table size.",
        "confidence": "exact",
        "learn_more": "full_table_scan"
      }
    },
    {
      "node_id": "n:c390875b730f",
      "folded_label": "INDEX LOOKUP [da.<auto_key0>] starts=24 rows=1",
      "short_label": "Index lookup [da.<auto_key0>]",
      "complexity": {
        "big_o": "O(log n + k)",
        "short": "log n + k",
        "severity": "good",
        "rationale": "Index lookup (ref): one B+tree descent then a sequential walk over k matching index entries. k = matching rows for the indexed predicate.",
        "confidence": "exact",
        "learn_more": "index_lookup"
      }
    },
    {
      "node_id": "n:b55f5202484b",
      "folded_label": "MATERIALIZE starts=1 rows=8",
      "short_label": "Materialize",
      "complexity": {
        "big_o": "build: O(n log n)  •  scan: O(1) per probe, O(rows) to fetch all",
        "short": "build+scan",
        "severity": "medium",
        "rationale": "Materialize is a two-phase operator. The build pays the inner subquery's cost ONCE; each scan of the temp table is effectively constant time. Don't compound build cost by loops.",
        "confidence": "typical",
        "learn_more": "materialization",
        "build_complexity": {
          "big_o": "O(n log n)",
          "short": "n log n",
          "severity": "medium",
          "rationale": "Sort-based aggregation: MySQL sorts by the grouping key before collapsing duplicates.",
          "confidence": "typical"
        },
        "scan_complexity": {
          "big_o": "O(1) per probe, O(rows) to fetch all",
          "short": "O(1)/probe",
          "severity": "good",
          "rationale": "Materialize scan: reads the pre-computed temp table. Each probe is constant time once the table is in memory.",
          "confidence": "typical",
          "learn_more": "materialization"
        }
      }
    },
    {
      "node_id": "n:dff6f45b7a36",
      "folded_label": "GROUP starts=1 rows=8",
      "short_label": "Group",
      "complexity": {
        "big_o": "O(n log n)",
        "short": "n log n",
        "severity": "medium",
        "rationale": "Sort-based aggregation: MySQL sorts by the grouping key before collapsing duplicates.",
        "confidence": "typical"
      }
    },
    {
      "node_id": "n:90028dbcb362",
      "folded_label": "INDEX SCAN [employees.idx_dept] starts=1 rows=2000",
      "short_label": "Index scan [employees.idx_dept]",
      "complexity": {
        "big_o": "O(n)",
        "short": "n",
        "severity": "medium",
        "rationale": "Index scan: reads every entry of the index in key order. Cheaper than a table scan because the index is smaller, but still O(n).",
        "confidence": "exact",
        "learn_more": "index_scan"
      }
    }
  ]
}

Teach: Operator deep dive