07. 查询优化与执行计划

深入数据库查询优化器:基于代价的优化(CBO)原理、EXPLAIN 执行计划分析、索引选择策略与慢 SQL 优化实战。

1. 查询执行流程

SQL 语句
  → 词法/语法分析 → 解析树(Parse Tree)
  → 语义检查(表/列存在性、权限)
  → 查询重写(视图展开、子查询优化、常量传播)
  → 生成候选执行计划
  → 代价估算(统计信息 + 代价模型)
  → 选择最优计划
  → 执行引擎执行

2. EXPLAIN 详解

2.1 EXPLAIN 输出列

EXPLAIN ANALYZE
SELECT e.name, d.name, SUM(s.amount)
FROM employees e
JOIN departments d ON e.dept_id = d.id
JOIN sales s ON e.id = s.emp_id
WHERE s.date > '2024-01-01'
GROUP BY e.id, d.name;
说明关键值
id执行顺序编号相同 id 从上到下执行
select_type查询类型SIMPLE/PRI/ SUBQUERY/DERIVED
table访问的表别名/子查询
type访问类型** ALL < index < range < ref < eq_ref < const **
possible_keys可能用的索引
key实际用的索引
key_len索引使用长度越短越好(但需完整匹配)
ref索引对比值const/列名
rows估算扫描行数越小越好
filtered过滤后剩余比例越高越好
Extra额外信息Using index 好,Using filesort

2.2 type 列优化目标

type说明场景
system系统表,仅一行极罕见
const主键/唯一索引等值WHERE id = 1
eq_refJOIN 主键/唯一索引JOIN 条件为主键
ref普通索引等值WHERE status = 'active'
range索引范围WHERE id BETWEEN 1 AND 100
index全索引扫描索引覆盖 SELECT 列
ALL全表扫描无索引或索引未命中

优化目标:至少要达到 range,最好是 ref 或 eq_ref

2.3 Extra 列关注

含义好坏
Using index覆盖索引✅ 好
Using whereWHERE 过滤⚠️ 正常
Using filesort额外排序❌ 差(内存/磁盘排序)
Using temporary临时表❌ 差(GROUP BY/DISTINCT)
Using join buffer连接缓存⚠️ 大表 JOIN 需要
Impossible WHERE永远为假✅ 优化器直接返回空

3. 统计信息与代价模型

3.1 统计信息类型

表级统计:
  - 行数(rows)
  - 数据页数
  - 平均行长度

列级统计(直方图/采样):
  - 不同值数量(Cardinality)
  - 最小/最大值
  - NULL 比例
  - 数据分布(直方图,8.0+)

索引统计:
  - 索引页数
  - 索引 Cardinality

3.2 代价估算

代价 = I/O 代价 + CPU 代价

单表查询估算:
  - 全表扫描代价 = 数据页数 × 每页 I/O 代价
  - 索引扫描代价 = 索引页数 × I/O 代价 + 回表行数 × I/O 代价

多表 JOIN 估算:
  - Nested Loop:外表行数 × 内表单次查找代价
  - Hash Join:构建哈希表代价 + 探测代价
  - Merge Join:排序代价 + 合并代价

4. 优化技巧

4.1 常见慢查询优化

-- 1. 避免 SELECT *
SELECT id, name FROM users WHERE status = 'active';
  -- 比 SELECT * 更可能走覆盖索引

-- 2. 范围查询后的列用不了索引
CREATE INDEX idx_a_b ON t(a, b);
SELECT * FROM t WHERE a > 1 AND b = 2;
  -- a 是范围,b 不走索引
  -- 解决:INDEX(a) 或改写为多个 = 条件

-- 3. 大偏移分页优化(1000000, 10)
-- ❌ 慢:扫描 1000010 行,取 10 行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

-- ✅ 快:先查 id,再关联
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) tmp
ON o.id = tmp.id;

-- 4. 隐式转换导致索引失效
WHERE phone = 13800138000  -- phone 是 varchar,先转 int
-- ✅ WHERE phone = '13800138000'

-- 5. IN vs EXISTS
-- 大表 IN 小表 → EXISTS 更优
-- 小表 IN 大表 → IN 更优

4.2 JOIN 优化

-- 驱动表选择:小表驱动大表
SELECT * FROM small_table s
JOIN big_table b ON s.id = b.small_id;
  -- small_table 作为驱动表(外表)
  -- 对 big_table 走索引查找

-- 避免笛卡尔积
SELECT * FROM a, b;  -- 如果没有连接条件 → CROSS JOIN
SELECT * FROM a JOIN b ON a.id = b.a_id;  -- ✅ 正确

5. 慢查询分析流程

1. 发现慢查询
   - 开启慢查询日志:slow_query_log = ON, long_query_time = 1
   - 或 performance_schema.events_statements_summary_by_digest

2. 获取执行计划
   EXPLAIN ANALYZE SELECT ...;

3. 分析瓶颈
   - type = ALL?→ 加索引
   - Extra = Using filesort?→ 排序字段加入索引
   - Extra = Using temporary?→ 优化 GROUP BY
   - rows 很大?→ 减少扫描范围或加索引

4. 验证优化效果
   - 对比优化前后的 EXPLAIN
   - 使用 bench 测试真实性能

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. 缓存架构演进之路:从单机 Redis 到亿级分布式多级缓存体系
  2. Redis 7.x 重大新特性与架构升级深度解析
  3. Redis 消息队列深度对比:Pub/Sub、Streams 与 Kafka/RabbitMQ 选型指南