3. EXPLAIN 执行计划:命令详解与慢查询定位

在 Go 项目数据库调优中,EXPLAIN 是分析 SQL 慢查询最核心的工具。它模拟优化器如何执行一条 SQL 语句,并输出每一步的访问方式、索引使用、扫描行数等关键信息。掌握 EXPLAIN 的每个字段,是定位慢查询根源的必备技能。

3.1 EXPLAIN 命令基本用法

在 MySQL 中,只需在 SELECT / DELETE / INSERT / UPDATE 语句前加上 EXPLAIN 关键字即可查看执行计划:

-- 查看 SELECT 查询的执行计划
EXPLAIN SELECT u.name, o.order_amount
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 1
ORDER BY o.created_at DESC
LIMIT 20;

-- 在 Go 中通过 database/sql 执行 EXPLAIN
rows, err := db.Query("EXPLAIN SELECT ...")
// 遍历 rows 即可获取执行计划行
提示:MySQL 8.0.18+ 支持 EXPLAIN ANALYZE,它会实际执行 SQL 并输出每个步骤的实际耗时和行数,比普通 EXPLAIN 更精确,但生产环境慎用。

3.2 核心字段详解

EXPLAIN 输出包含多个字段,其中 type、key、rows、Extra 是判断查询性能的四大核心指标。下表列出各字段的含义与优化方向:

字段 含义 常见值(从优到劣) 优化方向
type 访问类型,表示 MySQL 如何查找行 system > const > eq_ref > ref > range > index > ALL 至少达到 range,最好 ref 或 eq_ref
key 实际使用的索引名 索引名称 或 NULL(未使用索引) 确保 key 不为 NULL,且与查询条件匹配
rows 预估需要扫描的行数 数字(如 1, 1000, 500000) 越小越好,超过 10000 需警惕
Extra 额外信息,包含优化器提示 Using index / Using where / Using filesort / Using temporary 避免 Using filesort 和 Using temporary
possible_keys 可能使用的索引列表 索引名列表 或 NULL 若为 NULL,说明无可用索引
ref 索引列与哪个值比较 const / 列名 / func const 表示常量匹配,效率高
filtered 按表条件过滤后剩余行的百分比 0~100 越高越好,低于 10% 说明索引选择性差

3.3 type 字段:访问类型深度解读

type 是衡量查询效率的第一指标。从最优到最差依次为:

  • system:表只有一行(系统表),几乎不出现。
  • const:通过主键或唯一索引等值匹配,最多返回一行。
  • eq_ref:JOIN 时使用主键或唯一索引关联,每行只匹配一行。
  • ref:使用普通索引等值匹配,可能返回多行。
  • range:索引范围扫描(BETWEEN、>、<、IN 等)。
  • index:全索引扫描,遍历整个索引树。
  • ALL:全表扫描,最差情况。
慢查询红线:如果 EXPLAIN 结果中 type 为 ALL 且 rows 超过 10000,该查询几乎一定会成为慢查询。在 Go 高并发场景下,这种查询会迅速耗尽数据库连接池。

3.4 key 与 rows:索引使用与扫描行数

key 字段显示实际使用的索引。如果为 NULL,说明没有使用索引,需要检查 WHERE 条件是否被索引覆盖,或者是否因为函数/隐式转换导致索引失效。

rows 是优化器预估的扫描行数,并非精确值。当 rows 远大于实际返回行数时,说明索引选择性差,或者统计信息过时。在 Go 应用中,如果 EXPLAIN 显示 rows 超过 10 万,即使当前响应时间可接受,随着数据量增长也会急剧恶化。

3.5 Extra 字段:隐藏的性能杀手

Extra 字段中的几个关键词需要特别警惕:

Extra 值 含义 性能影响 优化建议
Using index 覆盖索引,无需回表 ✅ 最优 保持即可
Using where 使用 WHERE 过滤 ✅ 正常 确保过滤条件有索引
Using index condition 索引条件下推(ICP) ✅ 较好 MySQL 5.6+ 自动优化
Using filesort 需要额外排序操作 ⚠️ 较差 为 ORDER BY 字段加索引
Using temporary 使用临时表(GROUP BY / DISTINCT) ⚠️ 较差 优化索引或改写 SQL
Using where; Using index 覆盖索引 + 过滤 ✅ 最优 理想状态

3.6 通过执行计划定位慢查询:实战流程

在 Go 项目中,定位慢查询的标准化流程如下:

  1. 开启慢查询日志:在 MySQL 配置中设置 slow_query_log=1 和 long_query_time=1(超过 1 秒记录)。
  2. 收集慢 SQL:通过 mysqldumpslow 或 pt-query-digest 聚合分析。
  3. EXPLAIN 分析:对慢 SQL 执行 EXPLAIN,重点关注 type、key、rows、Extra。
  4. 定位瓶颈:
    • type=ALL → 缺少索引或索引失效。
    • rows 过大 → 索引选择性差或统计信息过期。
    • Extra=Using filesort → 排序字段无索引。
    • Extra=Using temporary → GROUP BY 或 DISTINCT 需优化。
  5. 优化并验证:添加索引或改写 SQL 后,再次 EXPLAIN 确认 type 和 rows 改善。
Go 实战案例:某订单查询 EXPLAIN 显示 type=ALL、rows=500000、Extra=Using filesort。分析发现 WHERE 条件中 user_id 有索引但 status 无索引,且 ORDER BY created_at 未建联合索引。添加 INDEX(user_id, status, created_at) 后,type 变为 ref,rows 降至 200,Extra 变为 Using index condition,查询耗时从 3.2s 降至 8ms。

3.7 EXPLAIN 执行计划流程图

下图展示了从慢 SQL 到执行计划解读的完整链路:

EXPLAIN 执行计划分析流程 EXPLAIN 执行计划分析流程 慢 SQL 捕获 EXPLAIN 执行 核心字段分析 type | key | rows | Extra type=ALL? → 缺索引 rows>10000? → 选择性差 Using filesort? → 排序优化 索引优化 / SQL 改写

3.8 常见慢查询模式与 EXPLAIN 特征对照表

下表总结了 Go 项目中常见的慢查询模式及其 EXPLAIN 特征,方便快速对照定位:

慢查询模式 典型 SQL EXPLAIN 特征 根因
全表扫描 SELECT * FROM orders WHERE status=0 type=ALL, rows=1000000, key=NULL status 列无索引
索引失效 WHERE DATE(created_at)='2024-01-01' type=ALL, possible_keys=idx_created, key=NULL 函数导致索引失效
排序无索引 ORDER BY amount DESC type=ref, Extra=Using filesort 排序字段未加入索引
临时表 GROUP BY user_id HAVING COUNT(*) > 10 type=ALL, Extra=Using temporary; Using filesort 无合适索引支持 GROUP BY
JOIN 无索引 JOIN details ON o.id = d.order_id type=ALL(驱动表), type=ALL(被驱动表) 被驱动表关联列无索引

3.9 在 Go 中集成 EXPLAIN 自动化检查

在 Go 项目中,可以将 EXPLAIN 集成到测试或 CI 流程中,自动检测新增 SQL 的执行计划是否达标:

// Go 自动化 EXPLAIN 检查示例
func CheckExplain(db *sql.DB, query string) error {
    explainSQL := "EXPLAIN " + query
    rows, err := db.Query(explainSQL)
    if err != nil {
        return err
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var selectType, table, typ, possibleKeys, key, keyLen, ref, extra string
        var rowsCount int
        var filtered float64
        err := rows.Scan(&id, &selectType, &table, &typ,
            &possibleKeys, &key, &keyLen, &ref, &rowsCount, &filtered, &extra)
        if err != nil {
            return err
        }

        // 规则:type 不能为 ALL,rows 不能超过 10000
        if typ == "ALL" {
            return fmt.Errorf("全表扫描: table=%s, rows=%d", table, rowsCount)
        }
        if rowsCount > 10000 {
            return fmt.Errorf("扫描行数过多: table=%s, rows=%d", table, rowsCount)
        }
        if strings.Contains(extra, "Using filesort") {
            return fmt.Errorf("存在文件排序: table=%s", table)
        }
    }
    return nil
}
最佳实践:在 Go 项目的数据库迁移脚本或 DAO 层单元测试中,对每个新 SQL 执行 EXPLAIN 检查,将慢查询扼杀在开发阶段,避免上线后拖垮连接池。