如何医治一条慢SQL?

笔记哥 / 05-13 / 36点赞 / 0评论 / 998阅读
## 1 术前检查:找准病灶 ### 1.1 EXPLAIN 查看执行计划 使用EXPLAIN查看SQL语句的执行计划,相当于给SQL拍了张X光。 下面是一个典型的SQL问题,它是某电商平台历史订单查询的SQL语句: ```sql SELECT * FROM orders o LEFT JOIN users u ON o.user_id = u.id LEFT JOIN products p ON o.product_id = p.id WHERE o.create_time > '2023-01-01' AND u.vip_level > 3 AND p.category_id IN (5,8) ORDER BY o.amount DESC LIMIT 1000,20; ``` 使用EXPLAIN关键字查看执行计划的结果如下: ```sql +----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+ | id | select_type | table | type | possible_keys | key | rows | Extra| key_len | +----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+ | 1 | SIMPLE | o | ALL | idx_user_time | NULL | 1987400 | Using where; Using filesort | | 1 | SIMPLE | u | ALL | PRIMARY | NULL | 100000 | Using where | | 1 | SIMPLE | p | ALL | PRIMARY | NULL | 50000 | Using where | +----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+ ``` **诊断报告**: - 全表扫描三连击(type=ALL) - filesort暴力排序(内存警告) - 索引全军覆没 ## 2 手术方案:精准打击 ### 2.1 单表代谢手术 如果通过执行计划查到是索引有问题,我们就需要单独优化索引。 **病根**:JSON字段索引失效 错误用法: ```sql ALTER TABLE users ADD INDEX idx_extend ((extend_info->'$.is_vip')); ``` extend\_info字段是JSON类型的字段,即使创建了索引,索引也会丢失。 正解姿势(MySQL 8.0+): ```sql ALTER TABLE users ADD INDEX idx_vip_level (vip_level); ALTER TABLE orders ADD INDEX idx_create_user (create_time, user_id) COMMENT '组合索引覆盖查询'; ``` 创建组合索引覆盖查询。 ### 2.2 血管疏通术 **卡点分析**: 原始join顺序是: ```sql orders → users → products ``` 优化后的方案: ```sql (子查询过滤users) → products → orders ``` 调整执行顺序,用小表驱动大表。 重写后的SQL: ```sql SELECT o.* FROM products p INNER JOIN ( SELECT o.id, o.amount, o.create_time FROM orders o WHERE o.create_time > '2023-01-01' ) o ON p.id = o.product_id INNER JOIN ( SELECT id FROM users WHERE vip_level > 3 ) u ON o.user_id = u.id WHERE p.category_id IN (5,8) ORDER BY o.amount DESC LIMIT 1000,20; ``` **术后效果**: - 先扫小表(users过滤后只有100条) - 消除冗余字段传输 - 减少Join时临时表生成 ### 2.3 开颅手术 通过执行计划锁定了问题,走错索引了,该怎么处理呢? 可以通过FORCE INDEX强制指定索引: ```sql SELECT /*+ INDEX(o idx_create_user) */ o.id, o.amount FROM orders o FORCE INDEX (idx_create_user) WHERE o.create_time > '2023-01-01'; ``` 使用衍生表加速: ```sql SELECT * FROM ( SELECT id, amount FROM orders WHERE create_time > '2023-01-01' ORDER BY amount DESC LIMIT 1020 ) tmp ORDER BY amount DESC LIMIT 1000,20; ``` **医嘱**: - 警惕OR导致的索引失效 - 用覆盖索引避免回表查询 - CTE表达式谨慎使用 ### 2.4 生命体征监测 查看索引使用: ```sql SHOW INDEX FROM orders; ``` 监控索引使用率: ```csharp SELECT object_schema, object_name, index_name, count_read, count_fetch FROM performance_schema.table_io_waits_summary_by_index_usage WHERE index_name IS NOT NULL; ``` ## 3 术后护理:体系化治理 ### 3.1 SQL消毒中心 需要制定优秀的代码规范,否则可能会出现全表扫描的问题。 在日常工作中,我们要尽可能减少**Java代码感染源**。 MyBatis危险写法: ```java @Select("SELECT * FROM orders WHERE #{condition}") List findByCondition(@Param("condition") String condition); ``` condition参数可以传入任何内容,如何传入了1=1,可能会导致查询所有的数据,走全表扫描,让查询效率变得非常低。 正确做法(参数化查询): ```csharp @Select("SELECT * FROM orders WHERE create_time > #{time}") List findByTime(@Param("time") Date time); ``` **消毒方案**: 1. SQL审核平台接入(如Yearning) 2. MyBatis拦截器拦截全表更新 3. 自动化EXPLAIN分析流水线 ### 3.2 查杀大表癌症 如果遇到大表的癌症病例,可以用分库分表的方案解决。 **病历案例**:3亿订单表终极解决方案 ```yml // Sharding-JDBC分片配置 spring.shardingsphere.rules.sharding.tables.orders.actual-data-nodes=ds$0..1.orders_$->{2020..2023} spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-column=create_time spring.shardingsphere.rules.sharding.tables.orders.table-strategy.standard.sharding-algorithm-name=time_range ``` **化疗方案**: - 时间维度分片(2020~2023年度表) - 用户ID取模分库 - 冷热分离(OSS归档历史数据) ## 医嘱总结 **优化三板斧**: 1. 定位:慢查询日志+执行计划分析 2. 切割:化繁为简拆分多步执行 3. 重建:符合业务场景的数据结构 **避坑口诀**: - 索引不是银弹,覆盖才是王道 - Join水深,能拆就拆 - Order By+Limit≠分页优化 当你优化SQL到怀疑人生时,不妨试试这三味药: 1. 删业务逻辑 2. 加缓存 3. 换数据库