文章目录
SQL 调优是数据库性能优化的核心环节,对于 MySQL 数据库尤其重要。本文将系统性地介绍 MySQL SQL 调优的各个方面,包括调优原则、工具使用、索引优化、查询优化、执行计划分析等,并提供大量实用示例和最佳实践。
一、SQL 调优基础与原则
1.1 SQL 调优的目标
- 提高查询响应速度:减少单个查询的执行时间
- 提高系统吞吐量:增加单位时间内处理的查询数量
- 降低资源消耗:减少 CPU、内存、I/O 等资源使用
- 提升系统稳定性:避免因低效 SQL 导致的系统波动
1.2 SQL 调优的基本原则
- 测量优先原则:不测量不优化,使用数据驱动决策
- 二八法则:优先优化最耗资源的 20% SQL
- 整体性原则:考虑整个系统而非单个查询
- 渐进式优化:每次只做一个变更并测量效果
- 平衡原则:在查询速度与资源消耗间取得平衡
二、性能分析工具
2.1 慢查询日志
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 设置慢查询阈值为1秒
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
-- 查看慢查询日志配置
SHOW VARIABLES LIKE '%slow_query%';
SHOW VARIABLES LIKE '%long_query_time%';
2.2 EXPLAIN 执行计划分析
EXPLAIN SELECT * FROM users WHERE age > 30 ORDER BY create_time DESC LIMIT 10;
EXPLAIN 输出关键列解读:
列名 | 说明 |
---|---|
id | 查询标识符,相同 id 表示同一执行单元 |
select_type | 查询类型(SIMPLE, PRIMARY, SUBQUERY, DERIVED 等) |
table | 访问的表名 |
partitions | 匹配的分区 |
type | 访问类型(从好到差: system > const > eq_ref > ref > range > index > ALL) |
possible_keys | 可能使用的索引 |
key | 实际使用的索引 |
key_len | 使用的索引长度 |
ref | 与索引比较的列 |
rows | 预估需要检查的行数 |
filtered | 条件过滤后剩余行的百分比 |
Extra | 额外信息(Using index, Using temporary, Using filesort 等) |
2.3 Performance Schema
-- 查看最耗资源的SQL
SELECT digest_text, count_star, avg_timer_wait/1000000000 as avg_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY avg_timer_wait DESC LIMIT 10;
2.4 SHOW PROFILE
-- 启用 profiling
SET profiling = 1;
-- 执行查询
SELECT * FROM orders WHERE customer_id = 1000;
-- 查看profile
SHOW PROFILE;
SHOW PROFILE FOR QUERY 1;
三、索引优化策略
3.1 索引设计原则
- 选择性原则:选择区分度高的列建索引(区分度 = 不重复值数量/总行数)
- 最左前缀原则:复合索引按最左列顺序匹配
- 覆盖索引原则:尽量让查询可以通过索引完成
- 适度原则:避免过度索引,索引也占用空间并影响写入性能
3.2 常见索引优化场景
3.2.1 单列索引优化
-- 优化前
SELECT * FROM users WHERE phone = '13800138000';
-- 优化后(添加索引)
ALTER TABLE users ADD INDEX idx_phone (phone);
3.2.2 复合索引优化
-- 查询1: WHERE a = ? AND b = ?
-- 查询2: WHERE a = ? ORDER BY b
-- 查询3: WHERE a = ? AND b > ?
-- 最佳索引
ALTER TABLE table_name ADD INDEX idx_a_b (a, b);
3.2.3 排序优化
-- 优化前(需要filesort)
SELECT * FROM orders WHERE user_id = 100 ORDER BY create_time DESC;
-- 优化后(避免filesort)
ALTER TABLE orders ADD INDEX idx_user_create (user_id, create_time);
3.2.4 分组优化
-- 优化前(需要临时表)
SELECT department_id, COUNT(*) FROM employees GROUP BY department_id;
-- 优化后
ALTER TABLE employees ADD INDEX idx_department (department_id);
3.3 索引失效的常见场景
-
索引列使用函数或计算
-- 索引失效 SELECT * FROM users WHERE DATE(create_time) = '2023-01-01'; -- 优化后 SELECT * FROM users WHERE create_time >= '2023-01-01' AND create_time < '2023-01-02';
-
隐式类型转换
-- 索引失效(phone是varchar类型) SELECT * FROM users WHERE phone = 13800138000; -- 优化后 SELECT * FROM users WHERE phone = '13800138000';
-
使用不等于(!= 或 <>)
-- 索引可能失效 SELECT * FROM users WHERE status != 1; -- 优化方案:考虑重写查询或使用其他条件
-
LIKE 以通配符开头
-- 索引失效 SELECT * FROM users WHERE name LIKE '%张%'; -- 部分优化(可以使用索引) SELECT * FROM users WHERE name LIKE '张%';
-
OR 条件使用不当
-- 索引可能失效 SELECT * FROM users WHERE age = 20 OR age = 30; -- 优化后 SELECT * FROM users WHERE age IN (20, 30);
四、SQL 查询优化技巧
4.1 SELECT 优化
-
只查询需要的列
-- 不推荐 SELECT * FROM users WHERE age > 30; -- 推荐 SELECT id, name, age FROM users WHERE age > 30;
-
使用 LIMIT 限制结果集
-- 优化分页查询 SELECT * FROM orders ORDER BY create_time DESC LIMIT 10000, 20; -- 优化方案1:使用索引覆盖 SELECT * FROM orders WHERE id >= (SELECT id FROM orders ORDER BY create_time DESC LIMIT 10000, 1) ORDER BY create_time DESC LIMIT 20; -- 优化方案2:记录上次查询的最大ID SELECT * FROM orders WHERE id > last_max_id ORDER BY create_time DESC LIMIT 20;
4.2 JOIN 优化
-
确保 JOIN 字段有索引
-- 优化前 SELECT * FROM orders o JOIN users u ON o.user_id = u.id; -- 优化后(确保user_id和id都有索引) ALTER TABLE orders ADD INDEX idx_user (user_id); ALTER TABLE users ADD PRIMARY KEY (id);
-
小表驱动大表原则
-- 优化前(大表驱动小表) SELECT * FROM large_table l JOIN small_table s ON l.id = s.large_id; -- 优化后(小表驱动大表) SELECT * FROM small_table s JOIN large_table l ON s.large_id = l.id;
-
避免多表 JOIN 导致笛卡尔积
-- 危险查询(可能导致结果集爆炸) SELECT * FROM table1, table2, table3 WHERE ...; -- 优化方案:明确指定JOIN条件 SELECT * FROM table1 JOIN table2 ON table1.id = table2.t1_id JOIN table3 ON table2.id = table3.t2_id;
4.3 子查询优化
-
用 JOIN 替代子查询
-- 优化前 SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000); -- 优化后 SELECT DISTINCT u.* FROM users u JOIN orders o ON u.id = o.user_id WHERE o.amount > 1000;
-
EXISTS 替代 IN
-- 优化前 SELECT * FROM products WHERE id IN (SELECT product_id FROM order_items WHERE quantity > 10); -- 优化后 SELECT * FROM products p WHERE EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = p.id AND oi.quantity > 10 );
4.4 排序与分组优化
-
避免不必要的排序
-- 优化前(UNION默认会去重排序) SELECT * FROM table1 WHERE condition1 UNION SELECT * FROM table2 WHERE condition2; -- 优化后(使用UNION ALL避免排序) SELECT * FROM table1 WHERE condition1 UNION ALL SELECT * FROM table2 WHERE condition2;
-
使用索引优化 GROUP BY
-- 优化前(需要临时表) SELECT department_id, COUNT(*) FROM employees GROUP BY department_id; -- 优化后(使用索引) ALTER TABLE employees ADD INDEX idx_department (department_id);
五、高级调优技术
5.1 分区表优化
-- 创建范围分区表
CREATE TABLE sales (
id INT AUTO_INCREMENT,
sale_date DATE,
amount DECIMAL(10,2),
PRIMARY KEY (id, sale_date)
) PARTITION BY RANGE (YEAR(sale_date)) (
PARTITION p2020 VALUES LESS THAN (2021),
PARTITION p2021 VALUES LESS THAN (2022),
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- 查询特定分区数据
SELECT * FROM sales PARTITION (p2022);
5.2 使用索引提示
-- 强制使用特定索引
SELECT * FROM users USE INDEX (idx_age) WHERE age > 30;
-- 忽略索引
SELECT * FROM users IGNORE INDEX (idx_name) WHERE name LIKE '张%';
-- 强制索引连接顺序
SELECT * FROM table1 FORCE INDEX (PRIMARY)
JOIN table2 FORCE INDEX (idx_table1_id) ON table1.id = table2.table1_id;
5.3 优化器提示
-- 设置优化器提示
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM large_table;
-- 指定JOIN顺序
SELECT /*+ JOIN_ORDER(t1, t2, t3) */ * FROM t1 JOIN t2 JOIN t3;
-- 使用MRR(Multi-Range Read)
SELECT /*+ MRR(users) */ * FROM users WHERE id BETWEEN 1000 AND 2000;
5.4 临时表与内存表
-- 使用内存临时表
SET tmp_table_size = 256*1024*1024;
SET max_heap_table_size = 256*1024*1024;
-- 创建内存表
CREATE TEMPORARY TABLE temp_stats (
id INT,
cnt INT,
PRIMARY KEY (id)
) ENGINE=MEMORY;
六、MySQL 配置调优
6.1 关键参数配置
# InnoDB缓冲池大小(通常设为物理内存的50-70%)
innodb_buffer_pool_size = 4G
# 日志文件大小
innodb_log_file_size = 256M
innodb_log_files_in_group = 2
# 连接相关
max_connections = 200
thread_cache_size = 10
# 查询缓存(MySQL 8.0已移除)
# query_cache_size = 0
# 排序缓冲区
sort_buffer_size = 4M
join_buffer_size = 4M
6.2 监控与调整
-- 查看InnoDB状态
SHOW ENGINE INNODB STATUS;
-- 查看缓冲池使用情况
SELECT * FROM sys.innodb_buffer_stats_by_table;
-- 查看线程缓存命中率
SHOW STATUS LIKE 'Threads_created';
SHOW STATUS LIKE 'Connections';
-- 计算命中率 = 1 - (Threads_created / Connections)
七、SQL 调优实战案例
7.1 案例一:分页查询优化
问题查询:
SELECT * FROM orders ORDER BY create_time DESC LIMIT 100000, 20;
优化方案:
-- 方案1:使用索引覆盖
SELECT * FROM orders
WHERE id >= (SELECT id FROM orders ORDER BY create_time DESC LIMIT 100000, 1)
ORDER BY create_time DESC LIMIT 20;
-- 方案2:记录上次查询的最大ID
SELECT * FROM orders
WHERE create_time < last_min_create_time
ORDER BY create_time DESC LIMIT 20;
7.2 案例二:大数据量统计优化
问题查询:
SELECT COUNT(*) FROM user_logs WHERE create_date BETWEEN '2023-01-01' AND '2023-01-31';
优化方案:
-- 方案1:使用近似值(对于MyISAM)
EXPLAIN SELECT COUNT(*) FROM user_logs;
-- 方案2:使用汇总表
CREATE TABLE stats_daily (
stat_date DATE PRIMARY KEY,
log_count INT
);
-- 方案3:使用索引覆盖
ALTER TABLE user_logs ADD INDEX idx_date (create_date);
SELECT COUNT(*) FROM user_logs USE INDEX (idx_date)
WHERE create_date BETWEEN '2023-01-01' AND '2023-01-31';
7.3 案例三:复杂多表 JOIN 优化
问题查询:
SELECT u.name, o.order_no, p.product_name, SUM(oi.quantity)
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE u.register_time > '2023-01-01'
GROUP BY u.name, o.order_no, p.product_name;
优化方案:
-- 1. 确保所有JOIN字段有索引
ALTER TABLE orders ADD INDEX idx_user (user_id);
ALTER TABLE order_items ADD INDEX idx_order (order_id);
ALTER TABLE order_items ADD INDEX idx_product (product_id);
-- 2. 使用覆盖索引
ALTER TABLE users ADD INDEX idx_register (register_time, id, name);
-- 3. 考虑使用物化视图或预计算
CREATE TABLE user_order_stats AS
SELECT u.id AS user_id, o.id AS order_id, p.id AS product_id,
u.name AS user_name, o.order_no, p.product_name, SUM(oi.quantity) AS total_quantity
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE u.register_time > '2023-01-01'
GROUP BY u.id, o.id, p.id, u.name, o.order_no, p.product_name;
-- 然后查询物化视图
SELECT user_name, order_no, product_name, total_quantity
FROM user_order_stats;
八、SQL 调优检查清单
-
索引检查
- 查询是否使用了适当的索引?
- 复合索引的列顺序是否正确?
- 是否有冗余或未使用的索引?
-
查询结构检查
- 是否查询了不必要的列?
- 是否可以重写子查询为 JOIN?
- 是否可以使用 EXISTS 替代 IN?
-
JOIN 操作检查
- JOIN 条件是否有索引?
- 是否是小表驱动大表?
- 是否可以添加 JOIN 提示?
-
排序和分组检查
- 是否可以使用索引避免 filesort?
- GROUP BY 是否可以优化?
- 是否可以去掉不必要的 DISTINCT?
-
配置检查
- 缓冲池大小是否合适?
- 临时表设置是否合理?
- 是否有必要调整优化器参数?
九、总结
MySQL SQL 调优是一个系统工程,需要结合索引优化、查询重写、执行计划分析和服务器配置调整等多个方面。有效的调优应该:
- 基于准确的性能测量和分析
- 遵循从大到小的优化顺序(先优化最耗资源的查询)
- 每次只做一个变更并验证效果
- 建立性能基准和监控机制
记住,没有放之四海而皆准的优化方案,每个系统都需要根据其数据特性和业务需求进行定制化调优。持续监控、定期优化是保持数据库高性能的关键。