1. 面试官为什么爱问数据库调优连环炮
1.1 一道题背后藏着三层能力考察
数据库调优这个话题,在技术面试里出现的频率高得离谱。你投的是后端开发,面试官问索引;你投的是运维,面试官问慢查询;你投的是数据开发,面试官还是问执行计划。为什么?因为数据库调优是一块试金石,它能同时检验三个维度的能力:基础原理的扎实程度、问题排查的系统性思维、以及工程实践中的取舍判断。
我面过不少人,也被人面过不少次。一个很深的感受是:面试官问“这条SQL为什么慢”,他真正想听的从来不是“加个索引就好了”这种标准答案。他想听的是你拿到一条慢SQL之后,脑子里那套完整的排查链路——先看什么、再看什么、怎么验证、怎么权衡。这套链路,才是区分“背过八股文”和“真正干过活”的分水岭。
1.2 连环combo的典型套路拆解
所谓“连环combo”,就是面试官不会只问一个问题就放过你,而是像剥洋葱一样一层一层往里钻。常见的套路是这样的:
- 第一层:“你遇到过慢查询吗?怎么排查的?”——这是热身,看你会不会用基本工具。
- 第二层:“你说加了索引,那为什么加了索引还是慢?”——这是试探,看你知不知道索引失效的场景。
- 第三层:“如果索引没问题,执行计划也走了索引,但还是很慢,怎么办?”——这是深水区,考的是锁、回表、数据量、硬件、参数配置等综合因素。
- 第四层:“线上不能随便改,你怎么在不影响业务的前提下定位问题?”——这是实战题,考的是你对生产环境工具链和风险控制的认知。
这四层下来,能扛住的人不多。但扛住了,基本就稳了。下面我按这个逻辑,把每一层拆开讲透,顺便把我在实际工作中踩过的坑和总结的技巧都倒出来。
2. 第一层:慢查询的定位与基础排查
2.1 慢查询日志:你的第一手现场证据
排查慢查询,第一步永远是拿到现场证据。数据库一般都有慢查询日志功能,以MySQL为例,核心参数是slow_query_log、long_query_time和log_queries_not_using_indexes。我通常会把long_query_time设成1秒,先跑一段时间看看整体情况,再根据业务敏感度调整。
-- 查看当前慢查询配置 SHOW VARIABLES LIKE 'slow_query%'; SHOW VARIABLES LIKE 'long_query_time'; -- 动态开启慢查询日志(重启失效,生产环境慎用) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;拿到慢查询日志后,不要急着一条条看。先用mysqldumpslow或者pt-query-digest做聚合分析,按执行次数、总耗时、平均耗时排序。执行次数多但单次快的SQL,往往比执行次数少但单次极慢的SQL更值得优化,因为它对系统整体吞吐的影响更大。
注意:生产环境开启慢查询日志会带来一定的I/O开销,建议在业务低峰期开启,或者只开启
log_queries_not_using_indexes来捕获未走索引的查询。
2.2 EXPLAIN:执行计划的正确读法
拿到可疑SQL之后,EXPLAIN是必用的。但很多人只看type列,看到ref或range就放心了,这是不够的。我读执行计划会重点关注这几列:
| 列名 | 关注点 | 危险信号 |
|---|---|---|
| type | 访问类型 | ALL(全表扫描)、index(全索引扫描) |
| key | 实际使用的索引 | NULL(没走索引) |
| rows | 预估扫描行数 | 数值过大,与实际返回行数差距悬殊 |
| filtered | 过滤比例 | 百分比过低,说明扫描了大量无用数据 |
| Extra | 额外信息 | Using filesort、Using temporary |
Extra列里的Using filesort和Using temporary是两个高频杀手。前者意味着排序没有用到索引,后者意味着用了临时表。这两个操作在数据量大的时候,性能下降是指数级的。
2.3 一个真实的排查案例
之前有个业务反馈某个列表接口响应时间从200ms涨到了3秒。我拿到慢查询日志,发现是一条带ORDER BY create_time DESC LIMIT 20的分页查询。EXPLAIN显示走了create_time的索引,type是range,看起来没问题。
但仔细看rows列,预估扫描行数是80万。问题就出在这里:虽然走了索引,但因为查询条件里还有一个status = 1的过滤条件,而status不在索引里,导致数据库需要回表80万次才能过滤出符合条件的20条记录。这就是典型的索引选择性不足导致的回表放大。
解决方案是建一个联合索引(status, create_time),让过滤和排序都在索引里完成,回表次数从80万降到20次。改完之后接口响应时间回到了150ms。
实操心得:看执行计划不能只看有没有走索引,还要看走了索引之后扫描了多少行、回了多少次表。
rows列和实际返回行数的比值,是判断索引效率的一个关键指标。
3. 第二层:索引失效的七种典型场景
3.1 最左前缀原则与范围查询的陷阱
联合索引(a, b, c),查询条件WHERE a = 1 AND b > 2 AND c = 3,c能用上索引吗?答案是不能。因为b用了范围查询,索引在b之后就不再有序了,c无法继续利用索引的有序性。
这个点面试官特别爱问,而且会变着花样问。比如WHERE a = 1 AND b LIKE 'xxx%' AND c = 3,c同样用不上索引,因为LIKE 'xxx%'本质上也是范围查询。
正确的做法是调整索引列的顺序,把等值查询的列放在范围查询的列前面。如果业务允许,也可以把范围查询拆成多个等值查询,比如把b > 2拆成b IN (3, 4, 5),这样c就能用上索引了。
3.2 隐式类型转换:看不见的索引杀手
这个坑我踩过不止一次。表里user_id是varchar类型,查询的时候写WHERE user_id = 12345,不加引号。MySQL会做隐式类型转换,把user_id转成数字再比较,索引直接失效。
-- 索引失效 SELECT * FROM orders WHERE user_id = 12345; -- 索引生效 SELECT * FROM orders WHERE user_id = '12345';这个问题在代码里特别隐蔽,因为很多ORM框架会自动处理参数类型,但有时候传参的时候类型不对,框架也不会报错,直接就发出去了。我的建议是:字段类型和传参类型必须严格一致,代码review的时候把这条列为必查项。
3.3 函数操作与计算:索引的天然敌人
WHERE DATE(create_time) = '2024-01-01',这个查询用不上create_time的索引。因为索引里存的是原始的create_time值,不是DATE(create_time)的结果。数据库没法用索引去匹配一个函数计算后的值。
正确的写法是改成范围查询:
-- 索引失效 SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01'; -- 索引生效 SELECT * FROM orders WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00';同样的道理,WHERE id + 1 = 10也用不上索引,要改成WHERE id = 9。索引列上不能做任何计算、函数、类型转换,这是铁律。
3.4 OR条件与索引合并的代价
WHERE a = 1 OR b = 2,如果a和b各自有索引,MySQL可能会用索引合并(index merge)来优化。但索引合并的效率通常不如联合索引,因为需要分别扫描两个索引再取并集,中间还有去重和排序的开销。
更好的做法是建联合索引(a, b),或者把OR改写成UNION:
-- 可能走索引合并,效率一般 SELECT * FROM t WHERE a = 1 OR b = 2; -- 改写为UNION,各自走索引 SELECT * FROM t WHERE a = 1 UNION SELECT * FROM t WHERE b = 2;注意:UNION会自动去重,如果不需要去重就用UNION ALL,性能更好。
3.5 LIKE通配符的位置决定生死
LIKE '%abc'和LIKE 'abc%',前者索引失效,后者索引生效。这个大家都知道。但实际业务里,经常有“模糊搜索”的需求,用户就是要搜中间包含某个关键词的记录。这时候怎么办?
我的经验是:如果数据量不大,全表扫描也能接受,那就别折腾。如果数据量大,考虑用全文索引(fulltext index),或者引入专门的搜索引擎。不要试图用B+树索引去解决所有搜索问题,工具要选对场景。
3.6 索引选择性:不是所有索引都值得建
索引的选择性 = 不重复的索引值数量 / 表的总行数。选择性越高,索引效率越高。比如性别字段,只有男/女两个值,选择性极低,建索引基本没用。而用户ID、订单号这种,选择性接近1,建索引效果最好。
我一般会用一个简单的SQL来评估:
SELECT COUNT(DISTINCT column_name) / COUNT(*) AS selectivity FROM table_name;如果结果小于0.1,这个字段单独建索引的意义就不大,需要考虑和其他字段组合。
3.7 索引失效速查表
| 场景 | 是否失效 | 解决方案 |
|---|---|---|
| 联合索引不满足最左前缀 | 失效 | 调整索引列顺序或查询条件 |
| 范围查询后的列 | 失效 | 把等值条件放前面,或拆分范围 |
| 隐式类型转换 | 失效 | 保证传参类型与字段类型一致 |
| 索引列上使用函数 | 失效 | 改写为范围查询 |
| OR条件 | 可能失效 | 改写为UNION或建联合索引 |
| LIKE以%开头 | 失效 | 改用全文索引或搜索引擎 |
| 索引选择性过低 | 效果差 | 考虑联合索引或不建 |
4. 第三层:索引没问题但依然慢的深层原因
4.1 回表:索引的隐形代价
二级索引的叶子节点存的是主键值,不是完整的行数据。所以通过二级索引查询时,如果需要获取索引之外的列,就必须拿着主键值回到聚簇索引里再查一次,这就是回表。
回表的代价在于:每次回表都是一次随机I/O。如果二级索引扫描出1000行,就要回表1000次。在机械硬盘上,随机I/O的代价是顺序I/O的几十倍甚至上百倍。
减少回表的手段有两个:一是覆盖索引,让查询需要的列全部包含在索引里;二是延迟关联,先用覆盖索引查出主键,再用主键去关联原表。
-- 延迟关联示例 SELECT t.* FROM orders t INNER JOIN ( SELECT id FROM orders WHERE status = 1 ORDER BY create_time DESC LIMIT 100000, 20 ) AS tmp ON t.id = tmp.id;子查询里只查主键,可以用覆盖索引完成,避免回表。外层再用主键关联,只回表20次。
4.2 锁等待与并发争用
有时候SQL本身没问题,执行计划也漂亮,但就是慢。这时候要怀疑锁。SHOW ENGINE INNODB STATUS可以看最近的锁等待情况,information_schema.INNODB_TRX和INNODB_LOCKS可以看当前活跃的事务和锁。
我遇到过一个案例:一个看似简单的UPDATE语句,执行时间偶尔飙到好几秒。排查发现是多个事务在竞争同一行记录的排他锁,形成了锁等待队列。根本原因是业务逻辑里事务粒度太大,一个事务里做了太多操作,持锁时间过长。
解决方案是缩小事务范围,把不必要的操作移出事务,或者调整业务逻辑减少热点行的并发更新。
4.3 数据量与统计信息失真
当表的数据量增长到一定程度,优化器的统计信息可能失真,导致它选了一个“看起来对但实际上错”的执行计划。这时候可以用ANALYZE TABLE手动更新统计信息。
另外,当单表数据量超过千万级别,即使有索引,维护索引的代价也会变得很高。这时候要考虑分区表或者分库分表。分区表把数据按规则拆到不同的物理文件,查询时可以只扫描相关分区,减少I/O。
-- 按时间范围分区示例 CREATE TABLE orders ( id BIGINT, create_time DATETIME, ... ) PARTITION BY RANGE (YEAR(create_time)) ( PARTITION p2022 VALUES LESS THAN (2023), PARTITION p2023 VALUES LESS THAN (2024), PARTITION p2024 VALUES LESS THAN (2025) );实操心得:分区表的分区键必须包含在查询条件里,否则会扫描所有分区,性能反而更差。而且分区数量不宜过多,一般建议控制在50个以内。
4.4 硬件与参数配置的瓶颈
如果SQL和索引都优化到位了,还是慢,那就要往上看:磁盘I/O、内存、CPU、网络。iostat看磁盘,top看CPU和内存,sar看网络。
数据库参数方面,innodb_buffer_pool_size是最关键的。它决定了InnoDB能缓存多少数据和索引在内存里。如果这个值太小,大量查询需要读磁盘,性能自然上不去。一般建议设成物理内存的50%到70%。
-- 查看缓冲池命中率 SHOW STATUS LIKE 'Innodb_buffer_pool_read%'; -- 命中率 = (1 - reads / read_requests) * 100%命中率低于95%就说明缓冲池可能不够用了。
5. 第四层:生产环境的安全排查与优化流程
5.1 线上问题定位的黄金三分钟
线上出问题,第一要务是恢复业务,不是找根因。我通常按这个顺序操作:
- 看监控大盘:确认是数据库问题还是应用问题,是单条SQL慢还是整体负载高。
- 看当前活跃会话:
SHOW PROCESSLIST或information_schema.PROCESSLIST,找出执行时间长的SQL。 - 看锁等待:如果有大量会话处于
Waiting for table metadata lock或Waiting for row lock,说明有锁争用。 - 必要时kill掉阻塞源:找到阻塞其他会话的源头,评估影响后kill掉。
-- 找出执行时间超过10秒的会话 SELECT * FROM information_schema.PROCESSLIST WHERE COMMAND != 'Sleep' AND TIME > 10 ORDER BY TIME DESC; -- kill指定会话 KILL CONNECTION 12345;注意:kill会话之前一定要确认这个会话在做什么,kill错了可能导致数据不一致。生产环境操作前最好和业务方确认。
5.2 在线DDL:加索引不能锁表
给大表加索引,如果直接ALTER TABLE ADD INDEX,在MySQL 5.6之前会锁表,业务直接不可用。5.6之后支持Online DDL,但也不是完全无锁,只是大部分阶段不阻塞读写。
更稳妥的做法是用pt-online-schema-change或gh-ost这类工具。它们的原理是:建一张新表,把原表数据慢慢同步过去,同步过程中在原表上建触发器捕获变更,最后原子性地替换表名。整个过程对业务几乎无感知。
# pt-online-schema-change 示例 pt-online-schema-change \ --alter "ADD INDEX idx_status_create_time (status, create_time)" \ --host=127.0.0.1 \ --user=admin \ --password=****** \ --database=biz_db \ --table=orders \ --execute实操心得:用这类工具之前,一定要在测试环境完整跑一遍,确认触发器逻辑和业务兼容。另外要关注磁盘空间,因为要复制一份完整的数据。
5.3 灰度验证与回滚预案
任何优化上线之前,必须准备好回滚方案。加索引的回滚就是删索引,但删索引本身也可能锁表,所以要用同样的在线工具操作。改SQL的回滚就是切回旧版本,这个相对简单。
灰度验证的思路是:先在小流量环境验证,观察核心指标(响应时间、QPS、错误率、数据库负载)有没有改善,确认无误后再逐步放量。不要一次性全量上线,否则出了问题影响面太大。
5.4 优化效果评估的量化指标
优化不能凭感觉,要有数据支撑。我一般会记录优化前后的这几个指标:
| 指标 | 优化前 | 优化后 | 目标 |
|---|---|---|---|
| 平均响应时间 | 3s | 150ms | < 200ms |
| 扫描行数 | 80万 | 20 | 接近返回行数 |
| 回表次数 | 80万 | 20 | 最小化 |
| 数据库CPU | 80% | 30% | < 50% |
| 慢查询数量 | 100/分钟 | 0 | 0 |
这些数据从慢查询日志、监控系统和执行计划里都能拿到。有了量化对比,才能证明优化是有效的,也方便后续复盘。
6. 面试中如何把连环炮变成加分项
6.1 用排查链路代替标准答案
面试官问“怎么排查慢查询”,不要只回答“看慢查询日志”。要展示你的完整链路:慢查询日志定位 → EXPLAIN分析执行计划 → 检查索引使用情况 → 分析回表和锁 → 评估数据量和配置 → 制定优化方案 → 灰度验证。这条链路说出来,面试官就知道你是真干过。
6.2 主动暴露取舍与权衡
优化从来不是“越快越好”,而是“在业务可接受的范围内找到性价比最高的方案”。比如加索引能提升查询性能,但会降低写入性能、占用更多磁盘。分库分表能解决数据量问题,但会带来分布式事务和跨库查询的复杂度。面试官很吃这一套,因为这说明你有全局观,不是只会背八股。
6.3 用真实案例收尾
最后,准备一两个你实际做过的优化案例。不用多复杂,但要有细节:什么业务场景、什么现象、怎么排查、怎么解决、效果如何。细节越具体,可信度越高。比如“我把一个分页查询从3秒优化到150毫秒,核心是建了一个联合索引避免了80万次回表”,这一句话就够面试官追问半天了,而你已经准备好了所有答案。
数据库调优这件事,说到底就是理解原理、掌握工具、积累经验。原理让你知道为什么,工具让你知道怎么做,经验让你知道什么时候该做什么。这三样凑齐了,面试连环炮也好,线上故障也好,都能从容应对。