Appearance
数据库语句优化手段
声明:默认指 MySQL 的 InnoDB 存储引擎,以 MySQL 8.0 为主视角,涉及 5.7/8.0 行为差异处会单独标注。
一句话概括 SQL 优化的方法论:定位 → 分析 → 改写/加索引 → 验证。面试时最有价值的不是背"多少条手段",而是能讲清楚三件事:
- 慢是怎么被发现的。
- 为什么慢。
- 改了之后怎么证明变快了。
1 优化方法论总览
慢在哪? 为什么慢? 怎么改? 怎么证明?
+-----------+ +----------------+ +------------------+ +-----------+
| 慢查询日志 | --> | EXPLAIN 执行计划 | --> | 索引 / SQL 改写 | --> | 对比耗时 |
| 应用监控 | | EXPLAIN ANALYZE | | 表结构 / 参数 | | rows/成本 |
| show proce| | 锁等待/IO 视角 | | 读写分离/分库分表 | | 慢日志回归 |
+-----------+ +----------------+ +------------------+ +-----------+- 先定位"哪几条 SQL 吃掉了大部分资源"(二八原则:少数慢 SQL 占了大部分总耗时);
- 再分析是索引问题、写法问题、还是数据量到了架构的临界点;
- 改完必须量化验证,避免"感觉变快了"。
2 定位慢 SQL 的手段
- 慢查询日志:
slow_query_log = ON,long_query_time(生产常设 0.5 s 或 1 s;排查期可临时调小到 0.1 并短开)。配合pt-query-digest按指纹聚合,直接给出 TOP N 慢 SQL; - 实时排查:
SHOW PROCESSLIST看当前正在跑的语句和状态(Sending data、Waiting for table lock、Sorting result 都是有信息量的状态)。遇到锁等待用performance_schema的data_locks/data_lock_waits(8.0)。5.7 及以前是information_schema.innodb_locks/innodb_lock_waits,8.0 已移除; - sys schema / performance_schema:
sys.statements_with_full_table_scans、sys.schema_index_statistics这类现成视图,8.0 开箱即用; - 应用层:APM(SkyWalking/Arthas trace)定位到具体接口里的具体 SQL,比从数据库侧倒推更快。
3 EXPLAIN 精读(分析手段的核心)
sql
EXPLAIN SELECT ...;重点看 4 列:
- type(访问类型),从好到坏:
| 级别 | 含义 | 典型场景 |
|---|---|---|
| system/const | 主键或唯一索引等值查询,最多一行 | WHERE id = 1 |
| eq_ref | JOIN 时被驱动表走主键/唯一索引 | JOIN b ON b.id = a.bid |
| ref | 普通二级索引等值查询 | WHERE idx_col = 'x' |
| range | 索引范围扫描 | WHERE id > 100、IN (...) |
| index | 扫整棵索引树(比全表好一点,免回表时还行) | 覆盖索引下的 count |
| ALL | 全表扫描 | 需要优化 |
- key/key_len:实际使用的索引及使用到的字节数,可反推联合索引用到第几列;
- rows/filtered:优化器估算的扫描行数与过滤后比例(估算值,不等于真实值;8.0.18+ 用
EXPLAIN ANALYZE可看真实行数与耗时); - Extra:
| 值 | 含义 | 好坏 |
|---|---|---|
| Using index | 覆盖索引,免回表 | 好 |
| Using index condition | ICP 索引条件下推 | 好(省回表) |
| Using where | 服务层再过滤 | 中性 |
| Using filesort | 额外排序 | 大结果集时要警惕 |
| Using temporary | 中间临时表 | GROUP BY/DISTINCT 常见,要警惕 |
| Using join buffer (Block Nested Loop) | 被驱动表无索引,5.7 会用 BNL | 坏 |
| Using join buffer (hash join) | 8.0.18+ 无索引 JOIN 时的哈希连接 | 比 BNL 好,但仍建议补索引 |
4 索引层优化手段
- 覆盖索引:把
SELECT、WHERE、ORDER BY涉及的列都放进联合索引,Extra: Using index免回表,是 SQL 优化里性价比最高的一招; - 最左前缀对齐:联合索引列顺序 = 等值条件列在前、范围条件列在后、排序列衔接 WHERE 列。已有
(a)再想要(a,b)时直接扩展原索引而不是新建; - 前缀索引:长字符串列取前 N 个字符建索引节省空间。
COUNT(DISTINCT LEFT(col, N)) / COUNT(*)趋于饱和的 N 即可选。代价是无法用于覆盖索引和ORDER BY; - 函数索引(8.0.13+):
CREATE INDEX idx_y ON t ((YEAR(create_time)));,让"列上带函数"的查询也能走索引;5.7 做不到,只能改写 SQL; - 索引列保持干净:
WHERE YEAR(t)=2024改成区间。字符串列与数字比较时给常量加引号,避免隐式转换导致索引失效。细节见同目录《数据库索引失效总结》; - 删索引走灰度:8.0 可先把索引设为
INVISIBLE观察,无退化再 DROP;5.7 建议用 gh-ost/pt-osc 在线操作。
5 SQL 写法层优化手段
5.1 只查需要的列
SELECT * 的三宗罪:无法覆盖索引(必然回表)、拉宽网络与内存占用、破坏通过视图/代理层的列变更兼容性。列出明确列名是底线。
5.2 深分页优化(高频考点)
sql
-- 反面:OFFSET 越大越慢,MySQL 要取出并丢弃前 1000000 行
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- 方案一:游标/书签(连续翻页、上页最大 id 已知时,最优)
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
-- 方案二:延迟关联(先在覆盖索引上拿 id,再回表取 10 行)
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) t ON o.id = t.id;原理:慢在"回表 100 万次";先用覆盖索引把目标 10 行的主键找出来,回表次数从 100 万+10 降到 10。
5.3 JOIN 优化
- 小结果集驱动大表(优化器一般会自动选,被驱动表的关联列必须有索引,争取 eq_ref/ref)。确认选错顺序时用
STRAIGHT_JOIN强制; - 关联两侧类型、字符集、collation 必须一致,否则隐式转换导致索引失效;
- 无索引 JOIN:5.7 走 Block Nested Loop(被驱动表被反复全扫,极慢)。8.0.18+ 自动改用 hash join(8.0.20 起彻底移除 BNL),能救急但不是不加索引的理由;
- 大表 JOIN 小表且数据可冗余时,考虑应用层组装代替 JOIN(分库分表后更是标准做法)。
5.4 子查询、IN 与 EXISTS
- 老经验"小表 IN 大表、大表 EXISTS 小表"源自 5.5 时代相关子查询的低效实现。5.6+ 优化器会把符合条件的 IN 子查询改写为半连接(semijoin),两者差距已大幅缩小;
NOT IN (子查询)仍容易踩相关子查询的坑,推荐NOT EXISTS或LEFT JOIN ... IS NULL;IN列表过长(几千个值)时改为临时表 JOIN 或改范围条件。
5.5 写操作优化
- 批量插入:
INSERT ... VALUES (...),(...),(...)合并成批(一批几百到几千行),比逐条插快一个量级;更大量级用LOAD DATA(配合local_infile); - 大批量删除/更新分批做:
DELETE ... LIMIT 1000循环 + 间歇提交,避免长事务、大段 undo、主从延迟;跨库归档用pt-archiver; - 避免在事务里做 RPC/耗时计算:长事务撑大 undo、占锁、拖垮 MVCC,是生产事故常客;
UPDATE/DELETE前先EXPLAIN或同条件SELECT确认影响行数。
5.6 其他高频细节
UNION ALL代替UNION(后者要临时表去重,业务允许时省掉);COUNT(*)就用这个写法(InnoDB 下COUNT(*)≈COUNT(1),都会挑最小的索引来扫;COUNT(列)忽略 NULL 语义不同别混用);- GROUP BY 想免临时表:分组列与索引前缀一致且 WHERE 是等值前缀。8.0 已移除 GROUP BY 隐式排序,依赖排序的老 SQL 需显式写 ORDER BY——升级必查项;
- 大事务里避免一次性
SELECT百万行到应用内存,改分页流式处理。
6 架构层优化手段(SQL 改不动时)
- 冷热分离/归档:历史大表拆到归档库,主表保持体量;
- 读写分离:报表类查询走从库;注意复制延迟下的读己之写;
- 分库分表:单表行数经验阈值千万级(更准确看 B+ 树高度和 buffer pool 命中率)。提前定好分片键,跨分片查询靠冗余/宽表;
- 缓存:读多写少的热点数据上 Redis,注意一致性策略(延迟双删/binlog 订阅);
- 异步化:削峰场景把同步写库改为 MQ 异步落库。
7 参数层优化(简要)
innodb_buffer_pool_size:物理内存的 50%~75%,最重要的一个参数;- redo log 容量:
innodb_redo_log_capacity(8.0.30+ 统一参数,替代旧的innodb_log_file_size×innodb_log_files_in_group)太小会频繁 checkpoint 抖动; - 双一配置的取舍:
innodb_flush_log_at_trx_commit=1+sync_binlog=1最安全最慢;非核心业务可放宽(如 2/100)换吞吐; max_connections结合连接池(应用侧 HikariCP 限流)一起治理,避免连接风暴。
8 MySQL 8 特有能力清单(答题时的版本加分项)
| 能力 | 版本 | 对优化的意义 |
|---|---|---|
| 降序索引真正生效 | 8.0 | 混合序 ORDER BY a ASC, b DESC 免 filesort(5.7 忽略 DESC) |
| 跳跃扫描 Skip Scan | 8.0.13 | 前导列取值少时,跳过前导列也能用联合索引 |
| 函数/表达式索引 | 8.0.13 | 列上带函数的查询可以建索引 |
| hash join | 8.0.18(8.0.20 移除 BNL) | 无索引 JOIN 的兜底,性能远好于 BNL |
| EXPLAIN ANALYZE | 8.0.18 | 真实执行并输出每步实际耗时/行数 |
| Instant DDL | 8.0.12(ADD COLUMN),8.0.29 扩展到 DROP COLUMN | 秒级加列不锁表(5.7 只能 gh-ost/pt-osc) |
| 直方图统计 | 8.0 | 无索引列的分布信息辅助成本估算 |
| CTE / 窗口函数 | 8.0 | 复杂分析逻辑(排名、分组 TopN)不用再写多层自连接 |
9 追问链(面试官会怎么往下问)
- Q:线上怎么发现慢 SQL? A:慢查询日志 + pt-query-digest 聚合出 TOP N,配合 APM 定位接口;实时问题用 show processlist 和 sys schema。
- Q:EXPLAIN 主要看什么? A:type 是否 ALL、key/key_len 用没用上/用到第几列、rows 估算量级、Extra 有没有 filesort/temporary/join buffer。
- Q:rows 是准的吗? A:是估算值,基于索引统计(基数)可能偏差很大;ANALYZE TABLE 重新采样,8.0.18+ 用 EXPLAIN ANALYZE 验证真实行数。
- Q:LIMIT 1000000,10 为什么慢?怎么改? A:要扫描并丢弃前 100 万行、且每行都可能回表。用游标(WHERE id > 上页最大 id)或延迟关联(覆盖索引先取 id)。
- Q:为什么加了索引反而更慢/不走? A:区分度低回表成本高于全表扫描。或触发隐式转换/函数/最左前缀这类失效场景。统计信息过期也会误判。
- Q:8.0 无索引 JOIN 还会像 5.7 那么惨吗? A:8.0.18+ 自动 hash join,比 BNL 好很多,但正确做法仍是给关联列补索引。
- Q:单表多少数据要分库分表? A:经验值千万行/单表 10 GB 量级开始规划。但真正的信号是 B+ 树长高、buffer pool 命中率下降、DDL/维护窗口不可接受。
10 小结
- 优化有固定套路:定位(慢日志)→ 分析(EXPLAIN)→ 手段(索引/写法/架构/参数)→ 验证(对比耗时与执行计划);
- 索引层三板斧:覆盖索引、最左前缀对齐、索引列保持干净;
- 写法层高频考点:深分页(游标+延迟关联)、批量写、JOIN 列对齐类型、UNION ALL;
- 数据量到架构临界点时,SQL 层优化收益有限,靠冷热分离/分库分表/缓存解决;
- MySQL 8 的新能力(函数索引、Skip Scan、hash join、EXPLAIN ANALYZE、Instant DDL)既是优化工具也是面试版本差异考点。
2026-10 依据主题重写(原文件为空壳)。