一、为什么需要SQL调优
在应用开发中,SQL性能直接影响系统响应速度:
慢SQL的影响:
- 页面加载缓慢,用户体验差
- 数据库CPU使用率飙升
- 连接池耗尽,应用不可用
- 甚至引发连锁故障
调优的目标:
- 查询时间从秒级降到毫秒级
- 减少数据库资源消耗
- 提升系统吞吐量
二、执行计划分析
1. EXPLAIN使用
sql
-- 基本分析
EXPLAIN SELECT * FROM orders WHERE user_id = 1001;
-- 详细分析(MySQL 8.0+)
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 1001;
返回字段说明:
| 字段 | 说明 |
|---|---|
| id | 查询编号 |
| select_type | 查询类型 |
| table | 涉及的表 |
| partitions | 涉及的分区 |
| type | 访问类型(重要) |
| possible_keys | 可用的索引 |
| key | 实际使用的索引 |
| key_len | 索引长度 |
| ref | 索引列的引用 |
| rows | 预计扫描行数(重要) |
| filtered | 过滤比例 |
| Extra | 额外信息(重要) |
2. 访问类型
| type值 | 说明 | 性能 |
|---|---|---|
| ALL | 全表扫描 | 最差 |
| index | 索引全扫描 | 较差 |
| range | 索引范围扫描 | 一般 |
| ref | 索引等值查询 | 较好 |
| eq_ref | 唯一索引查询 | 较好 |
| const | 常量查询 | 最好 |
3. Extra信息
| 信息 | 说明 |
|---|---|
| Using filesort | 需要额外排序 |
| Using temporary | 使用临时表 |
| Using index | 覆盖索引 |
| Using index condition | 索引下推 |
| Using where | 使用WHERE过滤 |
三、索引优化
1. 索引设计原则
1. 区分度高的列放在前面
2. 复合索引遵循最左前缀原则
3. 不要在索引列上做函数运算
4. 尽量使用覆盖索引
5. 避免索引失效
2. 索引示例
sql
-- 用户表索引设计
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
phone VARCHAR(20),
email VARCHAR(100),
status TINYINT,
create_time TIMESTAMP,
-- 手机号查询(高频率)
INDEX idx_phone (phone),
-- 邮箱查询
INDEX idx_email (email),
-- 复合索引:状态+创建时间(按状态筛选后按时间排序)
INDEX idx_status_time (status, create_time),
-- 复合索引:查询某个状态的最新用户
INDEX idx_status_create (status, create_time DESC)
);
-- 订单表索引设计
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32),
user_id BIGINT,
shop_id BIGINT,
order_status TINYINT,
order_amount DECIMAL(12,2),
order_time TIMESTAMP,
pay_time TIMESTAMP,
-- 订单号唯一索引
UNIQUE INDEX uk_order_no (order_no),
-- 用户订单列表(最常用)
INDEX idx_user_time (user_id, order_time DESC),
-- 商家订单列表
INDEX idx_shop_time (shop_id, order_time DESC),
-- 状态查询
INDEX idx_status (order_status),
-- 复合索引:商家+状态+时间
INDEX idx_shop_status_time (shop_id, order_status, order_time DESC)
);
3. 索引失效场景
sql
-- ❌ 索引列做运算
SELECT * FROM users WHERE YEAR(create_time) = 2024;
-- ✅ 正确写法
SELECT * FROM users WHERE create_time >= '2024-01-01'
AND create_time < '2025-01-01';
-- ❌ 使用函数
SELECT * FROM users WHERE LOWER(phone) = '13800138000';
-- ✅ 正确写法
SELECT * FROM users WHERE phone = '13800138000';
-- ❌ 类型转换
SELECT * FROM orders WHERE order_no = 12345;
-- ✅ 正确写法
SELECT * FROM orders WHERE order_no = '12345';
-- ❌ 前缀模糊查询
SELECT * FROM users WHERE phone LIKE '%138';
-- ✅ 正确写法(后缀模糊查询仍可以用索引)
SELECT * FROM users WHERE phone LIKE '138%';
四、SQL优化技巧
1. 避免SELECT *
sql
-- ❌ 查询所有列
SELECT * FROM orders WHERE order_id = 1;
-- ✅ 只查询需要的列
SELECT order_id, order_no, order_amount
FROM orders WHERE order_id = 1;
2. 批量操作
sql
-- ❌ 循环插入
INSERT INTO orders (order_no) VALUES ('A001');
INSERT INTO orders (order_no) VALUES ('A002');
-- ✅ 批量插入
INSERT INTO orders (order_no) VALUES ('A001'), ('A002'), ('A003');
3. 避免深度分页
sql
-- ❌ 深度分页
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- ✅ 方式1:游标分页
SELECT * FROM orders
WHERE id > 1000000
ORDER BY id LIMIT 10;
-- ✅ 方式2:子查询
SELECT * FROM (
SELECT id FROM orders ORDER BY id LIMIT 1000000, 10
) t
JOIN orders ON t.id = orders.id;
-- ✅ 方式3:记录总数(先查ID)
SELECT * FROM orders
WHERE id IN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10);
4. 预计算
sql
-- ❌ 每次统计
SELECT COUNT(*) FROM orders
WHERE order_date = '2024-01-15';
-- ✅ 预计算表
CREATE TABLE daily_order_stats (
stat_date DATE PRIMARY KEY,
order_count INT,
order_amount DECIMAL(14,2)
);
-- 定时更新统计数据
INSERT INTO daily_order_stats
SELECT order_date, COUNT(*), SUM(order_amount)
FROM orders
WHERE order_date = '2024-01-14'
GROUP BY order_date;
五、慢查询诊断
1. 开启慢查询日志
sql
-- 查看配置
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time%';
-- 开启慢查询
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 1秒
-- 查看慢查询
SHOW GLOBAL STATUS LIKE 'Slow_queries';
2. 分析慢查询
sql
-- 查看最近的慢查询
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;
-- 使用mysqldumpslow分析
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
3. 诊断脚本
sql
-- 查看最慢的查询
SELECT
query,
count(*) as executions,
avg(sec) as avg_sec,
max(sec) as max_sec,
sum(sec) as total_sec
FROM (
SELECT SUBSTRING(SQL_TEXT, 1, 100) as query,
TIME_TO_SEC(EXEC_TIME) as sec
FROM mysql.slow_query_log
) t
GROUP BY query
ORDER BY total_sec DESC
LIMIT 10;
六、实战案例
案例1:订单列表优化
原始SQL:
sql
SELECT * FROM orders
WHERE user_id = 1001
ORDER BY create_time DESC
LIMIT 0, 20;
分析结果:
- type: ALL(全表扫描)
- rows: 1000000(扫描100万行)
- Extra: Using filesort(需要排序)
优化方案:
sql
-- 添加复合索引
ALTER TABLE orders ADD INDEX idx_user_time (user_id, create_time DESC);
优化后:
- type: ref(索引查询)
- rows: 20(只扫描20行)
- Extra: Using index condition
案例2:统计查询优化
原始SQL:
sql
SELECT
DATE(order_time) as date,
COUNT(*) as order_count,
SUM(order_amount) as total_amount
FROM orders
WHERE order_time >= '2024-01-01'
GROUP BY DATE(order_time);
问题: 在GROUP BY上使用函数,导致索引失效
优化方案:
sql
-- 方案1:避免函数
ALTER TABLE orders ADD INDEX idx_order_time (order_time);
-- 方案2:预计算表
CREATE TABLE daily_stats (
stat_date DATE PRIMARY KEY,
order_count INT,
order_amount DECIMAL(14,2)
);
-- 定时任务每天0点计算前一天数据
INSERT INTO daily_stats
SELECT
DATE(order_time) as stat_date,
COUNT(*) as order_count,
SUM(order_amount) as order_amount
FROM orders
WHERE order_time >= NOW() - INTERVAL 1 DAY
GROUP BY DATE(order_time);
-- 查询预计算表
SELECT * FROM daily_stats WHERE stat_date >= '2024-01-01';
七、总结
SQL调优是提升数据库性能的核心:
- 执行计划:分析查询如何执行
- 索引优化:创建合适的索引
- SQL重构:避免性能陷阱
- 慢查询监控:及时发现问题
最佳实践:
- 优先使用索引,避免全表扫描
- 避免在索引列上使用函数
- 用EXPLAIN分析每条慢SQL
- 定期维护索引(重建、删除冗余)
个人观点,仅供参考