作为数据库性能调优中最常见的问题之一,慢SQL优化是每个开发者必须掌握的硬核技能。我经历过无数次凌晨被报警叫醒处理生产环境慢查询的经历,也见证过一条SQL从15秒优化到15毫秒的蜕变过程。本文将基于10个真实案例,拆解慢SQL优化的完整方法论。
生产环境中,我们通常通过以下方式捕获慢SQL:
关键诊断命令示例:
sql复制-- 查看慢查询日志配置
SHOW VARIABLES LIKE '%slow_query%';
-- 临时开启慢查询日志(生产环境慎用)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 设置1秒阈值
理解执行计划是优化的第一步,重点关注以下字段:
| 字段 | 关键含义 | 优化方向 |
|---|---|---|
| type | 访问类型(最好到最差) | 争取达到const/ref/range级别 |
| key | 实际使用的索引 | 确保使用最优索引 |
| rows | 预估扫描行数 | 减少扫描量 |
| Extra | 额外信息 | 消除"Using filesort"等警告 |
原始SQL:
sql复制SELECT * FROM orders
WHERE user_id = 10086
AND status = 'completed'
ORDER BY create_time DESC;
问题诊断:
优化方案:
sql复制ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
优化效果:
原始SQL:
sql复制SELECT product_name FROM products
WHERE product_name LIKE '%手机%';
问题诊断:
优化方案:
sql复制ALTER TABLE products ADD FULLTEXT INDEX ft_idx_name (product_name);
SELECT product_name FROM products
WHERE MATCH(product_name) AGAINST('手机');
原始SQL:
sql复制SELECT * FROM user_logs
ORDER BY create_time DESC
LIMIT 100000, 20;
问题诊断:
优化方案:
sql复制SELECT * FROM user_logs
WHERE id > (SELECT id FROM user_logs ORDER BY create_time DESC LIMIT 100000, 1)
ORDER BY create_time DESC
LIMIT 20;
优化原理:
原始SQL:
sql复制SELECT * FROM products
WHERE category_id = 5;
问题诊断:
优化方案:
sql复制SELECT product_id, product_name, price
FROM products
WHERE category_id = 5;
实测效果:
原始SQL:
sql复制SELECT * FROM large_table l
JOIN small_table s ON l.id = s.large_id;
问题诊断:
优化方案:
sql复制SELECT /*+ STRAIGHT_JOIN */ *
FROM small_table s
JOIN large_table l ON s.large_id = l.id;
优化要点:
原始SQL:
sql复制SELECT * FROM users
WHERE id IN (
SELECT DISTINCT user_id FROM orders
WHERE amount > 1000
);
问题诊断:
优化方案:
sql复制SELECT DISTINCT u.* FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;
性能对比:
原始场景:
优化方案:
sql复制-- 原表保留核心字段
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(255),
price DECIMAL(10,2)
);
-- 大字段单独存放
CREATE TABLE product_details (
product_id INT PRIMARY KEY,
description TEXT,
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
优化效果:
原始场景:
优化方案:
sql复制ALTER TABLE orders ADD COLUMN user_name VARCHAR(50);
-- 下单时同步更新
UPDATE orders SET user_name =
(SELECT name FROM users WHERE id = user_id)
WHERE id = NEW_ORDER_ID;
注意事项:
原始SQL:
sql复制SELECT * FROM (
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
) t WHERE order_count > 10;
问题诊断:
优化方案:
sql复制-- 方案1:使用物化视图
CREATE VIEW user_order_stats AS
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id;
-- 方案2:使用HAVING替代子查询
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
HAVING order_count > 10;
原始操作:
java复制for(Order order : orderList) {
jdbcTemplate.update("INSERT INTO orders(...) VALUES(...)");
}
问题诊断:
优化方案:
java复制// 使用批量插入
jdbcTemplate.batchUpdate("INSERT INTO orders(...) VALUES(...)",
new BatchPreparedStatementSetter() {
// 实现方法...
});
优化效果:
测量先行原则:永远不要凭直觉优化,先通过EXPLAIN和性能分析工具定位瓶颈
索引优化优先级:
查询设计要点:
架构层面考量:
关键提示:所有优化都要在测试环境验证,避免生产环境直接修改。我曾经遇到过因为优化导致执行计划变更反而引发性能下降的情况。
建立完善的监控体系:
推荐工具组合:
sql复制-- user_id是varchar类型但传入了数字
SELECT * FROM users WHERE user_id = 12345;
解决方案:保持类型一致或使用CAST显式转换
sql复制-- 即使name和phone都有独立索引,这个查询也无法有效使用
SELECT * FROM users WHERE name = '张三' OR phone = '13800138000';
优化方案:改用UNION ALL
sql复制SELECT * FROM users WHERE name = '张三'
UNION ALL
SELECT * FROM users WHERE phone = '13800138000';
sql复制-- DATE_FORMAT导致create_time索引失效
SELECT * FROM orders
WHERE DATE_FORMAT(create_time, '%Y-%m') = '2023-01';
优化方案:
sql复制SELECT * FROM orders
WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31';
经过多年实战,我发现慢SQL优化没有银弹,需要结合具体业务场景和数据特征进行分析。建议建立SQL评审机制,在开发阶段就预防性能问题。当面对特别复杂的查询时,不妨考虑拆分成多个简单查询,有时应用程序中组合处理反而比一条复杂SQL更高效。