Summary

Query scans 1 table and sorts the result; examines ~2,000 rows to return 50 in 0.49 ms. Main finding: no index covers (dept_id) on employees.

Total time
494 µs
Rows returned
50
Rows examined
2.00K
Operators
4
Fix first

Primary recommendation

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

Candidate DDL (copy & run)
CREATE INDEX idx_employees_dept_id ON employees (dept_id);
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

select e.name AS name,e.salary AS salary,(/*
select#2 */
select max(sh.salary)
from demodb.salary_history sh
where (sh.employee_id = e.id)) AS max_hist,(/*
select#3 */
select count(0)
from demodb.salary_history sh
where (sh.employee_id = e.id)) AS hist_count
from demodb.employees e
where (e.dept_id in (1,2,3))
order by e.salary desc
limit 50

Execution plan (flamegraph)

Click a plan operator to inspect details.

MySQL Query Plan Reset Zoom Search ic FILTER ((e.dept_id in (1,2,3))) starts=1 rows=750 (393 µs, 79.72%) Access: filter Rows: 750 actual (2000 estimated) [OVERESTIMATE] Time: 0.393 ms (last row) Cost: 202.00 Condition: (e.dept_id in (1,2,3)) FILTER ((e.dept_id in (1,2,3))) starts=1 rows=750 LIMIT starts=1 rows=50 (493 µs, 100.00%) Access: limit Rows: 50 actual (50 estimated) Time: 0.494 ms (last row) Cost: 202.00 LIMIT starts=1 rows=50 SORT starts=1 rows=50 (490 µs, 99.39%) Access: sort Rows: 50 actual (2000 estimated) [OVERESTIMATE] Time: 0.491 ms (last row) Cost: 202.00 SORT starts=1 rows=50 TABLE SCAN [employees] starts=1 rows=2000 (309 µs, 62.68%) Table: demodb.employees Access: table Rows: 2000 actual (2000 estimated) Time: 0.309 ms (last row) Cost: 202.00 TABLE SCAN [employees] starts=1 rows=2000

Warnings (2)

  1. WARN

    Full table scan: employees (2000 rows)

    Labeled: Table scan [employees]

  2. WARN

    1 sort operationLearn →(s) — 50 rows; may use disk-based filesortLearn →

    Labeled: Sort (limit 50)

Suggestions (2)

  1. HIGH

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

  2. 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": 0.494,
    "rows_sent": 50,
    "rows_examined_estimate": 2000,
    "operator_count": 4,
    "max_depth": 4
  },
  "optimizer_switches": [],
  "warnings": [
    {
      "severity": "warn",
      "category": "full_scan",
      "text": "Full table scan: employees (2000 rows)",
      "source": "plan",
      "node_labels": [
        "Table scan [employees]"
      ]
    },
    {
      "severity": "warn",
      "category": "filesort",
      "text": "1 sort operation(s) — 50 rows; may use disk-based filesort",
      "source": "plan",
      "node_labels": [
        "Sort (limit 50)"
      ]
    }
  ],
  "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 sort_buffer_size or add an ordered index to avoid filesort",
      "source": "plan",
      "target_variable": "sort_buffer_size"
    }
  ],
  "executive_summary": "Query scans 1 table and sorts the result; examines ~2,000 rows to return 50 in 0.49 ms. Main finding: no index covers (dept_id) on employees.",
  "plan_tree": {
    "node_id": "n:91695df69435",
    "short_label": "Limit: 50 rows",
    "folded_label": "LIMIT starts=1 rows=50",
    "children": [
      {
        "node_id": "n:d0fc793b4e18",
        "short_label": "Sort (limit 50)",
        "folded_label": "SORT starts=1 rows=50",
        "children": [
          {
            "node_id": "n:46e026049db1",
            "short_label": "Filter: ((e.dept_id in (1,2,3)))",
            "folded_label": "FILTER ((e.dept_id in (1,2,3))) starts=1 rows=750",
            "children": [
              {
                "node_id": "n:5a93cf1ee86a",
                "short_label": "Table scan [employees]",
                "folded_label": "TABLE SCAN [employees] starts=1 rows=2000",
                "children": []
              }
            ]
          }
        ]
      }
    ]
  },
  "query": {
    "raw": "/* select#1 */ select `e`.`name` AS `name`,`e`.`salary` AS `salary`,(/* select#2 */ select max(`sh`.`salary`) from `demodb`.`salary_history` `sh` where (`sh`.`employee_id` = `e`.`id`)) AS `max_hist`,(/* select#3 */ select count(0) from `demodb`.`salary_history` `sh` where (`sh`.`employee_id` = `e`.`id`)) AS `hist_count` from `demodb`.`employees` `e` where (`e`.`dept_id` in (1,2,3)) order by `e`.`salary` desc limit 50",
    "beautified": "select e.name AS name,e.salary AS salary,(/*\nselect#2 */\nselect max(sh.salary)\nfrom demodb.salary_history sh\nwhere (sh.employee_id = e.id)) AS max_hist,(/*\nselect#3 */\nselect count(0)\nfrom demodb.salary_history sh\nwhere (sh.employee_id = e.id)) AS hist_count\nfrom demodb.employees e\nwhere (e.dept_id in (1,2,3))\norder by e.salary desc\nlimit 50"
  },
  "index_suggestions": [
    {
      "table": "employees",
      "columns": [
        "dept_id"
      ],
      "ddl": "CREATE INDEX idx_employees_dept_id ON employees (dept_id);",
      "reason": "Full scan on employees with filter on (dept_id)"
    }
  ],
  "primary_action": {
    "ref": "suggestions[0]"
  },
  "teach_hooks": [
    {
      "lesson": "filesort",
      "match": {
        "folded_label": "SORT starts=1 rows=50",
        "short_label": "Sort (limit 50)"
      },
      "controls": {
        "rows": 50,
        "row_size": 200
      },
      "note": "Sort: e.salary DESC, limit input to 50 row(s) per chunk",
      "query_sql": "/* select#1 */ select `e`.`name` AS `name`,`e`.`salary` AS `salary`,(/* select#2 */ select max(`sh`.`salary`) from `demodb`.`salary_history` `sh` where (`sh`.`employee_id` = `e`.`id`)) AS `max_hist`,(/* select#3 */ select count(0) from `demodb`.`salary_history` `sh` where (`sh`.`employee_id` = `e`.`id`)) AS `hist_count` from `demodb`.`employees` `e` where (`e`.`dept_id` in (1,2,3)) order by `e`.`salary` desc limit 50"
    },
    {
      "lesson": "filter",
      "match": {
        "folded_label": "FILTER ((e.dept_id in (1,2,3))) starts=1 rows=750",
        "short_label": "Filter: ((e.dept_id in (1,2,3)))"
      },
      "controls": {
        "input_rows": 750,
        "selectivity": 37.5
      },
      "note": "Filter: (e.dept_id in (1,2,3))",
      "query_sql": "/* select#1 */ select `e`.`name` AS `name`,`e`.`salary` AS `salary`,(/* select#2 */ select max(`sh`.`salary`) from `demodb`.`salary_history` `sh` where (`sh`.`employee_id` = `e`.`id`)) AS `max_hist`,(/* select#3 */ select count(0) from `demodb`.`salary_history` `sh` where (`sh`.`employee_id` = `e`.`id`)) AS `hist_count` from `demodb`.`employees` `e` where (`e`.`dept_id` in (1,2,3)) order by `e`.`salary` desc limit 50"
    },
    {
      "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": "/* select#1 */ select `e`.`name` AS `name`,`e`.`salary` AS `salary`,(/* select#2 */ select max(`sh`.`salary`) from `demodb`.`salary_history` `sh` where (`sh`.`employee_id` = `e`.`id`)) AS `max_hist`,(/* select#3 */ select count(0) from `demodb`.`salary_history` `sh` where (`sh`.`employee_id` = `e`.`id`)) AS `hist_count` from `demodb`.`employees` `e` where (`e`.`dept_id` in (1,2,3)) order by `e`.`salary` desc limit 50"
    }
  ],
  "operator_complexities": [
    {
      "node_id": "n:d0fc793b4e18",
      "folded_label": "SORT starts=1 rows=50",
      "short_label": "Sort (limit 50)",
      "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:5a93cf1ee86a",
      "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"
      }
    }
  ]
}

Teach: Operator deep dive