|
上周帮同事看一条查询,优用三张表关联,化常跑了47秒。种方我看了一眼执行计划,优用驱动表选错了,化常中间结果集被放大到几百万行。种方加了个复合索引,优用调整了连接顺序,化常降到0.3秒。种方这种事我每个月都会遇到几次,优用SQL优化的化常收益有时候就是这么夸张。 下面这些方法,种方是优用我这些年踩坑踩出来的,按实际使用频率排列,化常不按教科书目录。种方 1. 先看执行计划,别猜 EXPLAIN是起点。MySQL用EXPLAIN,PostgreSQL用EXPLAIN ANALYZE,Oracle看AUTOTRACE。重点盯三样:type列(有没有全表扫描)、rows列(预估行数是否离谱)、Extra列(有没有Using filesort、Using temporary)。很多人跳过这步直接改SQL,等于蒙着眼睛修车。 2. 索引不是越多越好 见过一张订单表建了12个索引,写入性能惨不忍睹。每个索引都要占空间、维护B+树。原则是:高频查询的WHERE、ORDER BY、JOIN字段才建,低区分度的列(比如性别)单独建索引没意义。 3. 复合索引讲顺序 (a,b,c)和(c,b,a)是两回事。最左前缀原则大家都知道,但实际写SQL时经常忘。WHERE b=1 AND c=2用不上(a,b,c)索引,因为跳过了a。范围查询后面的字段也失效——WHERE a=1 AND b>2 AND c=3,c用不到索引。 4. 别在索引列上做运算 WHERE DATE(create_time) = '2024-01-01' 这种写法直接废掉索引。改成 WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'。函数、类型转换、表达式,只要包在索引列外面,索引就失效。 5. 用JOIN代替子查询 相关子查询在旧版MySQL里会逐行执行,性能灾难。能改写成JOIN就改。不过PostgreSQL和MySQL 8.0对子查询优化好了很多,具体看执行计划再决定。 6. 小表驱动大表 JOIN的时候让小结果集做驱动表。IN和EXISTS的选择也是这个逻辑:外层小用IN,内层小用EXISTS。听起来简单,但优化器有时候会选错,必要时用STRAIGHT_JOIN强制指定顺序。 7. 分页查询别用大OFFSET LIMIT 1000000, 20 会扫描前100万行再丢掉。改成游标方式:WHERE id > 上次最大id LIMIT 20。或者用延迟关联,先在索引上拿到主键再回表。 8. 只查需要的列 SELECT * 的问题不只是网络传输。如果查询能走覆盖索引,只取必要列就能避免回表,性能差好几倍。我见过有人把SELECT * 改成具体列,查询从2秒降到50毫秒。 9. 批量操作代替循环单条 循环里跑INSERT,每次一个事务,一万条数据能跑几分钟。改成批量INSERT,每批500到1000条,几秒钟搞定。UPDATE和DELETE同理。 10. 合理使用临时表 复杂查询拆成几步,中间结果放临时表,比一条巨型SQL好维护也好优化。尤其是多个大表JOIN再聚合的场景,临时表反而更快。 11. 注意隐式类型转换 user_id是varchar,你写WHERE user_id = 12345,MySQL会做隐式转换,索引直接失效。这种坑很隐蔽,执行计划里看type变成ALL就要警惕。 12. OR改UNION WHERE a=1 OR b=2,如果a和b都有索引,优化器可能不走索引。改成两个查询UNION ALL,各自走索引再合并,通常更快。 13. 分区表处理大表 单表过亿行,加索引也救不了全表扫描。按时间或范围分区,查询时能裁剪掉无关分区。不过分区键选不好反而添乱,要结合查询模式来定。 14. 统计信息要更新 优化器靠统计信息选执行计划。数据变化大了统计信息过期,执行计划就跑偏。MySQL的ANALYZE TABLE,PostgreSQL的VACUUM ANALYZE,定期跑一下。 15. 读写分离和缓存 有些查询优化到头也就那样了,不如加一层Redis缓存,或者把读请求分流到从库。这不是SQL层面的优化,但解决实际问题时经常是最有效的手段。 最后说一句:别背规则,看执行计划。同一条SQL在不同数据量、不同版本下表现可能完全相反。我见过把索引删掉反而变快的案例——因为优化器选了错误的索引。工具是死的,数据是活的。 |