SQL查询性能直接影响应用的用户体验和系统资源消耗。一个优化不当的查询可能导致数据库CPU飙升、内存耗尽、甚至整个系统宕机。本文系统性地介绍SQL查询性能调优的方法论、工具和实战技巧,帮助你打造高效的数据库查询。
一、性能问题识别
1.1 慢查询的常见表现
- 页面加载时间超过3秒
- API接口响应超时
- 数据库CPU使用率持续高于80%
- 连接池耗尽,无法建立新连接
- 锁等待时间过长
1.2 慢查询日志开启
MySQL配置:
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 1; -- 超过1秒的记录
SET GLOBAL min_examined_row_limit = 100; -- 最少扫描100行才记录
-- 查看慢查询日志
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 20;
PostgreSQL配置:
-- postgresql.conf
log_min_duration_statement = 1000 -- 记录超过1秒的查询
log_statement = 'mod' -- 记录修改操作
1.3 性能监控指标
| 指标 | 健康范围 | 警告阈值 | 危险阈值 |
|---|---|---|---|
| 查询响应时间 | 100ms - 1s | > 1s | |
| 每秒查询数(QPS) | 根据业务 | 接近上限80% | 超过上限 |
| 活跃连接数 | 70% - 90% | > 90% | |
| 锁等待时间 | 10ms - 100ms | > 100ms | |
| 缓冲池命中率 | > 99% | 95% - 99% | |
| 临时表使用率 | 5% - 20% | > 20% |
二、查询分析方法
2.1 EXPLAIN深度分析
EXPLAIN是SQL性能分析的核心工具。以下是一个完整的EXPLAIN分析示例:
EXPLAIN FORMAT=JSON
SELECT
u.username,
COUNT(o.order_id) AS order_count,
SUM(o.total_amount) AS total_spent
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE u.status = 'active'
AND o.order_date >= '2024-01-01'
GROUP BY u.username
HAVING COUNT(o.order_id) > 10
ORDER BY total_spent DESC
LIMIT 50;
2.2 执行计划关键指标
| 指标 | 说明 | 优化方向 |
|---|---|---|
| type = ALL | 全表扫描 | 添加索引或优化WHERE条件 |
| rows值很大 | 扫描行数过多 | 缩小查询范围或优化索引 |
| Using filesort | 文件排序 | 使用索引排序 |
| Using temporary | 使用临时表 | 优化GROUP BY或调整查询结构 |
| Using where | 服务器端过滤 | 考虑是否能通过索引直接过滤 |
2.3 查询执行时间分析
-- 开启profiling
SET profiling = 1;
-- 执行目标查询
SELECT ... FROM ... WHERE ...;
-- 查看执行时间
SHOW PROFILES;
-- 查看详细信息
SHOW PROFILE ALL FOR QUERY 1;
三、常见性能反模式
3.1 SELECT * 的危害
-- 反模式:SELECT *
SELECT * FROM users WHERE status = 'active';
-- 问题:
-- 1. 读取不需要的列,浪费I/O
-- 2. 无法使用覆盖索引
-- 3. 表结构变更可能导致应用出错
-- 4. 增加网络传输量
-- 正确做法:只查询需要的列
SELECT user_id, username, email, created_at
FROM users
WHERE status = 'active';
3.2 不必要的子查询
-- 反模式:相关子查询(每行都会执行一次子查询)
SELECT
o.order_id,
o.total_amount,
(SELECT username FROM users WHERE user_id = o.user_id) AS username,
(SELECT COUNT(*) FROM order_items WHERE order_id = o.order_id) AS item_count
FROM orders o
WHERE o.order_date >= '2024-01-01';
-- 优化方案:使用JOIN
SELECT
o.order_id,
o.total_amount,
u.username,
COUNT(oi.item_id) AS item_count
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.order_date >= '2024-01-01'
GROUP BY o.order_id, o.total_amount, u.username;
3.3 隐式类型转换
-- 反模式:字符串列传入数字
SELECT * FROM users WHERE phone = 13800138000;
-- 导致索引失效,全表扫描
-- 正确做法:使用匹配的数据类型
SELECT * FROM users WHERE phone = '13800138000';
3.4 过度使用OR
-- 反模式:复杂的OR条件
SELECT * FROM orders
WHERE customer_name LIKE '%张%'
OR order_id IN (SELECT order_id FROM returns)
OR status = 'refunded'
OR total_amount > 10000;
-- 优化方案:拆分查询,使用UNION
SELECT * FROM orders WHERE customer_name LIKE '张%'
UNION
SELECT * FROM orders WHERE order_id IN (SELECT order_id FROM returns)
UNION
SELECT * FROM orders WHERE status = 'refunded'
UNION
SELECT * FROM orders WHERE total_amount > 10000;
3.5 大数据量OFFSET分页
-- 反模式:大OFFSET分页(越往后越慢)
SELECT * FROM articles ORDER BY id LIMIT 20 OFFSET 100000;
-- 需要扫描100020行,丢弃前100000行
-- 优化方案:游标分页(基于上一页最后一条记录)
SELECT * FROM articles
WHERE id > 100020 -- 上一页最后一条的id
ORDER BY id
LIMIT 20;
四、优化策略详解
4.1 查询重写
将NOT EXISTS改为LEFT JOIN:
-- 原始写法
SELECT *
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
-- 优化写法
SELECT c.*
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.customer_id IS NULL;
将COUNT(*)优化为近似值:
-- 精确计数(慢)
SELECT COUNT(*) FROM orders WHERE status = 'completed';
-- 近似计数(快,用于不需要精确数字的场景)
-- MySQL
SELECT TABLE_ROWS
FROM information_schema.TABLES
WHERE TABLE_NAME = 'orders';
-- 或使用缓存的计数器
SELECT cnt FROM table_counters WHERE table_name = 'orders_completed';
4.2 分页优化
-- 延迟关联优化分页
SELECT
p.id,
p.title,
p.content,
p.created_at
FROM posts p
INNER JOIN (
SELECT id
FROM posts
WHERE category_id = 5
ORDER BY created_at DESC
LIMIT 20 OFFSET 1000
) tmp ON p.id = tmp.id;
4.3 JOIN优化
| JOIN类型 | 适用场景 | 性能特点 |
|---|---|---|
| INNER JOIN | 只需要匹配行 | 性能最好 |
| LEFT JOIN | 需要左表全部行 | 性能中等 |
| RIGHT JOIN | 需要右表全部行 | 性能中等 |
| CROSS JOIN | 笛卡尔积 | 性能最差,慎用 |
| Subquery | 逻辑简单的子查询 | 优化器可能改写为JOIN |
4.4 批量操作优化
-- 反模式:逐条插入
INSERT INTO logs (message, level, created_at) VALUES ('msg1', 'INFO', NOW());
INSERT INTO logs (message, level, created_at) VALUES ('msg2', 'WARN', NOW());
INSERT INTO logs (message, level, created_at) VALUES ('msg3', 'ERROR', NOW());
-- 优化:批量插入
INSERT INTO logs (message, level, created_at) VALUES
('msg1', 'INFO', NOW()),
('msg2', 'WARN', NOW()),
('msg3', 'ERROR', NOW());
-- 大批量数据导入时临时禁用索引
ALTER TABLE large_table DISABLE KEYS;
-- 批量插入数据...
ALTER TABLE large_table ENABLE KEYS;
4.5 EXISTS vs IN 性能对比
-- 当外表小、内表大时,使用IN更好
SELECT * FROM small_table
WHERE id IN (SELECT id FROM large_table WHERE status = 'active');
-- 当外表大、内表小时,使用EXISTS更好
SELECT * FROM large_table lt
WHERE EXISTS (
SELECT 1 FROM small_table st
WHERE st.id = lt.id AND st.type = 'special'
);
五、反模式汇总
| 反模式 | 问题 | 优化方案 |
|---|---|---|
| SELECT * | 读取多余列,无法覆盖索引 | 指定需要的列 |
| 相关子查询 | 每行执行一次子查询 | 改为JOIN |
| 隐式类型转换 | 索引失效 | 使用匹配的类型 |
| 复杂OR条件 | 全表扫描 | 使用UNION |
| 大OFFSET分页 | 扫描大量数据 | 游标分页 |
| 函数操作索引列 | 索引失效 | 改写为范围查询 |
| 逐条INSERT | 频繁磁盘写入 | 批量INSERT |
| 未加LIMIT | 返回过多数据 | 添加LIMIT限制 |
六、实战调优案例
6.1 案例:电商首页推荐查询
原始查询(耗时8秒):
SELECT *
FROM products
WHERE category_id IN (
SELECT category_id FROM user_preferences WHERE user_id = ?
)
AND price BETWEEN 100 AND 5000
AND stock > 0
ORDER BY (
SELECT AVG(rating) FROM reviews WHERE product_id = products.id
) DESC
LIMIT 20;
优化后(耗时0.2秒):
SELECT
p.id,
p.name,
p.price,
p.stock,
COALESCE(rs.avg_rating, 0) AS avg_rating
FROM products p
INNER JOIN user_preferences up
ON p.category_id = up.category_id
LEFT JOIN (
SELECT product_id, AVG(rating) AS avg_rating
FROM reviews
GROUP BY product_id
) rs ON p.id = rs.product_id
WHERE up.user_id = ?
AND p.price BETWEEN 100 AND 5000
AND p.stock > 0
AND p.is_active = 1
ORDER BY rs.avg_rating DESC
LIMIT 20;
七、总结
SQL性能调优是一个系统性工程,需要结合查询分析、索引优化、架构设计等多方面手段。关键步骤:
- 识别慢查询:通过慢查询日志和监控工具发现问题
- 分析执行计划:使用EXPLAIN理解查询的执行方式
- 消除反模式:避免SELECT *、相关子查询等常见问题
- 合理使用索引:根据查询模式创建和维护索引
- 持续监控优化:性能优化是一个持续的过程
记住:没有银弹,每种优化方案都需要在实际场景中验证效果。