ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SQL优化实战:从执行计划到索引设计,让查询效率提升数十倍

SQL优化实战:从执行计划到索引设计,让查询效率提升数十倍 1. 从“能跑”到“跑得快”SQL优化的核心思路接触SQL优化这些年我最大的感触是绝大多数人写SQL的起点是“能查出结果就行”至于查得够不够快、资源吃得多不多、并发扛不扛得住往往是等到线上报警才想起来回头补课。这个项目标题里“从功能性到高效查询”这半句话其实点破了SQL优化的本质——先把语句写对再把语句写好。先说功能性。一个查询能返回正确结果这只是及格线。很多初级开发者踩过的坑像多表关联时忘了关联条件导致笛卡尔积、子查询里排序失效、用OR连接不同索引列导致索引失效这些问题在数据量小的时候根本看不出来等生产环境表里几百万行数据一压过来慢得让你怀疑是不是数据库挂了。再说高效查询。从功能性到高效中间隔着三层东西执行计划的理解、索引的设计、SQL改写能力。这三层不是孤立的而是层层递进的关系。你只有读得懂数据库是怎么跑你这条SQL的才知道该在哪里加索引只有理解了索引的底层结构才知道为什么有些写法能走索引、有些写法会让索引失效只有掌握了SQL改写技巧才能在不改变业务结果的前提下把一条跑了三秒的语句优化到五十毫秒以内。这个内容适合谁来参考主要是两类人。一类是刚接触数据库不久、写SQL靠百度拼凑的初中级开发者需要建立起“优化意识”和一套可复用的排查方法另一类是已经有一定经验、但遇到慢查询还是靠瞎猜加索引的从业者需要系统性地梳理一遍执行计划、慢查询日志这些基本功。说白了这篇文章写的是我这些年踩坑踩出来的经验没有什么玄学全是实打实的操作思路。下面我按照实际工作中处理慢SQL的顺序把从发现问题到解决问题的完整链路拆开讲一遍。2. 先定位问题慢查询日志和执行计划是两把尺子很多人在优化SQL时犯的第一个错误就是不知道瓶颈到底出在哪上来就瞎改。改完之后也不知道到底有没有效果靠感觉办事。这就好比发烧了不看体温计、不验血直接抓一把药往嘴里塞——碰巧吃对了算运气吃不对就耽误事。2.1 打开慢查询日志把“犯罪嫌疑人”揪出来优化的第一步永远是找到那些执行时间超过阈值的SQL语句。MySQL里有一个非常实用的工具叫慢查询日志Slow Query Log它会记录所有执行时间超过设定阈值的SQL语句。我以最常见的MySQL 8.0为例实际操作步骤如下-- 查看当前慢查询日志是否开启 SHOW VARIABLES LIKE slow_query_log; -- 查看慢查询日志的阈值默认10秒 SHOW VARIABLES LIKE long_query_time; -- 动态开启慢查询日志重启后失效如需永久生效请改my.cnf SET GLOBAL slow_query_log ON; -- 把阈值调低一些比如1秒方便抓更多有优化空间的语句 SET GLOBAL long_query_time 1; -- 查看慢查询日志文件位置 SHOW VARIABLES LIKE slow_query_log_file;这里我习惯把long_query_time设为1秒或者更低。生产环境如果硬件不错0.5秒都比较合理。默认的10秒阈值太宽松了等你抓到一条10秒的SQL时业务早就被打爆了用户体验已经不可逆地受损。慢查询日志文件里记录的信息包括SQL文本、执行时间、锁等待时间、扫描的行数等。但直接用文本编辑器去看日志很痛苦尤其是慢SQL多的时候。我一般配合mysqldumpslow工具来汇总分析mysqldumpslow -s at -t 10 /var/lib/mysql/slow-query.log这个命令的意思是按照平均查询时间at排序显示前10条最慢的SQL。它能自动把SQL语句里的具体值替换成N和S把结构相同的SQL聚合成一条有效过滤掉那些只是参数值不同的重复语句。拿到慢SQL列表后先别急着优化每一条。我的做法是先看总执行次数和总耗时优先处理“累计耗时最高”的几条。一条单次执行2秒但一天只跑三五次的SQL优先级远低于一条单次500毫秒但每分钟跑几百次的SQL。前者优化后节省的数据库资源极其有限后者优化后可能直接让数据库CPU降下来一半。2.2 用EXPLAIN读懂执行计划看到数据库的真实想法找到慢SQL之后下一步就是分析它为什么慢。这时候就得祭出EXPLAIN这个神器。你可以把它理解成体检报告它能告诉你这条SQL在数据库内部是怎么执行的——先查哪张表、用了哪个索引、扫描了多少行、有没有临时表、有没有文件排序。EXPLAIN SELECT u.username, o.order_no, o.amount FROM user u LEFT JOIN orders o ON u.id o.user_id WHERE u.created_at 2025-01-01 ORDER BY o.created_at DESC;执行完之后你会看到一长串字段。我重点看以下几个type列这是访问类型也是判断性能好坏的第一指标。从好到差大致是systemconsteq_refrefrangeindexALL。如果看到ALL就是全表扫描大数据量下这是性能杀手基本可以确定需要加索引看到index也需要注意说明虽然扫了索引但扫的是整棵索引树比全表扫描强点有限。key列实际用到的索引是哪个。如果key为NULL说明没用到索引。这时候再结合possible_keys看看是有索引但没用上还是压根就没建合适的索引。rows列估算的扫描行数。这个数字越接近实际结果集越好。如果估算扫描行数是几十万但实际结果只有几百行说明数据筛选效率极低大概率是索引选择有问题。Extra列包含了大量关键信息。看到Using filesort说明排序操作没走索引在数据量大时会造成额外的性能损耗看到Using temporary说明用了临时表通常和GROUP BY、DISTINCT相关也需要注意看到Using index反而是好事这是覆盖索引扫描连表数据都不用回。拿到执行计划后优化方向就清晰了。一般来说如果type是ALL或者rows特别大优先考虑调整索引如果Extra里出现Using filesort和Using temporary考虑改写SQL结构或者调整索引顺序。2.3 一个值得警惕的误区EXPLAIN的rows只是估算值用EXPLAIN分析时有个很容易被忽略的点rows是一个基于统计信息的估算值不是精确值。数据库优化器会根据表的索引基数、分布情况等估算扫描行数但这个估算可能和实际值偏差很大尤其是在表数据频繁更新、统计信息没来得及刷新的情况下。所以我的建议是EXPLAIN看的是方向和趋势不是绝对数值。定位问题的时候除了看执行计划还要结合实际的响应时间和系统负载一起判断。有时候EXPLAIN看起来走索引了但实际执行还是很慢这时候就要考虑是否因为表数据量太大导致索引失效、是否因为缓冲池命中率太低、是否需要分析表更新统计信息等。-- 如果表数据变化频繁可以主动分析表更新统计信息 ANALYZE TABLE orders;这条命令不阻塞读写成本很低在怀疑优化器选错索引时可以先用它试试。3. 核心优化手段索引设计、SQL改写、执行计划调优定位到问题之后就要动手改了。这一节我按照实用频率从高到低把最常用的优化手段过一遍。要知道SQL优化的核心目标其实很简单减少数据扫描量、避免不必要的排序和临时表、让优化器做出更优的执行计划。所有技巧本质上都是围绕这三件事展开的。3.1 索引设计加索引之前先想清楚这几个问题加索引是SQL优化里性价比最高的手段往往一行CREATE INDEX就能把查询时间缩短几个数量级。但索引不是加得越多越好我见过不少开发者在所有字段上都建索引结果写操作的性能被拖垮磁盘空间也被浪费。索引设计要回答四个问题给哪个字段加、加什么类型的索引、什么时候该用联合索引、什么时候根本不该加。给哪个字段加优先考虑WHERE条件的筛选字段、JOIN的关联字段、ORDER BY的排序字段。这三个场景是索引发挥价值最明显的地方。加什么类型如果字段值区分度太低比如性别字段只有“男”“女”两个值加普通索引那也没多大意思——区分度太低的列优化器算算发现扫索引和扫全表成本差不多直接放弃索引。反过来区分度高的字段比如订单号、手机号、身份证号这种几乎一值一行的字段加索引效果立竿见影。什么时候该用联合索引当一条SQL的多个筛选条件经常一起出现时单列索引只能帮上忙但发挥不了最大效果这时候就该考虑联合索引。联合索引遵循最左前缀原则查询条件里必须包含联合索引的最左列索引才会被用到。所以建联合索引时字段顺序至关重要。一个简单的经验法则是把筛选力度最强、区分度最高的字段放在最左边把常用于排序的字段也纳入索引如果排序字段能用上就不用额外做文件排序。我举个实际场景。查询某商家的某时段订单SELECT id, order_no, amount, status FROM orders WHERE shop_id 10001 AND created_at BETWEEN 2025-01-01 AND 2025-01-31 ORDER BY created_at DESC;这里shop_id和created_at的组合查询很频繁。最佳方案是建联合索引(shop_id, created_at)这样既用到了shop_id的等值筛选又让created_at的范围筛选走索引而且ORDER BY created_at也可以在索引内部完成不需要文件排序。ALTER TABLE orders ADD INDEX idx_shop_created (shop_id, created_at);注意这里我把shop_id放在左边因为它是等值查询created_at放在右边因为它用于范围查询和排序。反过来如果建(created_at, shop_id)在筛选条件没限定created_at范围时这个索引就发挥不了作用。什么时候根本不该加小表不需要加索引。一张只有几百行的配置表全表扫描也就几毫秒加了索引反而增加了写操作的开销和存储空间的占用。另外频繁更新的字段也要谨慎加索引每次UPDATE都可能触发索引维护写多读少的业务场景尤其要想清楚。3.2 SQL改写不改业务结果只改执行路径有些慢SQL的问题不在索引缺失而在于SQL本身的写法让优化器没办法高效执行。学会SQL改写很多问题可以不用加索引就解决。尽量避免前置通配符。这个真的是老生常谈但每次都能碰到有人踩坑。LIKE %keyword%这种写法会让索引完全失效即便字段上有索引因为B树索引是按前缀匹配组织的。如果业务允许改成LIKE keyword%就能走索引。避免在索引列上做函数运算或隐式类型转换。举两个实际例子。第一个WHERE DATE(created_at) 2025-01-01这条语句对索引列created_at套了一个DATE()函数优化器没法直接用索引。正确的写法应该是WHERE created_at 2025-01-01 AND created_at 2025-01-02这样created_at的范围条件可以直接走索引。第二个WHERE phone 13800138000如果表里phone字段是VARCHAR类型这里却写成了数字数据库会做隐式类型转换同样会导致索引失效。老老实实给数字加引号写成字符串就行。用UNION ALL替代OR。当OR连接的条件涉及不同索引列时优化器往往束手无策只能对结果集做合并扫描甚至全表扫描。比如SELECT * FROM orders WHERE status CANCELLED OR pay_type WECHAT;status上有索引pay_type上也有索引但OR让优化器很难同时利用两个索引高效合并。如果这两条查询的字段之间没有大量重复可以拆成UNION ALLSELECT * FROM orders WHERE status CANCELLED UNION ALL SELECT * FROM orders WHERE pay_type WECHAT;需要注意UNION会去重有额外的排序开销UNION ALL不去重、更高效。业务上如果明确两个子查询结果不可能重复直接使用UNION ALL。避免SELECT *。这不仅仅是为了省传输流量。SELECT *会让优化器被迫去主键索引拿整行数据同时可能触发回表。只查询你真正需要的列不仅能减少回表次数还给了覆盖索引发挥的空间。覆盖索引就是“查询列全部包含在索引列中”查询时只需要扫描索引树无需回表性能提升很明显。拿这个例子来说-- 假设联合索引 (shop_id, created_at) 已存在 -- 这条SQL只查索引里的列可以走覆盖索引Extra为Using index SELECT shop_id, created_at FROM orders WHERE shop_id 10001;3.3 正确使用LIMIT和分页深分页是个隐蔽的坑分页查询翻到很后面时变慢是生产环境非常常见的性能问题。比如SELECT * FROM orders ORDER BY created_at DESC LIMIT 100000, 20;这条SQL看起来挺简单但执行时MySQL要先把前100020行全部查出来再丢掉前面的100000行只返回最后20行。前面那10万行的排序、回表成本全部白白浪费了。优化的思路有两种。第一种如果排序字段是自增主键或者时间戳且结果集增量有序可以用游标分页也叫Keyset PaginationSELECT * FROM orders WHERE id 100000 ORDER BY id DESC LIMIT 20;这种写法利用主键索引直接跳到目标位置附近翻页再深也不怕。第二种如果没法用游标就延迟关联SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders ORDER BY created_at DESC LIMIT 100000, 20 ) tmp ON o.id tmp.id;子查询里只查主键id可以走覆盖索引完成排序和分页拿到20个主键后再去主表回表取完整数据。这样分页越深优势越大。3.4 让GROUP BY、ORDER BY和DISTINCT别那么“重”GROUP BY和DISTINCT本质上是去重和分组操作如果处理不当会用到临时表和文件排序。优化的核心思路有两个一是让分组或排序字段走索引二是尽量减小参与分组的数据集。-- 如果经常按 state 分组统计可以考虑在 state 上建索引 SELECT state, COUNT(*) FROM user GROUP BY state;state的区分度也许不高但在数据量较大时走索引分组和全表扫描分组的性能差距依然明显。索引会让数据库按顺序读取数据相同值的记录排在相邻位置分组时可以边读边缘盘省掉临时表。另外如果ORDER BY字段不只一个尽量保证这几个字段的排序方向一致全升序或全降序因为联合索引的键值是按固定方向排列的方向不一致会导致优化器放弃索引排序。MySQL 8.0开始支持降序索引如果业务确实需要混合方向排序可以考虑建降序索引来适配。3.5 WHERE vs HAVING别把过滤放在分组后一个常见的性能杀手的写法是先用GROUP BY分组再用HAVING过滤大范围的数据。WHERE是在分组前过滤的HAVING是在分组后过滤的——作用范围完全不同性能差距巨大。-- 不推荐先按用户分组再过滤订单数大于10的用户 SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING COUNT(*) 10; -- 但这种过滤只能放在HAVING里因为COUNT(*)是分组后才有的聚合结果 -- 推荐能用WHERE前置过滤的一定在WHERE里做 SELECT user_id, COUNT(*) FROM orders WHERE status ! CANCELLED GROUP BY user_id HAVING COUNT(*) 10;第二条SQL先把取消状态的订单排除掉再对剩余数据做分组统计参与分组的数据量会显著减少。原则就是能在WHERE过滤掉的绝不留到HAVING。3.6 视图会不会加快查询速度热搜词里有人问“视图可以加快查询速度吗”这里专门说明一下。视图本质上是一段保存起来的SQL查询定义它不存储数据普通视图每次查询视图时底层还是会执行那段查询逻辑。所以视图本身不会提升性能它带来的是逻辑封装和代码复用上的便利。但有一种特殊情况——物化视图Materialized View。物化视图会把查询结果实际存储下来查询它时不需要重新执行底层逻辑。Oracle、PostgreSQL等都支持物化视图MySQL原生不支持物化视图但可以通过定时任务Event把查询结果写入汇总表来实现类似效果。不过物化视图有数据滞后的问题使用场景比较受限主要用在报表统计这种对实时性要求不高的查询上。4. 实战案例一条慢SQL从3秒到50毫秒的完整优化过程讲了这么多理论用一个真实场景把整个流程串起来。假设业务里有一条查询用户订单列表的SQL线上执行耗时约3秒接口响应极慢。4.1 原始SQL和业务背景SELECT u.username, o.order_no, o.amount, o.pay_type, o.created_at FROM user u LEFT JOIN orders o ON u.id o.user_id WHERE u.phone LIKE %138% OR o.status PAID ORDER BY o.created_at DESC LIMIT 200;业务需求是搜索手机号中包含138的用户同时显示已支付订单按订单创建时间倒序取200条。4.2 定位问题看执行计划第一步打开慢查询日志发现这条SQL每次执行都在3秒左右经常进入慢日志排行榜。第二步用EXPLAIN查看执行计划得到的关键信息如下user表type为ALL全表扫描orders表type为ALL全表扫描Extra里有Using where; Using temporary; Using filesort看到这三个Using基本就心里有数了全表扫描干了两张表还用了临时表和文件排序。这个查询从筛选到排序全都低效不改SQL光加索引是行不通的。4.3 一步步推导优化方案第一步先看OR条件。u.phone LIKE %138%是模糊搜索无法走索引o.status PAID是等值条件本身可以走索引但被OR连接后优化器没法在两个不同表的不同列上同时高效利用索引。这里我选择用UNION ALL把两个查询拆开-- 分支1手机号包含138的用户不管订单状态 SELECT u.username, o.order_no, o.amount, o.pay_type, o.created_at FROM user u LEFT JOIN orders o ON u.id o.user_id WHERE u.phone LIKE %138% UNION ALL -- 分支2已支付订单不管手机号 SELECT u.username, o.order_no, o.amount, o.pay_type, o.created_at FROM user u RIGHT JOIN orders o ON u.id o.user_id WHERE o.status PAID ORDER BY created_at DESC LIMIT 200;不过这里马上会遇到一个问题UNION ALL外面套ORDER BY ... LIMIT 200会把两个分支的结果先合并再统一排序和截断。这个最终排序操作如果数据量很大依然会是瓶颈。但没关系先给它一个合理索引再来验证。第二步分析索引设计。分支1的核心条件是u.phone LIKE %138%由于左模糊这个条件本身无法利用B树索引所以这个分支注定要全表扫user表。如果这个搜索词出现频率很高建议业务侧改成前缀搜索phone LIKE 138%就能利用phone上的普通索引来加速。如果业务强制必须包含中间值那只能接受扫描成本或者引入搜索引擎方案这不是单纯SQL层面能解决的。分支2的核心条件是o.status PAID加上排序字段o.created_at。这里建一个联合索引(status, created_at)就很合适——相等条件定位索引内排序。ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);第三步重新看LEFT JOIN和RIGHT JOIN的必要性。分支1里用户即使没有订单也需要显示用户名。但需求是“搜索手机号包含138的用户”同时返回他们的订单信息——如果用户没有订单返回一条全NULL的订单行其实没有意义。这里我把LEFT JOIN改成普通的INNER JOIN不仅不影响业务展示前端会过滤掉NULL订单行还能让优化器在更多情况下选择更高效的驱动表。第四步还有一个细节UNION ALL后排序截断。这里所有订单数据合起来可能有几十万条全部排序取出200条的成本不低。但好消息是两个分支的订单都已经按created_at倒序排列了分支2走索引排序分支1如果加一个(created_at)索引也可以做到可以退而求其次在每个分支里分别做排序截断再在合并层做一次最终排序这样参与最终排序的数据量不会太大。经过一轮调整最终SQL长这样-- 分支1手机号命中用户前缀匹配可走索引 SELECT u.username, o.order_no, o.amount, o.pay_type, o.created_at FROM user u INNER JOIN orders o ON u.id o.user_id WHERE u.phone LIKE 138% ORDER BY o.created_at DESC LIMIT 200; -- 分支2已支付订单 SELECT u.username, o.order_no, o.amount, o.pay_type, o.created_at FROM user u INNER JOIN orders o ON u.id o.user_id WHERE o.status PAID ORDER BY o.created_at DESC LIMIT 200;再把两个结果集在应用层合并、排序、截断。实际测试下来单分支查询耗时都在30毫秒以内应用层合并排序也只要几毫秒总耗时从原来的3秒降到了50毫秒左右。4.4 这个案例说明什么这个案例里的每一步其实对应了前面讲的原则减少扫描量去掉全表扫描、加索引、避免不必要的临时表和文件排序联合索引覆盖排序、SQL结构改写拆OR为UNION ALL、缩小排序数据集各分支先LIMIT再合并。没有一步是玄学全部建立在执行计划可观测的基础之上。5. 常见问题与排查技巧实录实战中踩过的坑太多了我把高频问题整理成一个速查表方便遇到问题时直接对号入座。5.1 问题速查表典型症状可能原因排查方式解决建议查询突然变慢数据量增长、统计信息过期、索引失效看慢日志EXPLAIN对比执行计划更新统计信息重建索引必要时加索引EXPLAIN显示typeALL缺索引或写法导致索引失效检查possible_keys和key加合适索引改写SQL避免函数/隐式转换Extra显示Using filesort排序字段未走索引或排序方向不一致查看ORDER BY字段是否有索引建联合索引覆盖排序字段Extra显示Using temporaryGROUP BY、DISTINCT或UNION导致分析分组字段是否可走索引加索引或改写SQL减少参与分组的数据量分页越翻越慢深分页OFFSET过大查看LIMIT后偏移量改为游标分页或延迟关联索引存在但没被使用区分度低、统计信息旧、字段有函数运算EXPLAIN看possible_keys更新统计信息改写SQL检查区分度查询只返回几行却扫描全表没有WHERE条件驱动索引看WHERE和LIMIT加条件或加索引让优化器有索引可选同一SQL快慢差异大缓存命中率波动、锁等待看执行时间和锁等待优化事务隔离级别排查锁竞争IN子查询特别慢子查询结果集大重复执行EXPLAIN查看执行方式用EXISTS改写或改为JOINLIKE左模糊慢索引失效看是否用了前缀通配符改为右模糊或引入全文索引/搜索引擎5.2 几个容易被忽略的排查场景场景一查询本来很快凌晨突然变慢。这个我遇到过好几次最终定位到是晚上跑批任务的定时任务在凌晨更新大量数据把缓冲池和磁盘IO都占满了。这种问题不是单纯SQL层面能解决的需要从任务调度、资源隔离层面协调。排查时可以看数据库的活跃会话数、锁等待信息和系统IO指标。场景二SQL单跑很快并发一上来就崩。这种情况往往是并发场景下缓存命中率下降、行锁竞争加剧、连接池排队导致的。优化方向从SQL本身转向事务粒度、隔离级别、连接池参数调整。比如一个事务里同时更新多条记录尽量按固定顺序更新避免死锁和锁等待。场景三子查询和JOIN都可行时选哪个。很多人一听到“SQL优化”就想到把子查询改成JOIN但实际上不全是这样。早期MySQL对子查询的执行计划优化不好IN (SELECT ...)经常被优化成逐行执行性能很差。但MySQL 5.6之后优化器改进了派生表合并和物化策略很多子查询也能生成和JOIN类似的执行计划。到底选哪种以EXPLAIN的执行计划为准不要凭感觉。如果子查询在EXPLAIN里显示DEPENDENT SUBQUERY说明是相关子查询每行都要执行一次这种性能一定差果断改成JOIN或重写。场景四同一条SQL联调和生产性能差异巨大。大概率是数据分布差异导致的。联调环境几万行数据全表扫描也无感生产几千万行全表扫描直接卡死。这时候唯一有效的方法就是看生产环境的EXPLAIN不要拿联调环境的执行计划做参考。5.3 防SQL注入优化之外的基本功搜索热词里出现了“sql注入”“万能密码绕过”这类词这里也顺带提一嘴。SQL优化讲的是让查询更快SQL注入讲的是让查询被恶意控制——两者都是SQL层面的安全问题和技术问题但注入的危害远比性能问题严重。SQL注入的本质是应用层直接拼接SQL字符串外部输入变成了可执行的SQL代码。经典的万能密码绕过就是利用恒真条件-- 恶意输入 OR 11 SELECT * FROM user WHERE username OR 11 AND password x;这条语句在数据库中永远会返回所有用户因为11恒为真且OR的优先级低于AND但足以绕过原本的密码校验逻辑。优化做得再好如果数据库被注入一切都没有意义了。防御手段没什么花哨的就三条第一一律使用参数化查询或预编译语句让SQL结构和数据彻底分离这是最有效的防线第二严格校验输入按业务规则限制输入格式和长度第三数据库账户遵循最小权限原则应用账号不要用root或者拥有DDL权限的高权限账号。这三条做到了大部分注入风险就堵住了。6. 实战经验与扩展方向SQL优化是一个“越做越深”的事。一开始可能只是加个索引、改写个写法到后来你会慢慢接触到执行计划成本模型、统计信息、优化器行为、事务隔离级别、数据库参数调优等更加底层的东西。每个数据库产品的优化器都不同MySQL的优化器策略和PostgreSQL、SQL Server、Oracle不完全一样但底层的原理——B树索引结构、执行计划的代价估算、扫描和排序的成本逻辑——是共通的。我在实际工作中对SQL优化的体会有三点。第一先量后优没有慢日志和监控指标之前不要动手优化否则你根本不知道自己改完有没有效果。第二一次只改一个变量很多人在一条慢SQL上同时加索引、改SQL结构、调数据库参数出了问题根本不知道是哪一步引入的。第三亲手用EXPLAIN验证每一步任何优化建议都要回到执行计划上验证不要只看语句跑完的时间时间会受缓存和系统负载干扰执行计划更稳定更可信。最后再分享一个实操中的小技巧日常开发中写完一条SQL养成随手EXPLAIN看一眼的习惯重点看type、key、rows、Extra四个字段。只要type不是ALL、key不是NULL、rows接近实际结果集、Extra里没有Using filesort和Using temporary这条SQL基本就是健康的。我以前带团队时定了一个简单的规矩所有涉及多表关联或者大表查询的需求代码评审时必须附上EXPLAIN结果截图。就这么一个小小的习惯上线后的慢SQL数量直接降了一个量级。SQL优化的学习路径很长但只要掌握“发现问题—分析执行计划—设计索引—改写SQL—验证效果”这一套闭环你就已经超过了大多数停留在“能跑就行”阶段的开发者。后续如果大家感兴趣关于执行计划的成本模型、MySQL和PostgreSQL优化器的差异、以及慢查询自动监控告警的搭建都可以展开写写。
返回列表