Summary

Query scans 1 table; returns 24,950 rows in 19 ms. Main finding: no index covers (y, x) on t2.

Total time
18.81 ms
Rows returned
24.95K
Rows examined
14.47K
Operators
4
Fix first

Primary recommendation

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

Candidate DDL (copy & run)
CREATE INDEX idx_t2_y_x ON t2 (y, x);
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 straight_join testdb.t1.id AS id,testdb.t1.a AS a,testdb.t1.b AS b,testdb.t1.c AS c,testdb.t1.d AS d
from testdb.t2
  join testdb.t1
where ((testdb.t1.a = testdb.t2.x) and (testdb.t2.y < 1000))

Execution plan (flamegraph)

Click a plan operator to inspect details.

MySQL Query Plan Reset Zoom Search ic Batched key access inner join starts=1 rows=24950 (19 ms, 100.00%) Access: join Rows: 24950 actual Time: 18.808 ms (last row) Batched key access inner join starts=1 rows=24950 Multi-range index lookup on t1 using idx_a (a=t2.x) starts=2 rows=12475 (18 ms, 94.74%) Table: testdb.t1 (index: idx_a) Access: index Rows: 12475 actual (50 estimated) [UNDERESTIMATE] Loops: 2 Time: 8.772 ms (last row) Cost: 12.01 Covering: No Multi-range index lookup on t1 using idx_a (a=t2.x) starts=2 rows=12475

Warnings (1)

  1. WARN

    Full table scan: t2 (2000 rows)

    Labeled: Table scan [t2]

Suggestions (1)

  1. HIGH

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

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:27Z",
  "myflames_version": "1.5.0",
  "source": {
    "type": "file"
  },
  "plan_summary": {
    "total_time_ms": 18.808,
    "rows_sent": 24950,
    "rows_examined_estimate": 14475,
    "operator_count": 4,
    "max_depth": 3
  },
  "optimizer_switches": [
    {
      "name": "batched_key_access",
      "value": "on",
      "explanation": "Collects keys from the outer table into the join buffer, sorts them, then uses MRR to fetch inner rows in (mostly) index order — cuts random I/O on large indexed joins.",
      "node_labels": [
        "Batched key access inner join"
      ]
    },
    {
      "name": "join_cache_bka",
      "value": "on",
      "explanation": "MariaDB variant of batched_key_access: inner rows are fetched in batches keyed from the join buffer, using MRR to reorder I/O.",
      "node_labels": [
        "Batched key access inner join"
      ]
    },
    {
      "name": "mrr",
      "value": "on",
      "explanation": "Multi-Range Read: sorts row IDs before fetching rows so InnoDB reads pages in roughly primary-key order, turning random I/O into sequential I/O on range scans.",
      "node_labels": [
        "Multi-range index lookup on t1 using idx_a (a=t2.x)"
      ]
    }
  ],
  "warnings": [
    {
      "severity": "warn",
      "category": "full_scan",
      "text": "Full table scan: t2 (2000 rows)",
      "source": "plan",
      "node_labels": [
        "Table scan [t2]"
      ]
    }
  ],
  "suggestions": [
    {
      "severity": "high",
      "category": "index",
      "action": "Add indexes on filter/join columns to avoid full table scans",
      "source": "plan"
    }
  ],
  "executive_summary": "Query scans 1 table; returns 24,950 rows in 19 ms. Main finding: no index covers (y, x) on t2.",
  "plan_tree": {
    "node_id": "n:a5fa70f81162",
    "short_label": "Batched key access inner join",
    "folded_label": "Batched key access inner join starts=1 rows=24950",
    "children": [
      {
        "node_id": "n:f57cd32a81a5",
        "short_label": "Filter: (((t2.y < 1000) and (t2.x is not null)))",
        "folded_label": "FILTER (((t2.y < 1000) and (t2.x is not null))) starts=1 rows=499",
        "children": [
          {
            "node_id": "n:a7a05e53c0de",
            "short_label": "Table scan [t2]",
            "folded_label": "TABLE SCAN [t2] starts=1 rows=2000",
            "children": []
          }
        ]
      },
      {
        "node_id": "n:5f37843a8944",
        "short_label": "Multi-range index lookup on t1 using idx_a (a=t2.x)",
        "folded_label": "Multi-range index lookup on t1 using idx_a (a=t2.x) starts=2 rows=12475",
        "children": []
      }
    ]
  },
  "query": {
    "raw": "/* select#1 */ select straight_join `testdb`.`t1`.`id` AS `id`,`testdb`.`t1`.`a` AS `a`,`testdb`.`t1`.`b` AS `b`,`testdb`.`t1`.`c` AS `c`,`testdb`.`t1`.`d` AS `d` from `testdb`.`t2` join `testdb`.`t1` where ((`testdb`.`t1`.`a` = `testdb`.`t2`.`x`) and (`testdb`.`t2`.`y` < 1000))",
    "beautified": "select straight_join testdb.t1.id AS id,testdb.t1.a AS a,testdb.t1.b AS b,testdb.t1.c AS c,testdb.t1.d AS d\nfrom testdb.t2\n  join testdb.t1\nwhere ((testdb.t1.a = testdb.t2.x) and (testdb.t2.y < 1000))"
  },
  "index_suggestions": [
    {
      "table": "t2",
      "columns": [
        "y",
        "x"
      ],
      "ddl": "CREATE INDEX idx_t2_y_x ON t2 (y, x);",
      "reason": "Full scan on t2 with filter on (y, x)"
    }
  ],
  "primary_action": {
    "ref": "suggestions[0]"
  },
  "teach_hooks": [
    {
      "lesson": "filter",
      "match": {
        "folded_label": "FILTER (((t2.y < 1000) and (t2.x is not null))) starts=1 rows=499",
        "short_label": "Filter: (((t2.y < 1000) and (t2.x is not null)))"
      },
      "controls": {
        "input_rows": 499,
        "selectivity": 74.9
      },
      "note": "Filter: ((t2.y < 1000) and (t2.x is not null))",
      "query_sql": "/* select#1 */ select straight_join `testdb`.`t1`.`id` AS `id`,`testdb`.`t1`.`a` AS `a`,`testdb`.`t1`.`b` AS `b`,`testdb`.`t1`.`c` AS `c`,`testdb`.`t1`.`d` AS `d` from `testdb`.`t2` join `testdb`.`t1` where ((`testdb`.`t1`.`a` = `testdb`.`t2`.`x`) and (`testdb`.`t2`.`y` < 1000))"
    },
    {
      "lesson": "full_scan",
      "match": {
        "folded_label": "TABLE SCAN [t2] starts=1 rows=2000",
        "short_label": "Table scan [t2]"
      },
      "controls": {
        "rows": 2000,
        "row_size": 200,
        "selectivity": 100.0
      },
      "note": "Table scan on t2 (t2)",
      "query_sql": "/* select#1 */ select straight_join `testdb`.`t1`.`id` AS `id`,`testdb`.`t1`.`a` AS `a`,`testdb`.`t1`.`b` AS `b`,`testdb`.`t1`.`c` AS `c`,`testdb`.`t1`.`d` AS `d` from `testdb`.`t2` join `testdb`.`t1` where ((`testdb`.`t1`.`a` = `testdb`.`t2`.`x`) and (`testdb`.`t2`.`y` < 1000))"
    },
    {
      "lesson": "bka_join",
      "match": {
        "folded_label": "Multi-range index lookup on t1 using idx_a (a=t2.x) starts=2 rows=12475",
        "short_label": "Multi-range index lookup on t1 using idx_a (a=t2.x)"
      },
      "controls": {
        "outer_rows": 24950,
        "inner_rows": 24950,
        "row_size": 200,
        "key_size": 16
      },
      "note": "Multi-range index lookup on t1 using idx_a (a=t2.x) (t1)",
      "query_sql": "/* select#1 */ select straight_join `testdb`.`t1`.`id` AS `id`,`testdb`.`t1`.`a` AS `a`,`testdb`.`t1`.`b` AS `b`,`testdb`.`t1`.`c` AS `c`,`testdb`.`t1`.`d` AS `d` from `testdb`.`t2` join `testdb`.`t1` where ((`testdb`.`t1`.`a` = `testdb`.`t2`.`x`) and (`testdb`.`t2`.`y` < 1000))"
    }
  ],
  "operator_complexities": [
    {
      "node_id": "n:a5fa70f81162",
      "folded_label": "Batched key access inner join starts=1 rows=24950",
      "short_label": "Batched key access inner join",
      "complexity": {
        "big_o": "O(n · log m)",
        "short": "n · log m",
        "severity": "medium",
        "rationale": "Batched Key Access: outer keys are buffered and sorted, then the inner side is fetched via Multi-Range Read in clustered order. Cuts random I/O but keeps the index-descent cost per outer row.",
        "confidence": "typical",
        "learn_more": "batched_key_access"
      }
    },
    {
      "node_id": "n:a7a05e53c0de",
      "folded_label": "TABLE SCAN [t2] starts=1 rows=2000",
      "short_label": "Table scan [t2]",
      "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:5f37843a8944",
      "folded_label": "Multi-range index lookup on t1 using idx_a (a=t2.x) starts=2 rows=12475",
      "short_label": "Multi-range index lookup on t1 using idx_a (a=t2.x)",
      "complexity": {
        "big_o": "O(log n + k)",
        "short": "log n + k",
        "severity": "good",
        "rationale": "Index range scan: one B+tree descent to the range start, then a sequential walk over k matching index entries.",
        "confidence": "exact",
        "learn_more": "index_range_scan"
      }
    }
  ]
}

Teach: Operator deep dive