SQL EXPLAIN 可视化
MySQL / PostgreSQL EXPLAIN 输出图形化 · 自动标记可疑节点
粘贴 MySQL / PG EXPLAIN 输出(支持 EXPLAIN / EXPLAIN ANALYZE)
MySQL/PG EXPLAIN 输出图形化
MySQL / PostgreSQL EXPLAIN 输出图形化 · 自动标记可疑节点
粘贴 MySQL / PG EXPLAIN 输出(支持 EXPLAIN / EXPLAIN ANALYZE)
· MySQL EXPLAIN 关键字段:type(ALL=全表扫=坏 / range/ref/eq_ref/const=好)/ rows(扫描行数)/ Extra(Using filesort/temporary = 需优化)/ key(用到的索引)
· PG EXPLAIN ANALYZE:包含真实运行时间(actual time)· Seq Scan 全表 / Index Scan 索引 / Hash Join 等
了解工具定位 · 使用场景 · 对比优势
DBA 收到告警,某条业务 SQL 执行耗时从 50ms 飙升到 5s。将 EXPLAIN 输出粘贴到工具后,图形化展示显示:type 从 ref 退化为 ALL(全表扫描),rows 估算从 100 行变成 200 万行,Extra 列出现 Using filesort。DBA 立刻定位到索引被误删,无需逐行解析文本即可确定根因。
开发者在测试环境为订单表添加复合索引后,需要验证优化效果。分别导出加索引前后的 EXPLAIN 输出,工具并排渲染两张执行计划图。对比发现:possible_keys 从 NULL 变成 idx_order_date_status,key_len 从 NULL 变成 12,rows 从 50 万降到 800。图形化直观确认索引生效。
技术主管在 PR 审核中看到一条复杂 JOIN 查询,文本 EXPLAIN 长达 30 行难以审阅。将输出粘贴到工具后,生成树形图清晰展示驱动表、嵌套循环顺序、临时表使用位置。主管在 10 秒内发现第 3 个 JOIN 使用了 Using join buffer (Block Nested Loop),指出需要调整表连接顺序。
培训讲师讲解执行计划时,学员难以理解 type 列 ref/range/index/ALL 的差异。讲师将同一查询的不同写法(无索引、单列索引、复合索引)的 EXPLAIN 输出分别粘贴,工具并排显示 3 张执行计划图。学员直观看到 rows 从 100 万降到 1000、Extra 从 Using where 变为 Using index,理解索引对扫描范围的影响。
事故复盘会上,需要分析某次数据库 CPU 飙高根因。运维导出事故时间段的慢查询 EXPLAIN,粘贴到工具后生成执行计划图。图形显示多个查询的 type 为 ALL(全表扫描),且 rows 估算偏差超过 10 倍(实际 500 万行,估算 50 万行)。团队据此确认是统计信息过旧导致优化器选择错误执行计划。
| 维度 | 本工具 (explain-viz.tl654.com) | pgMustard | 手工分析 |
|---|---|---|---|
| 数据隐私 | 纯浏览器端处理,SQL 不上传服务器 | SQL 上传至云端服务器解析 | SQL 完全本地,无外传风险 |
| 处理速度 | 提交后即时渲染,<1 秒 | 网络请求 + 服务器解析,通常 2-5 秒 | 人工逐行阅读计划树,数分钟至数十分钟 |
| 离线可用 | 完全离线,打开页面后断网仍可用 | 必须联网,服务器不可用时无法访问 | 完全离线,依赖本地工具和人工 |
| 可视化形式 | 树形图 / 流程图,直观展示执行顺序与成本 | 带颜色编码的节点图,侧重成本占比 | 纯文本表格,需脑补执行流程 |
| 学习成本 | 零学习成本,粘贴即出图 | 需理解其颜色和节点符号含义 | 需掌握 EXPLAIN 输出中每个字段含义 |
| 大小限制 | 受浏览器内存限制,普通查询(<1000 行计划)无问题 | 受服务器限制,超大查询可能超时或被拒绝 | 无限制,但人工处理超大计划极不现实 |
| 收费模式 | 完全免费,无隐藏付费项 | 免费版有限制(如每日查询次数),高级功能需订阅 | 免费(仅需数据库客户端),但时间成本极高 |
上手步骤 · 输入输出 · 避坑提示
| 输入 | 输出 | 说明 |
|---|---|---|
| EXPLAIN SELECT * FROM users WHERE id = 1; | 图形化树:Seq Scan on users (cost=0.00..35.50 rows=10 width=500) → 根节点 | 典型常规场景:最简单的全表扫描查询 |
| EXPLAIN SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.status = 'active'; | 图形化树:Hash Join (cost=10.00..200.00 rows=100) → Hash (cost=5.00..5.00 rows=50) → Seq Scan on users; Hash (cost=100.00..100.00 rows=1000) → Seq Scan on orders | 典型常规场景:两表 JOIN 查询,展示 Hash Join 节点 |
| EXPLAIN FORMAT=JSON SELECT * FROM products WHERE category = 'electronics' AND price > 100; | 图形化树:Index Scan on products_category_idx (cost=0.25..8.27 rows=2 width=200) → Filter: (price > 100) | 边界 case:使用 FORMAT=JSON 格式输入,工具需正确解析 |
| EXPLAIN ANALYZE SELECT * FROM logs WHERE created_at > '2023-01-01'; | 图形化树:Seq Scan on logs (cost=0.00..5000.00 rows=100000 width=300) (actual time=0.012..150.000 rows=95000 loops=1) | 边界 case:包含 ANALYZE 实际执行时间信息 |
| EXPLAIN SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5); | 图形化树:Index Scan using users_pkey on users (cost=0.15..8.17 rows=5 width=500) → Index Cond: (id = ANY ('{1,2,3,4,5}'::integer[])) | 典型常规场景:使用 IN 条件的主键索引扫描 |
| EXPLAIN SELECT * FROM huge_table ORDER BY random_column; | 图形化树:Sort (cost=10000.00..12000.00 rows=800000 width=200) → Seq Scan on huge_table (cost=0.00..5000.00 rows=800000 width=200) | 易错 case:无索引的排序操作,cost 极高,提示性能风险 |
| EXPLAIN SELECT count(*) FROM users; | 图形化树:Aggregate (cost=35.50..35.51 rows=1 width=8) → Seq Scan on users (cost=0.00..35.50 rows=10 width=0) | 典型常规场景:聚合查询,展示 Aggregate 节点 |
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|----|-------------|-------|------|---------------|-----|---------|-----|------|-------|
| 1 | SIMPLE | users | ALL | NULL | NULL| NULL | NULL| 10000| Using where |EXPLAIN SELECT * FROM users WHERE age > 18;工具只解析 EXPLAIN 命令的原始输出文本(如 MySQL 的表格格式或 JSON 格式),而非 markdown 表格或格式化后的内容。直接粘贴表格会因格式不匹配导致解析失败。
1 SIMPLE users ALL NULL NULL NULL NULL 10000 Using whereEXPLAIN SELECT * FROM users WHERE age > 18;EXPLAIN 输出是结构化多行文本,每列有固定含义。只粘贴部分行或缺失列头会导致工具无法正确识别字段,进而无法生成图形。
-> Filter: (age > 18) (cost=1000 rows=1000) (actual time=0.123..5.456 rows=950 loops=1)
-> Table scan on users (cost=500 rows=10000) (actual time=0.010..3.200 rows=10000 loops=1)EXPLAIN SELECT * FROM users WHERE age > 18;EXPLAIN ANALYZE 输出包含实际执行时间和行数,格式与标准 EXPLAIN 不同。本工具仅解析标准 EXPLAIN 的表格或 JSON 输出,ANALYZE 的文本结构会导致解析错误。
SELECT * FROM users WHERE age > 18;EXPLAIN SELECT * FROM users WHERE age > 18;工具需要的是 EXPLAIN 命令的输出,而不是原始 SQL 语句。直接输入 SELECT/INSERT 等语句,工具无法识别,必须先在数据库中执行 EXPLAIN 命令并粘贴其输出。
{"query_block": {"select_id": 1, "table": "users"}}EXPLAIN FORMAT=JSON SELECT * FROM users WHERE age > 18;如果使用 FORMAT=JSON,必须粘贴完整的 JSON 输出(包含外层 query_block 和所有嵌套),而非手动提取的部分字段。工具依赖完整结构来解析节点关系和成本信息。
(粘贴 MySQL 的 EXPLAIN 表格输出到 PG 模式)(在工具中选择正确的数据库类型,或粘贴对应数据库的 EXPLAIN 输出)MySQL 和 PostgreSQL 的 EXPLAIN 输出列名、格式和含义不同(如 MySQL 有 rows 列,PG 有 rows 和 width)。选错数据库类型会导致字段映射错误,图形节点信息不准确。
[1mEXPLAIN[0m SELECT * FROM users;EXPLAIN SELECT * FROM users;某些终端或工具(如 MySQL 客户端开启 --color 选项)会在输出中加入 ANSI 颜色代码。这些非打印字符会干扰解析器,导致工具无法正确识别 EXPLAIN 输出内容。
EXPLAIN SELECT * FROM users;
| id | select_type | ... |
|----|-------------|-----|
| 1 | SIMPLE | ... || id | select_type | ... |
|----|-------------|-----|
| 1 | SIMPLE | ... |工具只解析 EXPLAIN 命令的输出部分,不解析命令本身或注释。粘贴时包含命令行或空行可能导致工具误判输入起点,建议只复制表格或 JSON 输出。
公式推导 · 流程图解 · 依据出处
Cost = Σ(node_cost) + Σ(join_cost) + Σ(filter_cost)
Cost — 查询执行总代价(任意单位)node_cost — 每个扫描/索引节点的 I/O 与 CPU 代价join_cost — 连接操作的代价(嵌套循环/哈希/归并)filter_cost — WHERE 条件过滤的行数评估代价EXPLAIN 输出:Seq Scan on users (cost=0.00..35.50 rows=2550 width=36)。其中 0.00 为启动代价,35.50 为总代价。该查询扫描全表 2550 行,每行代价约 0.014。若加上 JOIN:Hash Join (cost=12.25..47.80 rows=100 width=72),总代价 = 47.80,其中哈希构建代价 12.25,探测代价 35.55。
适用于 MySQL 和 PostgreSQL 的 EXPLAIN 输出解析。代价单位是优化器内部估算值,非真实时间(毫秒/秒)。不同数据库(如 SQLite、Oracle)代价模型不同,不适用。
3 种主流语言 · 复制即用
import json
import subprocess
# 模拟 MySQL EXPLAIN 输出(实际从数据库获取)
explain_output = """
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | users | NULL | ALL | NULL | NULL | NULL | NULL | 1000 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
"""
# 解析 MySQL 文本表格格式
lines = explain_output.strip().split('\n')
headers = [h.strip() for h in lines[1].split('|')[1:-1]]
values = [v.strip() for v in lines[3].split('|')[1:-1]]
# 构建结构化数据
parsed = dict(zip(headers, values))
print(json.dumps(parsed, indent=2))
# 输出:{"id": "1", "select_type": "SIMPLE", "table": "users", ...}
# 生成可视化节点(简化版:用 Mermaid 流程图)
node = f"{parsed['id']}[{parsed['table']}]\n{parsed['id']}-->{parsed['type']}"
print(f"Mermaid 流程图:\n{node}")package main
import (
"encoding/json"
"fmt"
"strings"
)
// 模拟 PostgreSQL EXPLAIN (JSON 格式)
func main() {
// 实际从数据库获取的 EXPLAIN (FORMAT JSON)
rawJSON := `[
{
"Plan": {
"Node Type": "Seq Scan",
"Relation Name": "orders",
"Startup Cost": 0.00,
"Total Cost": 35.50,
"Plan Rows": 1000,
"Plan Width": 40
}
}
]`
// 解析 JSON 格式的 EXPLAIN 输出
var plans []map[string]interface{}
if err := json.Unmarshal([]byte(rawJSON), &plans); err != nil {
panic(err)
}
plan := plans[0]["Plan"].(map[string]interface{})
// 提取关键字段用于可视化
fmt.Printf("节点类型: %s\n", plan["Node Type"])
fmt.Printf("表名: %s\n", plan["Relation Name"])
fmt.Printf("预估行数: %.0f\n", plan["Plan Rows"].(float64))
// 生成 DOT 格式(Graphviz 可视化)
dot := fmt.Sprintf(`digraph {
"%s" [label="%s\nrows: %.0f"];
}`, plan["Relation Name"], plan["Node Type"], plan["Plan Rows"])
fmt.Println("DOT 格式:\n" + dot)
}
// 浏览器端解析 MySQL EXPLAIN 文本格式
const explainText = `
+----+-------------+-------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | users | range | idx_email | idx_email | 1022 | NULL | 100 | 100.00 | Using index |
+----+-------------+-------+-------+---------------+---------+---------+------+------+----------+-------------+
`;
// 解析表格行
const lines = explainText.trim().split('\n');
const headers = lines[1].split('|').slice(1, -1).map(h => h.trim());
const values = lines[3].split('|').slice(1, -1).map(v => v.trim());
const parsed = {};
headers.forEach((h, i) => { parsed[h] = values[i]; });
console.log('解析结果:', parsed);
// 输出: { id: '1', select_type: 'SIMPLE', table: 'users', type: 'range', ... }
// 生成 SVG 节点(简化版)
const svg = `<svg width="200" height="80">
<rect x="10" y="10" width="180" height="60" rx="5" fill="#f0f0f0" stroke="#333"/>
<text x="100" y="35" text-anchor="middle" font-size="14">${parsed.table}</text>
<text x="100" y="55" text-anchor="middle" font-size="12" fill="#666">${parsed.type}</text>
</svg>`;
console.log('SVG 可视化:', svg);8 个高频疑问