SQL查询性能调优

工具相关 ·

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性能调优是一个系统性工程,需要结合查询分析、索引优化、架构设计等多方面手段。关键步骤:

  1. 识别慢查询:通过慢查询日志和监控工具发现问题
  2. 分析执行计划:使用EXPLAIN理解查询的执行方式
  3. 消除反模式:避免SELECT *、相关子查询等常见问题
  4. 合理使用索引:根据查询模式创建和维护索引
  5. 持续监控优化:性能优化是一个持续的过程

记住:没有银弹,每种优化方案都需要在实际场景中验证效果。

阅读 14