如何“醫治”一條慢SQL?
前言
"蘇工,訂單列表又崩了!"
接到電話時,我對著監控大屏上999ms的SQL響應時間哭笑不得。
幾年來,我發現一個定律:所有SQL問題都是在凌晨三點爆發!
今天抽絲剝繭,教你用架構師的思維給慢SQL開刀手術。
希望對你會有所幫助。
1.術前檢查:找準病灶
1.1 EXPLAIN 查看執行計劃
使用EXPLAIN查看SQL語句的執行計劃,相當于給SQL拍了張X光。
下面是一個典型的SQL問題,它是某電商平臺歷史訂單查詢的SQL語句:
SELECT *
FROM orders o
LEFTJOINusers u ON o.user_id = u.id
LEFTJOIN 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)
ORDERBY o.amount DESC
LIMIT1000,20;
使用EXPLAIN關鍵字查看執行計劃的結果如下:
+----+-------------+-------+------+---------------+------+---------+------+---------+---------------------------------+
| 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字段索引失效
錯誤用法:
ALTER TABLE users ADD INDEX idx_extend ((extend_info->'$.is_vip'));
extend_info字段是JSON類型的字段,即使創建了索引,索引也會丟失。
正解姿勢(MySQL 8.0+):
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順序是:
orders → users → products
優化后的方案:
(子查詢過濾users) → products → orders
調整執行順序,用小表驅動大表。
重寫后的SQL:
SELECT o.*
FROM products p
INNERJOIN (
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
INNERJOIN (
SELECTid
FROMusers
WHERE vip_level > 3
) u ON o.user_id = u.id
WHERE p.category_id IN (5,8)
ORDERBY o.amount DESC
LIMIT1000,20;
術后效果:
- 先掃小表(users過濾后只有100條)
- 消除冗余字段傳輸
- 減少Join時臨時表生成
2.3 開顱手術
通過執行計劃鎖定了問題,走錯索引了,該怎么處理呢?
可以通過FORCE INDEX強制指定索引:
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';
使用衍生表加速:
SELECT *
FROM (
SELECTid, amount
FROM orders
WHERE create_time > '2023-01-01'
ORDERBY amount DESC
LIMIT1020
) tmp
ORDERBY amount DESC
LIMIT1000,20;
醫囑:
- 警惕OR導致的索引失效
- 用覆蓋索引避免回表查詢
- CTE表達式謹慎使用
2.4 生命體征監測
查看索引使用:
SHOW INDEX FROM orders;
監控索引使用率:
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危險寫法:
@Select("SELECT * FROM orders WHERE #{condition}")
List<Order> findByCondition(@Param("condition") String condition);
condition參數可以傳入任何內容,如何傳入了1=1,可能會導致查詢所有的數據,走全表掃描,讓查詢效率變得非常低。
正確做法(參數化查詢):
@Select("SELECT * FROM orders WHERE create_time > #{time}")
List<Order> findByTime(@Param("time") Date time);
消毒方案:
- SQL審核平臺接入(如Yearning)
- MyBatis攔截器攔截全表更新
- 自動化EXPLAIN分析流水線
3.2 查殺大表癌癥
如果遇到大表的癌癥病例,可以用分庫分表的方案解決。
病歷案例:3億訂單表終極解決方案
// 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歸檔歷史數據)
醫囑總結
優化三板斧:
- 定位:慢查詢日志+執行計劃分析
- 切割:化繁為簡拆分多步執行
- 重建:符合業務場景的數據結構
避坑口訣:
- 索引不是銀彈,覆蓋才是王道
- Join水深,能拆就拆
- Order By+Limit≠分頁優化
最后送上蘇三的傳秘方:當你優化SQL到懷疑人生時,不妨試試這三味藥:
- 刪業務邏輯
- 加緩存
- 換數據庫
保證藥到病除(老板打不打死你我就不管了,哈哈哈)!