「 简记往来」第十七篇:慢查询优化实录——从600ms到80ms
一、一个具体的慢查询数据量到30万条时我发现一个查询变慢了——按账本聚合所有联系人的收送礼统计同时还要按净额排序。这个查询的SQL如下SELECTcontact_id,SUM(CASEWHENtypereceiveTHENamountELSE0END)astotal_receive,SUM(CASEWHENtypesendTHENamountELSE0END)astotal_send,SUM(CASEWHENtypereceiveTHENamountELSE-amountEND)asnetFROMrecordsWHEREbook_id?GROUPBYcontact_idHAVINGnet!0这个查询是礼账本首页的核心查询——它要展示所有联系人的收礼、送礼和净额。数据量到30万条时这个查询的执行时间达到了600ms。二、定位问题使用慢查询日志MongoDB提供了慢查询日志功能// 开启慢查询日志记录超过200ms的查询db.setProfilingLevel(1,{slowms:200})查看慢查询日志后发现主要问题全表扫描虽然book_id有索引但GROUP BY contact_id需要扫描该账本下的所有记录内存排序ORDER BY net需要在内存中排序数据量大时耗时长索引不匹配现有索引(book_id, contact_id)和(book_id, date)都不完全匹配这个查询三、分析执行计划使用explain()查看执行计划db.records.find({book_id:book_xxx}).explain(executionStats)关键发现stage: COLLSCAN—— 全表扫描totalDocsExamined: 300000—— 扫描了30万条文档executionTimeMillis: 600—— 执行时间600ms这说明现有索引没有覆盖这个查询。四、优化方案增加复合索引增加(book_id, contact_id, type)复合索引db.records.createIndex({book_id:1,contact_id:1,type:1})这个索引的设计逻辑book_id在最前面快速过滤到特定账本contact_id在第二位支持GROUP BY contact_idtype在第三位支持CASE WHEN type receive的条件过滤复合索引的字段顺序遵循“最左前缀”原则。查询时索引从最左边开始匹配。{ book_id: 1, contact_id: 1, type: 1 }可以支持WHERE book_id ?匹配最左字段WHERE book_id ? AND contact_id ?匹配前两个字段WHERE book_id ? AND contact_id ? AND type ?匹配全部三个字段五、优化效果指标优化前优化后扫描方式全表扫描索引扫描扫描文档数300,000约800执行时间600ms80ms六、经验教训不要等慢了再优化数据量到30万条才发现慢回头补索引花了一周索引要覆盖查询分析查询模式后设计索引而不是随便建几个复合索引的顺序很重要最常用的过滤条件放前面监控慢查询定期检查慢查询日志提前发现性能问题七、总结这个慢查询优化的完整过程发现问题查询变慢用户反馈卡顿开启慢查询日志定位到具体查询分析执行计划发现全表扫描设计索引增加复合索引验证效果查询时间从600ms降到80ms索引是数据库性能的“第一道防线”。下一篇我们来聊聊云服务器部署——从购买到上线的完整流程。评论区聊聊你遇到过最棘手的慢查询是什么怎么解决的