
线上慢SQL的排查思路“先止损 - 再定位 - 后根治 - 防复发”的四步黄金流程 第一步紧急止损线上第一优先级线上出问题先保服务可用再慢慢排查根因确认影响范围通过 APM 工具SkyWalking/Pinpoint定位慢接口评估用户影响面快速熔断降级核心接口被打挂时先降级非核心功能或开启限流Kill 异常慢查询登录数据库执行show full processlist;找到耗时 10s 的线程kill [id];流量切分读压力大时切到从库写压力大时暂时关闭非核心写操作 第二步精准定位问题核心环节2.1 数据库层面排查80% 的问题在这里-- 1. 查看当前运行的所有线程快速定位慢SQLshowfullprocesslist;-- 2. 查看慢查询日志必须提前开启阈值建议设为1sshowvariableslikeslow_query_log;showvariableslikelong_query_time;-- 3. 分析执行计划最关键explainselect*fromuserwherename张三;-- 重点关注type(是否range/ref以上)、key(是否走索引)、rows(扫描行数)、Extra(是否有Using filesort/Using temporary)-- 4. 查看数据库状态showstatuslikeThreads_connected;-- 连接数showengineinnodbstatus;-- 查看事务和锁等待情况2.2 应用层面排查剩下 20% 的坑在这里有没有N1 查询循环调用数据库如查订单列表后循环查每个订单的用户有没有大分页查询limit 1000000, 10会扫描前 100 万行性能极差有没有长事务 / 大事务占用连接资源导致其他请求排队有没有参数配置问题连接池最大连接数太小或innodb_buffer_pool_size不足✅ 第三步针对性根治问题常见慢 SQL 原因 解决方案对照表常见原因典型场景解决方案全表扫描where 条件无索引或索引失效创建合适的联合索引遵循最左前缀原则索引失效索引列用函数 / 运算、隐式类型转换、like ‘% xxx’避免在 where 子句中对索引列做操作使用覆盖索引大分页查询limit offset 过大改用游标分页where id last_id limit 10N1 查询循环查询关联表改用批量查询where id in (...)或关联查询大事务一个事务包含多个操作甚至 RPC 调用拆分大事务将非数据库操作移出事务锁等待长事务持有行锁导致其他请求阻塞优化事务粒度避免在事务中做耗时操作进阶优化方案读写分离读请求走从库写请求走主库分库分表单表数据量超过 1000 万时考虑水平分表缓存热点数据用 Redis 缓存高频访问数据减少数据库压力️ 第四步防复发建立长效机制上线前 SQL 审核用工具阿里 Druid、SonarQube自动检测慢 SQL禁止不合格 SQL 上线压测验证上线前做全链路压测模拟真实流量发现潜在问题完善监控告警配置慢查询告警超过 1s 自动报警监控数据库 CPU、IO、连接数等指标定期巡检每月做一次数据库性能巡检清理无用索引优化表结构 整体排查流程图面试加分项提到实际踩过的坑比如 “我之前遇到过隐式类型转换导致的索引失效varchar 字段传了 int 值导致全表扫描改成传字符串后性能提升了上百倍”提到具体工具SkyWalking 链路追踪、Druid 连接池监控、Explain 执行计划分析提到架构层面的解决方案读写分离、分库分表、多级缓存提到预防措施说明你不仅会解决问题还能从流程上避免问题再次发生 核心代码与技术亮点1. SpringBootDruid 慢 SQL 自动监控与拦截必配技术亮点无需侵入业务代码自动采集慢 SQL 并告警提前发现潜在问题ConfigurationpublicclassDruidConfig{BeanConfigurationProperties(spring.datasource.druid)publicDataSourcedruidDataSource(){DruidDataSourcedataSourcenewDruidDataSource();// 开启慢SQL统计阈值1秒dataSource.setSlowSqlMillis(1000);dataSource.setLogSlowSql(true);// 开启WallFilter拦截全表扫描、恶意SQLdataSource.setFilters(stat,wall,log4j2);returndataSource;}// 注册Druid监控面板BeanpublicServletRegistrationBeanStatViewServletstatViewServlet(){ServletRegistrationBeanStatViewServletbeannewServletRegistrationBean(newStatViewServlet(),/druid/*);bean.addInitParameter(loginUsername,admin);bean.addInitParameter(loginPassword,123456);returnbean;}// 自定义慢SQL告警器生产级BeanpublicSlowSqlListenerslowSqlListener(){returnnewSlowSqlListener(){OverridepublicvoidonSlowSql(SlowSqlEventevent){Stringsqlevent.getSql();longtimeMillisevent.getTimeMillis();// 超过3秒直接发钉钉/企业微信告警if(timeMillis3000){DingTalkAlertUtil.sendAlert(【严重慢SQL告警】\nSQL: sql\n耗时: timeMillisms\n数据源: event.getDataSourceName());}}};}}2. 大分页查询优化性能提升 1000 倍技术亮点解决limit 1000000, 10全表扫描问题支持任意深度分页// ❌ 错误写法扫描前100万行性能极差ListUserbadPage(intpageNum,intpageSize){intoffset(pageNum-1)*pageSize;returnuserMapper.selectPage(offset,pageSize);}// ✅ 正确写法游标分页基于自增IDListUsergoodPage(longlastId,intpageSize){// 只扫描pageSize行性能恒定returnuserMapper.selectByCursor(lastId,pageSize);}// 对应的SQLSelect(select id, name, phone from user where id #{lastId} order by id asc limit #{pageSize})ListUserselectByCursor(Param(lastId)longlastId,Param(pageSize)intpageSize);3. N1 查询问题根治批量查询替代循环查询技术亮点将 N 次数据库调用合并为 1 次减少网络 IO 和连接开销// ❌ 错误写法N1查询1次查订单列表N次查用户ListOrderVObadGetOrderList(){ListOrderordersorderMapper.selectAll();returnorders.stream().map(order-{OrderVOvonewOrderVO();BeanUtils.copyProperties(order,vo);// 循环查询用户N次数据库调用UseruseruserMapper.selectById(order.getUserId());vo.setUserName(user.getName());returnvo;}).collect(Collectors.toList());}// ✅ 正确写法批量查询1次查订单1次查所有用户ListOrderVOgoodGetOrderList(){ListOrderordersorderMapper.selectAll();// 提取所有用户ID批量查询SetLonguserIdsorders.stream().map(Order::getUserId).collect(Collectors.toSet());MapLong,UseruserMapuserMapper.selectByIds(userIds).stream().collect(Collectors.toMap(User::getId,Function.identity()));returnorders.stream().map(order-{OrderVOvonewOrderVO();BeanUtils.copyProperties(order,vo);vo.setUserName(userMap.get(order.getUserId()).getName());returnvo;}).collect(Collectors.toList());}4. 热点行更新优化秒杀 / 库存扣减场景技术亮点用分段锁解决单条记录行锁竞争问题QPS 提升 50 倍以上// ❌ 错误写法单条记录锁竞争激烈数据库CPU飙升TransactionalpublicbooleandeductStock(LongproductId,Integernum){intaffectedstockMapper.deductStock(productId,num);returnaffected0;}// ✅ 正确写法分段锁将1条库存记录拆分为10条TransactionalpublicbooleandeductStockWithSegment(LongproductId,Integernum){// 随机选择一个分段intsegmentThreadLocalRandom.current().nextInt(10);intaffectedstockMapper.deductStockBySegment(productId,num,segment);// 如果当前分段库存不足尝试其他分段if(affected0){for(inti0;i10;i){if(isegment)continue;affectedstockMapper.deductStockBySegment(productId,num,i);if(affected0)returntrue;}returnfalse;}returntrue;}// 对应的SQLUpdate(update stock set stock stock - #{num} where product_id #{productId} and segment #{segment} and stock #{num})intdeductStockBySegment(Param(productId)LongproductId,Param(num)Integernum,Param(segment)Integersegment); 核心技术难点与解决方案对照表技术难点问题本质典型场景解决方案排查 / 验证命令隐式类型转换导致索引失效MySQL 对索引列做隐式函数转换破坏索引有序性varchar 类型的手机号传了 int 值int 类型的 id 传了字符串1. 统一参数类型2. 避免where phone 13800138000这种写法explain select * from user where phone 13800138000;观察key列是否为 null联合索引最左前缀陷阱范围查询、、between之后的索引列失效联合索引idx_a_b_c查询where a1 and b2 and c3c 列索引失效1. 将等值查询列放在最左2. 范围查询列放在最后3. 必要时使用覆盖索引explain select * from t where a1 and b2 and c3;观察key_len列判断用到了几个索引列大事务导致的锁等待雪崩长事务持有行锁时间过长导致后续请求排队阻塞事务中包含 RPC 调用、文件 IO、循环操作1. 拆分大事务将非数据库操作移出事务2. 开启事务超时配置3. 使用Transactional(timeout 5)show engine innodb status;查看TRANSACTIONS部分的事务时长死锁排查与解决多个事务互相等待对方持有的锁形成循环依赖事务 A 先更新表 1 再更新表 2事务 B 先更新表 2 再更新表 11. 统一更新顺序2. 降低事务粒度3. 开启死锁检测show engine innodb status;查看LATEST DETECTED DEADLOCK部分MySQL 查询优化器选错索引统计信息不准确导致优化器选择了错误的索引数据分布不均匀某列值大部分相同1. 强制指定索引force index(idx_name)2. 更新统计信息analyze table t;3. 创建更合适的联合索引explain analyze select * from t where a1;对比预估行数和实际行数Using filesort/Using temporary排序或分组操作无法使用索引需要在内存 / 磁盘临时表中完成order by非索引列group by非索引列1. 为排序 / 分组列创建索引2. 使用覆盖索引避免回表3. 限制排序结果集大小explain select * from t order by create_time desc;观察Extra列是否有Using filesort 面试加分提示结合实际踩坑经历比如 “我之前在用户中心项目中遇到过隐式类型转换的问题手机号字段是 varchar 类型前端传了 int 值导致全表扫描接口耗时从 10ms 变成了 3s改成传字符串后性能直接恢复”量化优化效果所有优化都要说清楚优化前后的性能对比比如 “大分页优化后第 10000 页的查询耗时从 2.5s 降到了 1ms”提到底层原理比如解释为什么隐式类型转换会导致索引失效是因为 MySQL 会把索引列转换成参数的类型相当于对索引列做了函数操作所以无法使用索引真实现场模拟面试真实现场模拟面试面试官 “假设生产环境数据库突然告警出现大量慢SQL老板让你去排查。说说你的整体思路。”我 “好的我一般会按发现→定位→分析→优化→验证这样一个闭环来做。先用一张图概括”“接下来每一步都有具体的工具和检查点我可以展开说说。”面试官 “好那第一步快速定位到具体是哪些SQL在慢你会怎么做”我 “线上讲究无侵入快速定位我一般四板斧慢查询日志– 已经开启的话配合pt-query-digest直接看 TOP N最省事。sys 库 / performance_schema– 比如查sys.statement_analysis能按总耗时排序找出那些执行次数多、累积影响大的SQL。☁️云监控/APM– 像阿里云DAS、SkyWalking 直接给出慢SQL和调用链路能知道是哪个接口触发。紧急情况– CPU 打满时直接SHOW FULL PROCESSLIST看当前正在跑的SQL重点盯Sending data、Creating tmp table这些状态。 这一步结束手里就是一份嫌疑SQL清单包括耗时、频率、来源。”面试官 “嗯拿到一条具体SQL了接下来怎么分析它为什么慢”我 “核心就是看执行计划。MySQL的话我优先用EXPLAIN ANALYZE8.0因为它显示实际耗时和预估行数的误差。然后逐字段排查就像看化验单一样”关注字段 异常信号⚠️ 实际意味着什么typeALL全表扫描基本是索引缺失/失效keyNULL完全没用到索引rows巨大扫描行数太多估算可能不准ExtraUsing filesort / Using temporary文件排序或临时表耗内存、磁盘filtered很小索引选择性差大量数据回表后被丢弃“同时我还会SHOW CREATE TABLE看一眼表结构和现有索引确认是不是有索引但没用上比如发生了隐式类型转换。”面试官 “分析完执行计划一般能归纳出哪几类根因”我 “根据经验90%的慢SQL都跑不出这六大根因索引缺失/失效榜首没建索引、索引列上用函数、类型转换、违反最左前缀。数据量暴涨表变大后回表开销大或优化器统计信息不准选错索引。锁等待SQL本身不慢被其他事务阻塞SHOW ENGINE INNODB STATUS能看出来。返回大量无用数据SELECT *拖了大字段或者深分页LIMIT 100000,20实际扫了很多行。复杂SQL写法多表JOIN 子查询 OR优化器很难生成好计划。硬件/配置瓶颈磁盘IOPS满、Buffer Pool太小导致刷脏页得结合iostat、top一起看。”面试官 “根因找到了那你会立刻怎么优化”我 “优化分紧急止血和长远根治。我常用的‘三板斧’1. 索引优化建覆盖索引让查询只扫索引不回表。调整联合索引列顺序贴合最左前缀。优化器选错索引时可临时FORCE INDEX止血但事后要分析统计信息为啥不准。2. ✂️ SQL改写深分页改成基于主键的游标式分页或延迟关联。OR条件改UNION ALL。用JOIN替代部分子查询。坚决去掉SELECT *只查必要字段。3. ⚙️ 架构调整表太大考虑分区、分库分表或历史数据归档。统计类查询引入ES等异构数据源避免直接在OLTP库上跑复杂分析。”面试官 “优化完了怎么验证真的有效而不是按下葫芦浮起瓢”我 “验证我严格走三步预发/低峰期测试– 再次EXPLAIN甚至EXPLAIN ANALYZE确认执行计划符合预期实际耗时下降。线上监控对比– 观察慢查询数量、CPU使用率、接口RT确保问题真正消失。闭环复盘– 把案例沉淀到知识库团队内同步同时在代码审查时加强对索引变更的关注有条件的接入SQL自动审核或慢SQL熔断工具。如果效果不理想我会重新回到执行计划分析那一步调整方案。”面试官 “不错。最后用一句话总结你的排查思路。”我 “监控先发现 → 工具抓SQL → EXPLAIN看计划 → 结合场景挖根因 → 建索引/改写法/调架构 → 验证闭环。永远先观测再动手线上最怕凭直觉改代码。”面试官 “思路很清晰。刚才你提到深分页优化、索引失效分析这些能不能贴一段核心代码示例展示一下你的具体实现最好有技术亮点。”我 “没问题我挑两个最有代表性的场景。1. 深分页优化延迟关联写法这是典型的大表LIMIT 100000,20慢查询传统做法数据库实际扫描了100020行。我用延迟关联改写-- ❌ 原始慢SQL扫描大量行SELECTid,title,content,create_timeFROMarticlesWHEREstatus1ORDERBYcreate_timeDESCLIMIT100000,20;-- ✅ 优化后先取主键再回表关联SELECTa.id,a.title,a.content,a.create_timeFROMarticles aINNERJOIN(SELECTidFROMarticlesWHEREstatus1ORDERBYcreate_timeDESCLIMIT100000,20)AStmpONa.idtmp.id;亮点子查询只扫索引覆盖列id 索引列避免了大量回表再由主键直接关联取出最终数据扫描行数从10万降为20行左右。2. 快速诊断索引失效检查隐式类型转换线上有一个SQL突然从毫秒变秒级执行计划显示typeALL。我怀疑是隐式类型转换导致索引失效用这个查询验证-- 假设 phone 字段是 varchar但传入的是数字EXPLAINSELECT*FROMusersWHEREphone13800138000;-- key: NULL, type: ALL 索引失效-- 验证转换发生SELECTCOLUMN_NAME,DATA_TYPE,COLUMN_TYPEFROMINFORMATION_SCHEMA.COLUMNSWHERETABLE_SCHEMAmydbANDTABLE_NAMEusersANDCOLUMN_NAMEphone;-- 结果varchar(20) 而传参是数字MySQL会将字段转为数字比较导致全表扫描️修复改成phone 13800138000或代码层统一参数类型执行计划立刻恢复。3. 用 sys 库揪出 TOP 累积慢查询当监控曲线抖动不是单个SQL特别慢而是高频SQL累积效应我会用这条语句快速抓凶-- 按总延迟排序找出最 “费时” 的 SQLSELECTquery,db,exec_count,total_latency,avg_latency,rows_sent_avgFROMsys.statement_analysisORDERBYtotal_latencyDESCLIMIT10; 这条能看到哪些SQL虽然单次不慢但执行次数极高拖垮整体性能是优先治理的对象。”面试官 “代码很扎实。那站在技术面试角度你觉得在这个线上慢SQL排查场景里最大的技术难点有哪些你是怎么解决它们的”我 “这个场景确实有几个特别棘手的地方我总结为三大难点难点具体描述✅ 我的解决方案 难点1生产环境无法随意操作不能跑EXPLAIN ANALYZE这种耗时诊断更不能加索引验证稍不慎会加剧故障① 优先使用慢日志 pt-query-digest 离线分析零侵入② 在从库或预发环境回放慢SQL安全地做 EXPLAIN 和压测③ 利用performance_schema的内存表采集不锁表。 难点2索引失效原因隐蔽隐式类型转换、字符集不一致、函数包装、统计信息不准等仅看执行计划不够① 对比SHOW CREATE TABLE和实际SQL参数类型计算 key_len判断是否用到完整索引② 打开optimizer_trace看优化器决策过程③ 定期ANALYZE TABLE更新统计信息防止选错索引。⏳ 难点3优化效果需要量化验证且不能回归优化一个慢SQL可能影响其他查询或者线上数据分布不同导致预期不符① 在影子表或预发环境用真实数据量压测对比EXPLAIN ANALYZE实际耗时② 使用数据库审计插件或APM记录优化前后SQL响应时间形成对比报告③ 灰度上线观察监控曲线确保CPU/慢查询数量下降且无新异常。这三大难点背后体现的是线上操作的敬畏心和量化驱动的优化方法论恰好是一线工程师区别于‘只会写SQL’的核心竞争力。”面试官 “很好既有落地代码又有抽象思考。这个问题我们就聊到这儿。”【别走交个朋友】我是 Rain 一个喜欢把复杂技术讲透、让代码落地的实践者。「Rain 的 Java 大神之路」全网同名在这里我会把每一次技术复盘、每一个项目的设计源码毫无保留地分享给你。微信有合集天天更新。技术这条路很酷我们结伴同行。Keep Coding, Keep Loving —— 与所有 Java 同路人共勉。