Summary
Query scans 2 tables and hash-joins 1 pair; examines ~7,000 rows to return 0 in 1.9 ms. Main finding: join predicate wraps the column in CONCAT() — no index can be used.
- Total time
- 1.94 ms
- Rows returned
- 0
- Rows examined
- 7.00K
- Operators
- 3
Primary recommendation
Rewrite the join condition to compare the bare column on both sides (e.g. 'a.id = b.other_id' instead of 'CONCAT(a.id)=CONCAT(b.other_id)').
Why does this help?
wrapping a column in a function means the optimizer cannot use any index on that column — every row-pair is evaluated in the server layer, making the cost O(outer × inner). Dropping the function lets MySQL/MariaDB pick an index lookup or hash joinLearn → with real selectivity, typically 100–1000× faster on mid-sized tables.
SQL
select 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,testdb.t2.id AS id,testdb.t2.x AS x,testdb.t2.y AS y
from testdb.t1
join testdb.t2
where (testdb.t1.d = concat('r',testdb.t2.y))
Execution plan (flamegraph)
Click a plan operator to inspect details.
Warnings (3)
-
ERROR
Non-sargable join predicate: CONCAT(...) in Inner hash joinLearn → (t1.d = concat('r',t2.y)) — a function wrapped around the join column prevents index use.
Labeled:
Inner hash join (t1.d = concat('r',t2.y)) -
WARN
Full table scan: t1 (5000 rows), t2 (2000 rows)
Labeled:
Table scan [t1]Table scan [t2] -
WARN
1 hash joinLearn →(s) — uses join_buffer_size
Labeled:
Inner hash join (t1.d = concat('r',t2.y))
Suggestions (3)
-
HIGH
Rewrite the join condition to compare the bare column on both sides (e.g. 'a.id = b.other_id' instead of 'CONCAT(a.id)=CONCAT(b.other_id)').
Why?
wrapping a column in a function means the optimizer cannot use any index on that column — every row-pair is evaluated in the server layer, making the cost O(outer × inner). Dropping the function lets MySQL/MariaDB pick an index lookup or hash joinLearn → with real selectivity, typically 100–1000× faster on mid-sized tables.
-
HIGH
Add indexes on filter/join columns to avoid full table scans
-
MEDIUM
Increase join_buffer_size if hash joins are slow or spill to disk
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:
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": 1.944,
"rows_sent": 0,
"rows_examined_estimate": 7000,
"operator_count": 3,
"max_depth": 2
},
"optimizer_switches": [
{
"name": "hash_join",
"value": "on",
"explanation": "Builds an in-memory hash table on the smaller (build) input and probes it from the other side. Uses join_buffer_size; spills to a tmp file (and tmpdir) if the build side does not fit.",
"node_labels": [
"Inner hash join (t1.d = concat('r',t2.y))"
]
}
],
"warnings": [
{
"severity": "error",
"category": "nonsargable_join",
"text": "Non-sargable join predicate: CONCAT(...) in Inner hash join (t1.d = concat('r',t2.y)) — a function wrapped around the join column prevents index use.",
"source": "plan",
"node_labels": [
"Inner hash join (t1.d = concat('r',t2.y))"
]
},
{
"severity": "warn",
"category": "full_scan",
"text": "Full table scan: t1 (5000 rows), t2 (2000 rows)",
"source": "plan",
"node_labels": [
"Table scan [t1]",
"Table scan [t2]"
]
},
{
"severity": "warn",
"category": "hash_join",
"text": "1 hash join(s) — uses join_buffer_size",
"source": "plan",
"node_labels": [
"Inner hash join (t1.d = concat('r',t2.y))"
]
}
],
"suggestions": [
{
"severity": "high",
"category": "rewrite",
"action": "Rewrite the join condition to compare the bare column on both sides (e.g. 'a.id = b.other_id' instead of 'CONCAT(a.id)=CONCAT(b.other_id)').",
"source": "plan",
"why": "wrapping a column in a function means the optimizer cannot use any index on that column — every row-pair is evaluated in the server layer, making the cost O(outer × inner). Dropping the function lets MySQL/MariaDB pick an index lookup or hash join with real selectivity, typically 100–1000× faster on mid-sized tables."
},
{
"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 join_buffer_size if hash joins are slow or spill to disk",
"source": "plan",
"target_variable": "join_buffer_size"
}
],
"executive_summary": "Query scans 2 tables and hash-joins 1 pair; examines ~7,000 rows to return 0 in 1.9 ms. Main finding: join predicate wraps the column in CONCAT() — no index can be used.",
"plan_tree": {
"node_id": "n:a528a4ef61ee",
"short_label": "Inner hash join (t1.d = concat('r',t2.y))",
"folded_label": "Inner hash join (t1.d = concat('r',t2.y)) starts=1 rows=0",
"children": [
{
"node_id": "n:0aa992c09c72",
"short_label": "Table scan [t1]",
"folded_label": "TABLE SCAN [t1] starts=1 rows=5000",
"children": []
},
{
"node_id": "n:4d0b633addc1",
"short_label": "Table scan [t2]",
"folded_label": "TABLE SCAN [t2] starts=1 rows=2000",
"children": []
}
]
},
"query": {
"raw": "/* select#1 */ select `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`,`testdb`.`t2`.`id` AS `id`,`testdb`.`t2`.`x` AS `x`,`testdb`.`t2`.`y` AS `y` from `testdb`.`t1` join `testdb`.`t2` where (`testdb`.`t1`.`d` = concat('r',`testdb`.`t2`.`y`))",
"beautified": "select 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,testdb.t2.id AS id,testdb.t2.x AS x,testdb.t2.y AS y\nfrom testdb.t1\n join testdb.t2\nwhere (testdb.t1.d = concat('r',testdb.t2.y))"
},
"primary_action": {
"ref": "suggestions[0]"
},
"teach_hooks": [
{
"lesson": "hash",
"match": {
"folded_label": "Inner hash join (t1.d = concat('r',t2.y)) starts=1 rows=0",
"short_label": "Inner hash join (t1.d = concat('r',t2.y))"
},
"controls": {
"build_rows": 2000,
"probe_rows": 5000,
"row_size": 200
},
"note": "Inner hash join (t1.d = concat('r',t2.y))",
"query_sql": "/* select#1 */ select `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`,`testdb`.`t2`.`id` AS `id`,`testdb`.`t2`.`x` AS `x`,`testdb`.`t2`.`y` AS `y` from `testdb`.`t1` join `testdb`.`t2` where (`testdb`.`t1`.`d` = concat('r',`testdb`.`t2`.`y`))"
},
{
"lesson": "full_scan",
"match": {
"folded_label": "TABLE SCAN [t1] starts=1 rows=5000",
"short_label": "Table scan [t1]"
},
"controls": {
"rows": 5000,
"row_size": 200,
"selectivity": 100.0
},
"note": "Table scan on t1 (t1)",
"query_sql": "/* select#1 */ select `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`,`testdb`.`t2`.`id` AS `id`,`testdb`.`t2`.`x` AS `x`,`testdb`.`t2`.`y` AS `y` from `testdb`.`t1` join `testdb`.`t2` where (`testdb`.`t1`.`d` = concat('r',`testdb`.`t2`.`y`))"
},
{
"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 `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`,`testdb`.`t2`.`id` AS `id`,`testdb`.`t2`.`x` AS `x`,`testdb`.`t2`.`y` AS `y` from `testdb`.`t1` join `testdb`.`t2` where (`testdb`.`t1`.`d` = concat('r',`testdb`.`t2`.`y`))"
}
],
"operator_complexities": [
{
"node_id": "n:a528a4ef61ee",
"folded_label": "Inner hash join (t1.d = concat('r',t2.y)) starts=1 rows=0",
"short_label": "Inner hash join (t1.d = concat('r',t2.y))",
"complexity": {
"big_o": "O(n + m)",
"short": "n + m",
"severity": "good",
"rationale": "Hash join: builds a hash on one side (O(m)), streams the other past it (O(n)). Disk-spill does NOT change the asymptotic class — only the constants.",
"confidence": "exact",
"learn_more": "hash_join"
}
},
{
"node_id": "n:0aa992c09c72",
"folded_label": "TABLE SCAN [t1] starts=1 rows=5000",
"short_label": "Table scan [t1]",
"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:4d0b633addc1",
"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"
}
}
]
}