在掌握了索引原理后,我们将深入MySQL最核心的性能优化领域——查询优化与执行计划分析。本文将从优化器工作原理到实战调优技巧,全方位提升你的SQL性能优化能力。
查询优化是数据库系统的核心能力,MySQL通过优化器将SQL转换为高效执行计划。优化前后的性能差异可能达到千倍级:
-- 优化前(执行时间12.8秒)
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.reg_time > '2023-01-01'
ORDER BY o.amount DESC
LIMIT 100;
-- 优化后(执行时间23毫秒)
-- 具体优化策略见下文...
查询优化的核心目标:
减少数据扫描量(磁盘I/O最小化)
避免临时表与文件排序
充分利用索引优势
减少锁竞争与上下文切换
graph LR
A[SQL语句] --> B[解析器]
B --> C[预处理器]
C --> D[优化器]
D --> E[执行计划]
E --> F[执行引擎]
F --> G[结果返回]
优化器基于成本模型(Cost-Based Optimizer)决策
# 伪代码:优化器决策逻辑
def choose_plan(query):
possible_plans = generate_plans(query) # 生成候选执行计划
for plan in possible_plans:
plan.cost = calculate_cost(plan) # 计算IO/CPU/内存成本
return min(possible_plans, key=lambda p: p.cost) # 选择成本最低计划
成本计算因素:
单表扫描行数
索引过滤效率
连接操作复杂度
内存排序开销
临时表创建成本
场景 | 问题 | 解决方案 |
---|---|---|
多表JOIN顺序选择 | n!级组合爆炸 | STRAIGHT_JOIN提示 |
统计信息过期 | 成本计算偏差 | ANALYZE TABLE更新统计 |
复杂子查询 | 嵌套执行效率低 | 改写为JOIN |
函数表达式 | 索引失效 | 计算列+索引 |
EXPLAIN FORMAT=JSON
SELECT p.name, SUM(oi.quantity) AS total
FROM products p
JOIN order_items oi ON p.id = oi.product_id
WHERE p.category = 'Electronics'
GROUP BY p.id
HAVING total > 100;
JSON关键路径分析:
{
"query_block": {
"cost_info": {"query_cost": "126.75"},
"ordering_operation": {
"using_temporary_table": true, // 使用临时表
"using_filesort": true, // 文件排序
"grouping_operation": {
"table": {
"table_name": "p",
"access_type": "ref", // 索引访问类型
"key": "idx_category", // 使用索引
"rows_examined_per_scan": 342,
"attached_condition": "p.category = 'Electronics'"
},
"block-nl-join": { // 嵌套循环连接
"table": {
"table_name": "oi",
"access_type": "ref",
"key": "idx_product",
"rows_examined_per_scan": 15
}
}
}
}
}
}
访问类型(type) | 扫描方式 | 性能等级 | 触发场景 |
---|---|---|---|
system | 系统表单行访问 | ⭐⭐⭐⭐⭐ | 内存表或const join |
const | 主键/唯一索引等值 | ⭐⭐⭐⭐⭐ | WHERE id=1 |
eq_ref | 关联查询主键连接 | ⭐⭐⭐⭐ | JOIN ON t1.id=t2.primary |
ref | 非唯一索引等值 | ⭐⭐⭐ | WHERE index_col=value |
range | 索引范围扫描 | ⭐⭐ | WHERE id>100 |
index | 全索引扫描 | ⭐ | SELECT 索引列 |
ALL | 全表扫描 | ✘ | 无可用索引 |
Extra字段解析:
Using where
:存储引擎返回后过滤
Using index
:覆盖索引(最优)
Using temporary
:需创建临时表
Using filesort
:额外排序操作
Select tables optimized away
:优化器已优化
Rows列:预估扫描行数(与实际差异大需ANALYZE TABLE)
Filtered列:条件过滤百分比(<10%需优化)
-- 优化前(大表驱动小表)
SELECT * FROM large_table l
JOIN small_table s ON l.key = s.key;
-- 优化后(小表驱动大表)
SELECT /*+ STRAIGHT_JOIN(s,l) */ *
FROM small_table s
JOIN large_table l ON s.key = l.key;
-- 优化前(DEPENDENT SUBQUERY)
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id AND o.amount > 1000
);
-- 优化后(性能提升50倍)
SELECT u.*
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000
GROUP BY u.id; -- 去重
-- 优化前(扫描100000行)
SELECT * FROM orders
ORDER BY id DESC
LIMIT 100000, 20;
-- 优化后(扫描20行)
SELECT * FROM orders
WHERE id < (SELECT id FROM orders ORDER BY id DESC LIMIT 100000, 1)
ORDER BY id DESC
LIMIT 20;
-- 优化前(Using filesort)
SELECT * FROM products
WHERE category='Books'
ORDER BY price DESC;
-- 优化后(利用索引排序)
ALTER TABLE products
ADD INDEX idx_category_price (category, price DESC);
SELECT * FROM products
WHERE category='Books'
ORDER BY price DESC; -- Using index
-- 优化前(全表扫描)
SELECT * FROM users
WHERE DATE_FORMAT(reg_time, '%Y-%m') = '2023-07';
-- 优化后(函数索引)
ALTER TABLE users
ADD INDEX idx_reg_month ((DATE_FORMAT(reg_time, '%Y-%m')));
SELECT * FROM users
WHERE DATE_FORMAT(reg_time, '%Y-%m') = '2023-07'; -- 索引生效
-- 优化前(两次全表扫描)
SELECT * FROM logs_2023 WHERE type='ERROR'
UNION ALL
SELECT * FROM logs_2022 WHERE type='ERROR';
-- 优化后(分区表查询)
CREATE TABLE all_logs (
id BIGINT,
log_time DATETIME,
type VARCHAR(20)
) PARTITION BY RANGE (YEAR(log_time)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024)
);
SELECT * FROM all_logs
WHERE type='ERROR'; -- 分区裁剪
-- 优化前(隐式转换)
SELECT * FROM users
WHERE phone = 13800138000; -- phone是varchar
-- 优化后(消除类型转换)
SELECT * FROM users
WHERE phone = '13800138000';
-- 优化前(大文本检索)
SELECT * FROM articles
WHERE content LIKE '%database%';
-- 优化后(全文索引)
ALTER TABLE articles
ADD FULLTEXT INDEX ft_content (content);
-- 优化前(单条插入)
INSERT INTO sales (product_id, amount) VALUES (101, 20);
INSERT INTO sales (product_id, amount) VALUES (102, 15);
-- 优化后(批量提交)
START TRANSACTION;
INSERT INTO sales (product_id, amount) VALUES
(101,20), (102,15), ... , (199,30);
COMMIT;
-- 禁用索引加速
ALTER TABLE sales DISABLE KEYS;
-- 批量插入...
ALTER TABLE sales ENABLE KEYS;
-- 手动更新统计信息
ANALYZE TABLE orders, order_items;
-- 查看统计信息
SELECT
table_name,
index_name,
stat_value AS pages,
stat_description
FROM mysql.innodb_index_stats
WHERE table_name = 'orders';
-- 设置采样页数(提高准确性)
SET GLOBAL innodb_stats_persistent_sample_pages = 200;
# my.cnf 关键参数
[mysqld]
# 排序缓冲区
sort_buffer_size = 4M
# 连接缓冲区
join_buffer_size = 8M
# 临时表大小
tmp_table_size = 64M
max_heap_table_size = 64M
# 查询缓存(MySQL 8.0已移除)
# query_cache_type = 0
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
-- 设置阈值(单位:秒)
SET GLOBAL long_query_time = 0.5;
-- 记录未用索引的查询
SET GLOBAL log_queries_not_using_indexes = ON;
-- 日志路径
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
# mysqldumpslow 基础分析
mysqldumpslow -s t /var/log/mysql/slow.log
# pt-query-digest 专业分析
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
分析报告关键部分:
# Profile
# Rank Query ID Response time Calls R/Call
# ==== ================== ============= ===== =======
# 1 0xABCDEF123456789 112.4638 78.2% 120 0.9372
# 2 0xBCDEF123456789A 28.1195 19.5% 85 0.3308
# Query 1: 0.94 QPS, ID 0xABCDEF123456789
SELECT * FROM orders WHERE user_id=? AND status=?
定位问题SQL:pt-query-digest分析
EXPLAIN诊断:分析执行计划瓶颈
索引优化:添加缺失索引或调整索引
重写SQL:调整查询结构与逻辑
-- 创建物化视图
CREATE MATERIALIZED VIEW sales_summary
REFRESH FAST ON COMMIT
AS
SELECT product_id, SUM(amount) total
FROM sales
GROUP BY product_id;
-- 查询优化
SELECT * FROM sales_summary WHERE total > 1000; -- 避免全表聚合
-- 启用并行扫描
ALTER TABLE orders PARALLEL 8;
-- 并行查询示例
SELECT /*+ PARALLEL(4) */
user_id, COUNT(*)
FROM big_table
GROUP BY user_id;
-- 安装Rewriter插件
INSTALL PLUGIN rewriter SONAME 'rewriter.so';
-- 定义重写规则
INSERT INTO query_rewrite.rewrite_rules (pattern, replacement)
VALUES(
'SELECT * FROM products WHERE price < ?',
'SELECT * FROM products WHERE price < ? AND is_active=1'
);
-- 刷新规则
CALL query_rewrite.flush_rewrite_rules();
提示类型 | 语法示例 | 适用场景 |
---|---|---|
索引提示 | USE INDEX(idx_name) |
强制使用特定索引 |
JOIN顺序提示 | STRAIGHT_JOIN |
固定JOIN顺序 |
子查询策略提示 | /*+ SEMIJOIN(MATERIALIZATION) */ |
控制子查询执行策略 |
资源控制提示 | /*+ MAX_EXECUTION_TIME(1000) */ |
设置查询超时时间 |
并行查询提示 | /*+ PARALLEL(4) */ |
指定并行度 |
-- 综合使用示例
SELECT /*+
STRAIGHT_JOIN(o,u),
USE INDEX(o.idx_user_status),
MAX_EXECUTION_TIME(500)
*/
u.name, o.order_no
FROM orders o FORCE INDEX (idx_user_status)
JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid'
AND u.reg_time > '2023-01-01';
查询优化黄金法则:
先测量后优化:EXPLAIN+慢日志定位瓶颈
索引优先原则:80%性能问题通过索引解决
避免全表扫描:尤其大数据量表
警惕隐式转换:统一字符集与数据类型
分而治之:复杂查询拆分为简单操作
不同规模数据优化策略:
数据量 | 优化重点 | 工具链 |
---|---|---|
<10万 | 基础索引优化 | EXPLAIN |
10-500万 | 执行计划调优+参数优化 | pt-query-digest |
500万+ | 架构优化+高级特性 | 分区表/并行查询/物化视图 |