凌晨2点,生产环境告警。查询耗时从200毫秒飙到2秒,CPU打满。罪魁祸首?我们刚加的那个"优化索引"。
这事听起来像段子,但确实是上周发生的真事。团队花了4小时才定位问题,最后发现错得离谱又合理——我们太相信直觉,忘了数据库优化第一条规则:EXPLAIN ANALYZE(执行计划分析)不是可选项,是必选项。
复合索引的顺序,比想象中更挑剔
问题出在一个看似标准的复合索引。表里有用户ID、状态、创建时间三个字段,查询条件是WHERE user_id = X AND status = Y ORDER BY created_at DESC。
直觉告诉我们:把过滤条件放前面,排序放最后。于是建了(user_id, status, created_at)。
但PostgreSQL的执行计划显示,它根本没走这个索引。为什么?
因为查询里status是可选参数——有时候传,有时候不传。当status没传时,索引前缀只剩user_id,后面跟着status和created_at的排序完全用不上。数据库被迫做全表扫描,或者选另一个更差的索引。
复合索引的顺序不是按"直觉重要性"排,而是按"查询条件的确定性"排。最常被用于等值查询、且几乎不会跳过的字段,才配站最前面。
我们最后把索引改成(user_id, created_at, status),配合一个部分索引处理特定状态,问题才解决。同一个查询,从2秒回到180毫秒。
覆盖索引的陷阱:省一次回表,可能赔十次写性能
另一个坑是"覆盖索引"(Covering Index,即INCLUDE索引)的滥用。
原理很简单:索引里带上查询需要的所有字段,数据库就不用回表查主数据了。读性能确实提升,但代价在写入端。
我们的表每小时新增50万行,更新频率也不低。每个INCLUDE字段都要在每次写入时维护,WAL(预写日志)膨胀、IO压力、锁竞争全来了。加索引前没算这笔账,结果读查询快了30%,写入慢了400%,整体吞吐量反而下降。
覆盖索引不是免费午餐,是读性能和写性能的交易。高频写入场景下,这个杠杆可能反向操作。
最后我们拆成了两个索引:一个轻量级的用于高频查询,另一个带INCLUDE的只在特定报表查询时启用。用部分索引(Partial Index)控制适用范围,而不是让索引扛着全表数据跑。
EXPLAIN和EXPLAIN ANALYZE,差了一个生产环境
最讽刺的是,我们一开始看过执行计划。EXPLAIN显示新索引被使用了,成本估算也很低。
但EXPLAIN只是基于统计信息的"猜测",EXPLAIN ANALYZE才是实际跑出来的真相。我们的表刚做了大批量导入,统计信息还没更新,优化器以为数据分布是A,实际是B。
这4小时里,前3小时都在跟"为什么优化器不选这个索引"较劲。直到有人跑了ANALYZE更新统计信息,再用EXPLAIN ANALYZE对比,才发现索引根本没被选中——之前的"选中"是过时统计信息制造的幻觉。
ANALYZE(统计信息更新)和EXPLAIN ANALYZE(实际执行分析),这两个命令在加索引后必须成套出现。少一个,你看到的可能是平行世界的执行计划。
索引不是越多越好,是越"被用到"越好
事后复盘,pg_stat_user_indexes(索引使用统计视图)给了我们一记耳光。那个"优化索引"在故障前的24小时内,被使用的次数是零。而另一个我们以为"可能过时"的老索引,承载了90%的查询。
PostgreSQL的优化器有自己的想法。它权衡的不仅是索引是否"理论上适用",还有缓存命中率、并发锁情况、数据相关性等一系列因素。有时候两个索引都能用,它偏偏选那个你觉得"次优"的——因为它的成本模型算出来,那个确实更便宜。
我们现在的流程:加索引前用pg_qualstats(查询条件统计)找真实的热点;加之后用EXPLAIN ANALYZE验证;上线后用pg_stat_user_indexes监控实际使用情况。30天零使用的索引,自动清理。
那个让查询慢10倍的索引?我们没删,把它改成了部分索引,只覆盖特定时间段的数据。现在它每天被用到2000次,但影响的写入开销降低了70%。
数据库优化没有银弹,只有假设、验证、再假设。你最近一次加索引后,真的看过EXPLAIN ANALYZE的Actual Time(实际耗时)吗?
热门跟贴