3、SQL查询执行计划基础:EXPLAIN命令解读、全表扫描与索引扫描、type列详解
当Go Web服务出现慢查询时,第一件事不是优化代码,而是用EXPLAIN查看SQL执行计划。执行计划是数据库优化器为SQL语句选择的执行路径,它直接告诉你:查询走了哪个索引、扫描了多少行、是否使用了临时表或文件排序。本章从最基础的EXPLAIN命令开始,逐步拆解type列中从全表扫描到常量查找的完整性能阶梯。
3.1 EXPLAIN命令基础
EXPLAIN是MySQL、PostgreSQL等数据库提供的诊断命令,用于展示SQL语句的执行计划。在MySQL中,用法非常简单:
-- 查看SELECT查询的执行计划
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- 在Go中,使用database/sql执行EXPLAIN
rows, err := db.Query("EXPLAIN SELECT * FROM orders WHERE user_id = ?", userID)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// 遍历rows,读取执行计划列
for rows.Next() {
var id int
var selectType, table, partitions, typ, possibleKeys, key, keyLen, ref, rows, extra string
rows.Scan(&id, &selectType, &table, &partitions, &typ, &possibleKeys, &key, &keyLen, &ref, &rows, &extra)
fmt.Printf("type=%s, rows=%s, key=%s, extra=%s\n", typ, rows, key, extra)
}
EXPLAIN输出包含多个关键列,其中type列是衡量查询性能的核心指标。下表列出了type列从最差到最优的完整取值:
| type值 | 性能等级 | 含义 | 典型场景 |
|---|---|---|---|
| ALL | 最差 | 全表扫描,遍历整张表 | 无索引、查询条件无法使用索引 |
| index | 较差 | 全索引扫描,遍历整个索引树 | 查询覆盖索引但无过滤条件 |
| range | 中等 | 索引范围扫描,只扫描索引的一部分 | BETWEEN、>、<、IN、LIKE前缀匹配 |
| ref | 良好 | 非唯一索引等值匹配 | 普通索引列等值查询 |
| eq_ref | 优秀 | 唯一索引等值匹配,最多返回一行 | 主键或唯一索引关联查询 |
| const | 最优 | 常量查找,最多返回一行 | 主键或唯一索引等值查询 |
EXPLAIN显示type=ALL且rows超过1000,几乎可以确定这是一个需要优化的慢查询。建议在ORM框架(如GORM、sqlx)的日志中间件中自动记录EXPLAIN结果,方便快速定位问题。
3.2 全表扫描(ALL)与索引扫描(index)
全表扫描(ALL)是性能最差的执行方式。数据库会从表的第一行开始,逐行读取所有数据,直到找到满足条件的行。对于大表,全表扫描意味着数百万甚至上亿行的I/O操作。
-- 假设users表有100万行,无索引
EXPLAIN SELECT * FROM users WHERE email = 'test@example.com';
-- type: ALL, rows: 1000000, Extra: Using where
全索引扫描(index)虽然比全表扫描稍好,但仍然遍历了整个索引树。区别在于:全表扫描读取的是数据行,全索引扫描读取的是索引条目。如果索引比数据小,全索引扫描的I/O成本更低,但本质上仍然是全量扫描。
-- 假设email列有索引,但查询条件无法利用索引前缀
EXPLAIN SELECT email FROM users WHERE email LIKE '%example.com';
-- type: index, rows: 1000000, Extra: Using where; Using index
- ALL:扫描数据行,通常伴随
Using where,表示在存储引擎层过滤。 - index:扫描索引条目,通常伴随
Using index,表示查询使用了覆盖索引(查询列都在索引中)。 - 两者都是O(n)复杂度,对于大表不可接受。
3.3 索引范围扫描(range)
range表示索引范围扫描,只扫描索引树中满足范围条件的部分。这是从全量扫描到部分扫描的质变,性能大幅提升。
-- 假设orders表在created_at列有索引
EXPLAIN SELECT * FROM orders WHERE created_at BETWEEN '2024-01-01' AND '2024-01-31';
-- type: range, rows: 5000, Extra: Using index condition
-- 使用IN子句也会触发range
EXPLAIN SELECT * FROM products WHERE id IN (1, 2, 3, 100, 200);
-- type: range, rows: 5, Extra: Using where
触发range的常见操作:BETWEEN、>、<、>=、<=、IN、LIKE 'prefix%'(前缀匹配)。
LIKE '%prefix'或LIKE '%middle%'不会触发range,而是退化为ALL或index。只有LIKE 'prefix%'才能利用索引范围扫描。
3.4 非唯一索引等值匹配(ref)
ref表示使用非唯一索引进行等值匹配。对于普通索引列(允许重复值),等值查询会返回一个或多个匹配行。这是OLTP系统中最常见的执行计划类型。
-- 假设users表在status列有普通索引
EXPLAIN SELECT * FROM users WHERE status = 'active';
-- type: ref, rows: 50000, key: idx_status, ref: const
-- 多列索引的前缀匹配也会触发ref
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'pending';
-- type: ref, rows: 10, key: idx_user_status, ref: const,const
ref的rows值表示估计的匹配行数。如果rows很大(例如超过表行数的10%),数据库可能认为全表扫描更优,从而退化为ALL。
3.5 唯一索引等值匹配(eq_ref)
eq_ref是关联查询中的最优类型之一。它表示使用主键或唯一索引进行等值关联,对于驱动表的每一行,被驱动表最多返回一行匹配数据。
-- 假设orders表通过user_id关联users表的主键id
EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE u.id = 100;
-- 对于users表:type: const
-- 对于orders表:type: eq_ref, key: PRIMARY, ref: const
-- 多表关联中的eq_ref
EXPLAIN SELECT * FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.id = 5000;
-- 对于orders表:type: const
-- 对于order_items表:type: eq_ref, key: idx_order_id, ref: const
eq_ref与ref的核心区别:eq_ref保证最多返回一行(唯一约束),而ref可能返回多行(非唯一索引)。
3.6 常量查找(const)
const是执行计划的最高等级。它表示数据库在查询优化阶段就能确定最多返回一行,且该行的值在编译期已知。通常发生在主键或唯一索引的等值查询中。
-- 主键等值查询
EXPLAIN SELECT * FROM users WHERE id = 100;
-- type: const, rows: 1
-- 唯一索引等值查询
EXPLAIN SELECT * FROM users WHERE email = 'admin@example.com';
-- type: const, rows: 1
-- 多列唯一索引的完全匹配
EXPLAIN SELECT * FROM accounts WHERE tenant_id = 1 AND account_id = 'ACC-001';
-- type: const, rows: 1
当type=const时,数据库会将查询结果当作常量处理,后续的关联查询可以直接使用这个常量值,避免重复查找。
3.7 执行计划类型对比SVG图
下图展示了从全表扫描到常量查找的性能阶梯,以及每种类型对应的扫描范围和典型场景:
3.8 实战:从EXPLAIN到Go代码优化
在Go Web服务中,当遇到慢查询时,按以下步骤排查:
- 捕获慢查询:在数据库连接池中设置
slow_query_log,或使用GORM的Logger接口记录执行时间超过阈值的SQL。 - 执行EXPLAIN:对慢查询SQL执行
EXPLAIN,重点关注type、rows、key、Extra列。 - 判断type等级:
- 如果
type=ALL或index:立即添加或优化索引。 - 如果
type=range且rows很大:考虑增加过滤条件或创建复合索引。 - 如果
type=ref或更高:检查rows是否合理,Extra是否有Using filesort或Using temporary。
- 如果
- 在Go中验证:修改查询后,在测试环境用
EXPLAIN验证执行计划变化,确认type提升。
// Go代码示例:使用EXPLAIN验证优化效果
func validateQuery(db *sql.DB, query string, args ...interface{}) error {
explainQuery := "EXPLAIN " + query
rows, err := db.Query(explainQuery, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int
var selectType, table, typ, key, extra string
var partitions, possibleKeys, keyLen, ref, rowsCount sql.NullString
rows.Scan(&id, &selectType, &table, &partitions, &typ, &possibleKeys, &key, &keyLen, &ref, &rowsCount, &extra)
fmt.Printf("表: %s, type: %s, 索引: %s, 预估行数: %s, 额外: %s\n",
table, typ, key.String, rowsCount.String, extra)
if typ == "ALL" || typ == "index" {
return fmt.Errorf("查询 %s 使用了全表/全索引扫描,需要优化", query)
}
}
return nil
}
EXPLAIN自动化检查。对于每个新SQL查询,自动执行EXPLAIN并检查type是否达到ref或更高,否则拒绝合并请求。这能有效防止慢查询进入生产环境。