免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

慢SQL优化实战:从执行计划到索引设计的性能排查指南

慢SQL优化实战:从执行计划到索引设计的性能排查指南 做数据库性能排查这些年我对慢SQL的态度早就从看到了顺手改一改变成了必须当成事故来对待。原因很简单一条慢SQL的破坏力远远超过它表面上那几秒的耗时。它可能只有一行代码却能在高峰期占满数据库连接池、拖慢主从同步甚至让整条业务链路在几十秒内雪崩——这种场景我见过太多次了。慢SQL优化本质上就是和数据库偷懒又不聪明的执行策略做斗争数据库并不知道哪条路最快它只会机械地按成本估算去扫描、去排序、去建临时表而你需要的是学会看懂它的执行计划然后在最关键的位置干预它。这篇文章不是教科书式的数据库理论堆砌而是我在MySQL、PostgreSQL以及日常业务SQL优化中沉淀下来的一套可以复用的思路从哪里发现慢SQL、怎么读懂执行计划、如何通过索引和SQL改写把性能拉回来、并行SQL优化该怎么做、以及慢SQL的长期治理机制。无论你是后端开发、专职DBA还是正在为接口越来越慢发愁的初学者希望这篇内容能帮你少走一些弯路至少下次再遇到慢SQL的时候不再两眼一黑无从下手。1. 慢SQL是怎么拖垮业务的先认清问题本质1.1 慢SQL的毒性不止是慢本身慢SQL字面意思就是响应时间超过某个阈值的SQL。阈值怎么定在MySQL里默认是10秒但生产环境一般建议设成1秒很多高并发的团队甚至把long_query_time直接调到500毫秒。为什么门槛要压这么低因为数据库不是孤立地处理你这一条查询它要同时服务成百上千个请求。一条SQL如果跑了3秒它占据的不只是那3秒的时间还有这期间一直握住不放的连接、锁、内存和磁盘IO资源。这些资源一占后面排队的SQL全都要等。我遇到过一个特别典型的雪崩链路某个后台聚合查询原本只跑几十毫秒结果数据量翻了几倍之后单次耗时涨到了5秒。一开始没人重视直到业务高峰时这个查询被并发调用5秒内就把连接池里的连接全部占满了。后续所有正常的读写请求都在队列里干等页面超时重启脚本介入主从切换故障面越滚越大。事后复盘就是一条SQL打崩一个库的完整剧本。除了连接池问题慢SQL还有两个容易被忽略的连锁反应锁等待放大更新类慢SQL会长时间持有行锁或表锁导致其他事务一直卡在Lock wait timeout上看起来像是系统死机其实是锁被一条慢事务拖住了。主从延迟慢查询如果在主库上执行产生的binlog量很大从库在回放时同样会慢直接影响读写分离场景下读数据的实时性报表和用户端看到的数据会明显滞后。1.2 哪些SQL最容易长成慢SQL根据我接触过的业务场景以下四类SQL是慢SQL的重灾区大家可以对照排查场景典型SQL形态为什么容易慢深分页查询LIMIT 100000, 20数据库要先把前10万行全部扫描出来再丢弃扫描成本极高大表聚合统计大表上的COUNT、SUM、GROUP BY全表扫描或大范围扫描CPU和IO消耗巨大多表关联三张以上表JOIN且过滤条件少驱动表选错时会产生大量无效的中间结果索引失效查询对索引列用了函数、隐式类型转换优化器无法走索引只能退化为全表扫描我在实践中发现很多慢SQL并不是一开始就慢的。表只有几十万行时就算全表扫描也就几百毫秒没人会在意等数据涨到几千万行同一个SQL直接就变成几秒甚至几十秒。慢SQL本质上是数据量增长和执行效率低下两个因素叠加的结果。所以在定位问题时千万别只看SQL本身写得对不对还要结合数据规模和历史增长趋势一起分析。2. 如何系统性地发现慢SQL日志、监控与告警要做慢SQL优化第一步不是改SQL而是先把发现这条路铺好。很多团队都是靠用户反馈或线上事故才知道有慢SQL存在这太被动了。系统化的发现手段主要有三层建议全部用上。2.1 慢查询日志最基础也最容易被漏掉的一层MySQL的慢查询日志是成本最低的切入点但很多开发同学甚至没确认过它是不是开着的。在my.cnf里可以这样配置slow_query_log 1 slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1 log_queries_not_using_indexes 1其中long_query_time是判定阈值单位为秒生产环境建议从1秒起步。log_queries_not_using_indexes这个参数我建议一定打开它会记录所有没走索引的查询哪怕执行时间只有几十毫秒——这类SQL往往是潜在的慢SQL等到数据量涨起来就麻烦了。如果不想重启数据库也可以动态开启SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_queries_not_using_indexes ON;拿到慢查询日志之后面对几万条记录别急着逐条看先用工具做聚合。MySQL自带的mysqldumpslow够用我一般这样取Top 10mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log如果想要更细的分析用Percona Toolkit里的pt-query-digest它能按查询指纹聚合直接告诉你哪些SQL是最热门的慢SQL、总耗时占比有多高pt-query-digest /var/log/mysql/mysql-slow.log使用慢查询日志时有一个很容易踩的坑如果日志量太大它会反过来拖累数据库的IO性能尤其是log_queries_not_using_indexes开启后一些烂SQL产生的日志量非常惊人。解决方法有两个一是把日志输出到独立的磁盘二是定期轮转清理不要让慢查询日志无限增长。2.2 监控平台把慢SQL从日志变成指标慢查询日志是事后的记录更主动的做法是把慢SQL变成实时的监控指标。我自己比较习惯用Prometheus加mysqld_exporter来采集MySQL状态然后通过Grafana配置告警。需要重点盯的指标有这几个Slow_queries从MySQL启动以来的慢查询累计值。它的增量突增往往意味着某个索引失效或某段代码上线后引入了问题SQL。Threads_running当前正在执行的线程数。这个值一旦持续超过50甚至上百基本可以断定有慢SQL在堆积连接池随时可能被耗尽。Queries_per_second整体QPS。慢SQL占大量IO时QPS会出现断崖式下跌。告警规则建议这样设连续3个采集周期Threads_running 50或者Slow_queries的5分钟增量超过基线值的3倍就触发告警。阈值可以结合自己业务的峰值来调整但是原则是宁可误报不可漏报。2.3 应用层线索超时日志、调用链和用户反馈数据库层的监控不是万能的。有些慢SQL不会在慢查询日志里出现因为瓶颈不在数据库单条语句的执行时间而是应用层发起了大量SQL请求比如N1查询。这个时候应用监控的价值就体现出来了。我接手过不少线上问题最终都是在APM系统里通过调用链找到根因的。一个接口如果从200ms变成3秒点开链路能看到耗时大头落在哪一条SQL上如果SQL累计调用了几百次每次只花10ms那慢的根源就是循环查询而不是某一条SQL本身。这里有一个容易被忽视的信号用户反馈往往比监控系统来得更快。尤其是面向内部员工的后台系统大家用得不爽会直接在群里喊。遇到这种反馈不要只想着安抚第一件事是去把相关接口的数据库调用和慢查询日志拉出来同步排查。监控做得再好也替代不了真实的业务感知。3. 拿到一条慢SQL之后如何精准定位瓶颈发现慢SQL只是第一步真正考验功力的是定位瓶颈。很多同学一上来就加索引但加了发现没效果原因就是没有先读懂执行计划。这一章我讲清楚EXPLAIN怎么读、重点看哪些信号再带大家走一遍完整的实操案例。3.1 EXPLAIN输出要读什么type、rows、Extra是关键在MySQL里EXPLAIN可以查看执行计划用法很简单EXPLAIN SELECT * FROM orders WHERE user_id 123456 ORDER BY create_time DESC LIMIT 20;输出结果里有几个关键字段我单独讲一下字段含义关注点type访问类型从好到差依次是system const eq_ref ref range index ALL若为ALL则说明全表扫描大概率是慢SQLrows优化器预估需要读取的行数预估行数越大执行成本越高需要和实际数据量对照key实际使用的索引如果为NULL说明没走任何索引Extra额外的执行信息重点关注Using filesort、Using temporary、Using indextype字段特别直观。const表示通过主键或唯一索引直接命中了某一行这是最快的ref是普通索引等值匹配表现也不错range是范围查询尚可接受index是扫描了整个索引树虽然比全表扫描好一点但数据量大时也很慢最差的就是ALL全表扫描。Extra里的信息同样关键Using filesort不等于磁盘文件排序它指无法利用索引的顺序直接返回必须在排序缓冲区里额外排序。数据量小时问题不大超过排序缓冲区就会用临时文件性能骤降。Using temporary表示执行过程中需要创建临时表常见于GROUP BY和DISTINCT通常意味着优化空间很大。Using index这是好信号说明查询所需的列都在索引中不需要回表也就是覆盖索引。3.2 三个最值得关注的信号拿到一条慢SQL我看执行计划就盯三件事信号一rows扫描行数远大于实际返回行数。这是最常见的低效模式。理论上数据库只需要找到目标行就行但如果它预估要扫几百万行必然存在索引没走对或过滤条件写得有问题。信号二出现了Using temporary。临时表意味着数据在内存和磁盘之间来回腾挪尤其是GROUP BY和ORDER BY同时出现且排序列不在索引里时临时表几乎必然出现。这种SQL在数据量大时非常危险。信号三多表JOIN时驱动表选错。看EXPLAIN结果里第一行的表它决定了最外层的扫描量。如果驱动表是一个大表而小表反而被当成被驱动表整体执行成本就会暴涨。优化器偶尔会抽风这时候通过调整关联顺序或强制索引来干预效果很直接。3.3 一个真实排查案例从十几秒到几十毫秒接下来我带大家完整走一遍我优化过的一个报表查询案例让前面这些理论落到实处。场景是订单表orders当时全表3000万行左右。后台要统计某个时间范围内每个用户的订单金额SQL大概是这样的SELECT user_id, SUM(amount) AS total_amount FROM orders WHERE create_time 2024-01-01 AND create_time 2024-02-01 GROUP BY user_id ORDER BY total_amount DESC LIMIT 100;这个SQL在后台任务里跑了18秒才出结果直接超时。第一步我先用EXPLAIN看执行计划核心输出是typeALLrows30000000Extra里还有Using temporary; Using filesort。这说明优化器选择了全表扫描并且要在临时表中做分组排序慢是有道理的。第二步分析WHERE条件和查询需求。过滤条件是create_time上的范围查询但create_time没有索引于是只能全表扫GROUP BY user_id和ORDER BY total_amount都需要排序因此临时表也逃不掉。第三步我决定加一个联合索引。这里考虑了遍历条件、分组字段和查询列最终建了这样一条索引ALTER TABLE orders ADD INDEX idx_ct_uid_amount (create_time, user_id, amount);加完索引后再跑EXPLAINtype变成了range扫描行数从3000万降到了大约200万耗时从18秒降到了1.8秒左右。不过我仍然不满意。因为这个报表统计每天都要跑1.8秒虽然能接受但如果数据量再涨一倍又会变成3秒以上。于是我在索引优化之上又加了一层架构优化把聚合结果用一张日汇总表冗余出来每天定时任务跑增量统计查询的时候只查汇总表。最终上线的查询变成了这样SELECT user_id, total_amount FROM order_daily_summary WHERE stat_date 2024-01-01 ORDER BY total_amount DESC LIMIT 100;这时的执行耗时稳定在80毫秒左右。这个案例其实揭示了一条通用的优化阶梯先看执行计划能走索引就优先走索引索引解决不了的超大范围聚合就用预计算最终目标是让查询在最小数据集上完成。4. 索引设计慢SQL优化中收益最高的一块索引是解决慢SQL最高性价比的手段没有之一。很多时候一条SLOW SQL加一个索引就能从秒级降到毫秒级。但怎么加、加在哪有不少讲究。4.1 B树让查询有据可依索引为什么能让查询变快我用一个生活化的类比来解释一本几十万字的书你要找一个生僻字。没有索引时只能从第一页开始翻运气差要翻完大半本书有索引时先翻目录再翻偏旁部首然后根据页码直接定位。B树索引起的就是这个目录的作用。InnoDB里数据本身是按照主键构建聚簇索引存放的普通索引的叶子节点存的是主键值。如果查询条件命中普通索引数据库会先在索引树里找到主键然后再去聚簇索引里回表拿整行数据。这个过程叫做回表会产生额外IO所以索引设计尽量要减少回表次数最理想的情况就是覆盖索引——查询所需的字段全部都在索引里不需要回表。在实际优化中同样的查询条件有索引和没索引的扫描成本差距是数量级的。这也是为什么我在拿到慢SQL之后第一反应永远是检查它的WHERE条件、ORDER BY和GROUP BY字段有没有对应的索引。4.2 联合索引怎么建最左前缀与选择性单字段索引比较容易理解实际业务中更常见的是联合索引。联合索引遵循最左前缀原则MySQL会从联合索引的最左边字段开始逐个匹配查询条件直到遇到不匹配或范围查询为止。拿idx_a_b_c(a, b, c)举例查询条件里如果只用到b和c这个索引就无法被充分利用因为它最左边的a没有出现在条件中。所以在设计联合索引时字段顺序直接决定索引能否命中。我的设计习惯是两条经验将等值过滤条件中区分度最高的字段放最左。区分度可以用选择性来衡量选择性等于列中不同值的数量除以总行数越接近1越好。比如用户ID的选择性就比状态字段高得多WHERE user_id ? AND status ?这种场景user_id应该放在联合索引首位。把排序字段放进来。如果查询里已经固定了WHERE user_id ? ORDER BY create_time那么在user_id后面紧接create_time可以让索引天然有序避免Using filesort。一个典型的联合索引示例是idx_user_status_create_time(user_id, status, create_time)它对应WHERE user_id ? AND status ? ORDER BY create_time这类的查询模式既满足了过滤条件又让排序字段被索引覆盖回表也会减少。4.3 索引失效高发场景盘点索引加上了但查询不一定就能走索引。以下几个场景是我在实战中反复踩过的坑列出来给大家做一个避坑清单失效场景示例正确写法对索引列使用函数WHERE DATE(create_time) 2024-01-01WHERE create_time 2024-01-01 AND create_time 2024-01-02隐式类型转换WHERE phone 13800138000phone是varcharWHERE phone 13800138000LIKE前置通配符WHERE name LIKE %张%考虑全文索引或减少前缀通配符OR连接非索引列WHERE status 1 OR remark x分解为UNION或用索引覆盖OR两侧条件联合索引不满足最左前缀索引是(a,b)条件却只有b调整索引字段顺序或增加b的独立索引关于隐式类型转换我想多说一句。它是最容易踩的坑因为SQL表面上看着没毛病但MySQL会把varchar类型的列转成数字再比较一旦发生类型转换索引就失效了。这种问题用EXPLAIN一看typeALL才能发现排查起来还特别隐蔽。另外一个很多人误解的场景是IS NULL。其实在MySQL里WHERE name IS NULL在特定条件下是可以走索引的不用一竿子打死。真正的重点还是回到执行计划上任何关于有没有走索引的判断都以EXPLAIN结果为准。5. SQL改写技巧不增加任何资源也能提速索引优化并不能覆盖所有慢SQL尤其是那些写法本身就存在严重浪费的查询。改写SQL是零资源成本提速的手段也是每位后端开发都应该掌握的技能。5.1 别拿SELECT *闯天下SELECT *是慢SQL的常见隐患。它至少带来三个问题第一如果表里字段特别多查询需要回表读取所有列IO开销翻倍第二多出的字段会占用网络带宽拖慢整个接口的响应第三会让优化器无法高效利用覆盖索引因为覆盖索引必须包含查询的所有列。我一般不直接要求团队禁用SELECT *但在性能敏感的核心查询里我会建议只列出需要的字段。比如一个列表接口只需要用户ID和名称写成SELECT id, name和SELECT *的性能差距在千万级表上会非常明显。5.2 子查询、IN与EXISTS的取舍在MySQL 5.6之后的版本中优化器已经能够自动做半连接优化很多子查询其实会被优化成高效的JOIN。但在复杂的业务SQL里手动改写仍然有不可替代的价值。举个我实际优化过的例子。要找出所有已经付款的用户SELECT * FROM users WHERE id IN ( SELECT user_id FROM orders WHERE order_status PAID );如果orders表的user_id没有索引这个IN子查询会非常吃力。改写为EXISTS后逻辑上等价但对于外层users表的每一行子查询只需要判断是否存在命中了就会立即停止SELECT * FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id u.id AND o.order_status PAID );这里的关键原则是小表或驱动表的结果集应该尽量小被驱动表的关联字段必须有索引。用通俗的话说就是用小结果集驱动大结果集。MySQL优化器有时会自己调整但遇到复杂SQL手动改写能帮助优化器少走弯路。5.3 深翻页的经典解法延迟关联与Keyset分页LIMIT 100000, 20这种深分页写法非常经典也非常坑。数据库实际上会先读取前100020行再丢弃前面的100000行只返回最后的20行。扫描和排序的成本全部浪费在那些被丢弃的数据上。解决办法之一是延迟关联。先只查主键然后再回表获取完整数据SELECT o.* FROM orders o JOIN ( SELECT id FROM orders ORDER BY create_time DESC LIMIT 100000, 20 ) t ON o.id t.id;内层查询只返回主键列扫描和排序的负载会大大降低。但是当偏移量进一步增大时即使只查主键也很慢因为那些被丢弃的主键还是要被读取和排序。更彻底的解法是Keyset分页也叫基于游标的分页。它不再使用OFFSET而是通过WHERE条件直接定位到上一次查询结束的位置SELECT * FROM orders WHERE create_time 2024-05-01 10:30:00 ORDER BY create_time DESC, id DESC LIMIT 20;这个方案把跳过N行变成了按位置直接查找下一批在无限滚动、App信息流这类场景下效果奇佳唯一要注意的是查询条件里必须带上唯一排序键一般用主键来保证边界稳定。5.4 聚合统计汇总表是终极答案频繁做COUNT(*)或SUM(amount)这类聚合而且数据量又特别大的时候SQL再怎么写都很难快起来。我的一贯态度是能预计算的就不要在线计算。方案有两种实现路径。第一种是用定时任务把每一天的聚合结果存到一张汇总表里查询时直接读汇总表第二种是利用binlog或MQ在业务写入的同时异步更新汇总数据。第一种实现简单但可能有延迟适合报表系统第二种接近实时但需要投入开发维护成本。我在前面那个案例里订单金额统计从1.8秒降到80毫秒本质上就是汇总表替换聚合查询的结果。对于低频后台任务偶尔跑几次慢SQL也许可以接受但对于面向用户的在线接口聚合查询必须在数据产生时就做好预制菜。6. 并行SQL优化让多核CPU真正派上用场慢SQL优化不止是索引和改写还有一个经常被忽视的进阶方向并行SQL优化。很多开发者一听到并行就觉得是数据库自动调优的事情但实际上正确理解和控制并行查询往往能解决单核扫描无法解决的性能瓶颈。6.1 并行查询的核心原理一条SQL拆成多个工人普通SQL在执行时尤其是扫描大表和做复杂聚合时往往只有一个执行线程在那里吭哧吭哧干CPU的其他核心只能围观。并行查询做的事情就是把这个任务拆分成多个子任务让多个线程或进程同时干活最后把结果合并起来。打个比方一个仓库里有十万个箱子要盘点。一个人从头清点到尾可能需要一整天如果叫来十个人每人分一片区域可能下午就能完成。并行SQL就是这个道理只不过拆分的单位是数据块或分区。不过不是所有SQL都能并行。数据库的优化器会判断当前SQL的规模以及并行的收益是否值得。如果一个查询只用几十毫秒就能完成硬要并行反而会因为任务拆分、结果合并的开销变得更慢。并行优化的精髓在于让足够大的查询享受并行的红利同时避免小查询被过度调度。6.2 Postgres与Oracle的实现差异与实操我们先看PostgreSQL它对并行查询的支持在开源数据库里是比较成熟的。相关参数主要有这几个max_parallel_workers_per_gather单个查询最多使用的并行Worker数。max_parallel_workers整个数据库实例允许的并行Worker总数量。min_parallel_table_scan_size表大小超过这个阈值才考虑并行扫描默认是8MB。min_parallel_index_scan_size索引扫描启用并行的阈值。如果想确认一条SQL有没有真正并行执行用EXPLAIN ANALYZE查看输出的Workers Planned和Workers Launched即可。Workers Launched大于0说明确实有并行Worker参与执行。在指定某条SQL临时用并行时我不建议直接改全局配置更稳妥的做法是在事务内做局部设置BEGIN; SET LOCAL max_parallel_workers_per_gather 4; SELECT user_id, SUM(amount) FROM orders GROUP BY user_id; COMMIT;这样设置只对当前事务生效不会殃及系统里的其他小查询。Oracle的做法更加张扬直接在SQL上用Hint控制并行度SELECT /* PARALLEL(employees, 4) */ * FROM employees;Oracle的并行度是由多个从属进程协同完成的。对于数据仓库和报表类的复杂查询它的并行能力非常强但同样需要严格控制并行度防止一条大查询抢走整台机器所有CPU资源。6.3 并行SQL的正确使用姿势别把全局参数调上天我见过不少团队把max_parallel_workers_per_gather直接从默认值调到8、16期望所有慢SQL都变快结果业务高峰期CPU直接被打满普通读写查询反而变慢。原因很简单并行Worker同样需要CPU和内存整个实例的并行Worker总池是有限的如果大查询把Worker全占掉其他SQL自然没有资源可用。并行SQL的正确使用姿势我总结为三条原则说明只在特定场景开启大表扫描、大聚合、复杂多表JOIN最适合并行按SQL控制而非全局控制优先使用Hint或SET LOCAL避免影响OLTP短事务并行度跟着硬件走单次并行度建议不超过物理核心数的一半留出余量给在线业务关于MySQL这里要特别说明一下MySQL官方至今没有提供像Oracle和PostgreSQL那样成熟的并行查询执行器。虽然InnoDB具备一些并行扫描的特性比如并行读取聚簇索引但整体的并行度远不如前两者。所以如果你在MySQL上遇到超大分析型查询与其期待并行优化不如把这类查询分流到只读从库或专业的分析型数据库上执行。7. 慢SQL治理的长期机制从救火到防火慢SQL优化做得再熟练如果每次都靠线上暴露问题再补救团队永远是被动的。我越来越认同一个观点SQL性能是需要治理的而不只是优化。治理意味着建立制度让慢SQL在酿成事故之前就被拦截下来。7.1 SQL Review把慢SQL拦截在上线前先分享一个真实的教训。有一次新功能上线后当晚数据库IO直接飙升查下来是一条全表扫描的查询被高频调用。事后发现这条SQL只是在一个内部管理页面上展示数据单次执行200ms并不算太慢但接口被定时任务每分钟调用一次加上返回行数极大直接把IO打满了。从那以后我在团队里强制推行一条规则所有涉及数据库的变更上线前必须附带EXPLAIN结果确认没有全表扫描和明显的高开销操作。代码评审时不再只盯着业务逻辑而是把索引命中情况、扫描行数、是否文件排序都纳入评审清单。有条件的话可以引入SQL审核平台自动检测不规范SQL并提醒修改。MySQL中每次写完SQL都手动EXPLAIN可能有点麻烦但这个方法最可靠。建议至少把核心业务SQL、报表SQL和定时任务里的统计SQL过一遍。7.2 性能基线与巡检为每条核心SQL建立档案我在实践中的一个习惯是给核心SQL建立性能档案。每个迭代记录它的常规耗时、扫描行数、执行计划核心字段后续版本对比时就能一眼看出性能是否劣化。巡检频率可以按月进行用pt-query-digest拉最近一个月的慢查询报告对比Top SQL列表看有没有新面孔出现。如果某条SQL的耗时环比上涨超过30%就要重点排查可能是数据量增长也可能是优化器选择的执行计划变了。建立基线还有一个好处就是能够及时发现量变引起的质变。比如一个查询一个月前还只需要扫描10万行现在要扫描100万行虽然还达不到慢查询阈值但趋势已经不对了。这时候提前优化远比等到它变成真正的慢SQL再来处理要舒服得多。7.3 架构级的兜底缓存、读写分离与数据拆分索引和SQL改写能解决的问题很多但也不是万能的。当单表数据量到达几千万甚至上亿就算SQL走最优索引单次查询的IO开销依然可观。这时候就需要利用架构手段做兜底。缓存对热点数据和统计结果做缓存把数据库的高频读压力挡在缓存层。我常用的做法是Redis缓存接口级结果设置合理的过期时间并配合缓存预热。读写分离把慢查询和分析型SQL引流到从库避免拖累主库的读写性能。注意从库延迟问题读一致性要求高的场景要谨慎。数据拆分分库分表是最后的手段运维复杂度较高但只要数据量真的到了那个级别就必须面对。分区表、垂直拆分和水平拆分是三个不同层次的方案需要结合业务形态选择。对于绝大多数中小团队来说先把SQL Review和性能基线做起来再根据业务增长节奏逐步考虑架构升级就已经能避免80%以上的慢SQL事故了。最后分享一个我直到现在仍在坚持的工作习惯每条慢SQL优化完成后我都会把优化前后的执行计划、SQL耗时、改动内容记录在团队的文档里。这不仅是为了复盘更是为了让后来者能够通过历史记录快速理解为什么某条SQL长得跟直觉不一样为什么这个索引要这样建。踩过一次坑就记录下来下次就能绕过去。慢SQL优化没有太多高深莫测的理论真正拉开差距的就是这些扎实的排查习惯和持续沉淀的细节。
返回列表