1. 项目概述:SQL性能调优的核心价值
在数据库应用开发中,SQL查询性能往往是决定系统响应速度的关键瓶颈。我处理过太多因为一条糟糕的SQL拖垮整个系统的案例——某个电商平台在促销期间因为未优化的商品查询SQL导致数据库CPU飙升至100%,某个OA系统因为报表查询没有合理使用索引让用户每次等待超过30秒。这些血淋淋的教训让我深刻认识到:掌握EXPLAIN工具和慢查询优化技巧,不是DBA的专属技能,而是每个与数据库打交道的开发者必须修炼的内功。
EXPLAIN作为SQL性能分析的显微镜,能揭示查询执行计划的每一个细节。而慢查询日志则是数据库系统的"黑匣子",记录下所有执行时间超过阈值的SQL语句。两者结合使用,就像给数据库装上了X光机和心电图仪,让性能问题无所遁形。本专题将带您深入理解EXPLAIN的输出含义,并通过真实案例演示如何将一条执行需要5秒的SQL优化到50毫秒以内。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. EXPLAIN工具深度解析
2.1 EXPLAIN基础语法与输出解读
在MySQL中,只需在SELECT语句前加上EXPLAIN关键字即可查看执行计划。但理解这些输出信息需要专业知识:
sql复制EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'completed';
典型的EXPLAIN输出包含以下关键列:
| 列名 | 说明 | 优化关注点 |
|---|---|---|
| id | 查询标识符 | 复杂查询中各部分的执行顺序 |
| select_type | 查询类型 | 是否出现低效的DEPENDENT SUBQUERY等类型 |
| table | 访问的表 | 确认是否访问了预期表 |
| partitions | 匹配的分区 | 分区裁剪是否生效 |
| type | 访问类型 | 从优到差:system > const > eq_ref > ref > range > index > ALL |
| possible_keys | 可能使用的索引 | 检查预期索引是否列出 |
| key | 实际使用的索引 | 是否使用了最优索引 |
| key_len | 使用的索引长度 | 复合索引的使用情况 |
| ref | 列与索引的比较 | 索引使用的准确性 |
| rows | 预估检查行数 | 数值越大性能风险越高 |
| filtered | 条件过滤百分比 | 100%表示完全通过索引过滤 |
| Extra | 额外信息 | Using filesort、Using temporary需要警惕 |
特别注意:type列为ALL表示全表扫描,在大型表上这是性能灾难的信号。而Extra中出现"Using filesort"或"Using temporary"通常意味着需要优化。
2.2 高级EXPLAIN技巧
MySQL 8.0之后,EXPLAIN ANALYZE提供了实际执行统计信息:
sql复制EXPLAIN ANALYZE
SELECT p.* FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.name = 'Electronics'
ORDER BY p.price DESC LIMIT 100;
这个命令会执行查询并返回实际耗时(而非常规EXPLAIN的预估数据),包括:
- 每个操作的实际执行时间
- 迭代器之间的数据传递量
- 内存使用情况
对于复杂查询,FORMAT=JSON参数能获取更详细的信息:
sql复制EXPLAIN FORMAT=JSON
SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM large_table;
JSON格式的输出包含成本估算、优化器决策过程等深度信息,适合分析特别复杂的查询计划。
3. 慢查询优化全流程
3.1 慢查询日志配置与解读
启用慢查询日志是性能调优的第一步。在my.cnf中配置:
ini复制[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 1 # 超过1秒的查询
log_queries_not_using_indexes = 1 # 记录未使用索引的查询
log_throttle_queries_not_using_indexes = 10 # 限制每分钟记录数量
使用mysqldumpslow工具分析日志:
bash复制# 查看最耗时的10个查询
mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log
# 按出现次数排序
mysqldumpslow -s c -t 10 /var/log/mysql/mysql-slow.log
对于更复杂的分析,Percona的pt-query-digest是专业选择:
bash复制pt-query-digest /var/log/mysql/mysql-slow.log > slow_report.txt
该工具提供:
- 查询执行时间分布
- 表访问统计
- 索引使用情况
- 优化的优先级建议
3.2 索引优化实战策略
案例1:联合索引顺序优化
原始查询:
sql复制SELECT * FROM orders
WHERE create_time > '2023-01-01' AND status = 'shipped'
ORDER BY amount DESC;
初始EXPLAIN显示type=ALL,Extra=Using filesort。优化步骤:
- 确认WHERE条件和ORDER BY涉及的列
- 创建联合索引:(status, create_time, amount)
- 等值条件status放最左
- 范围条件create_time次之
- 排序字段amount放在最后
优化后索引完全覆盖查询,避免了排序操作。
案例2:函数索引解决LIKE查询
对于如下低效查询:
sql复制SELECT * FROM users
WHERE SUBSTRING(email, 1, 5) = 'admin' AND department_id = 10;
MySQL 8.0+可以创建函数索引:
sql复制ALTER TABLE users ADD INDEX idx_email_prefix ((SUBSTRING(email, 1, 5)));
5.7版本则需要改写查询或使用计算列:
sql复制ALTER TABLE users
ADD COLUMN email_prefix VARCHAR(5) AS (SUBSTRING(email, 1, 5)) STORED,
ADD INDEX idx_email_prefix (email_prefix);
3.3 查询重写技巧
案例3:将OR条件转为UNION
低效查询:
sql复制SELECT * FROM products
WHERE category_id = 5 OR price > 1000;
优化为:
sql复制SELECT * FROM products WHERE category_id = 5
UNION
SELECT * FROM products WHERE price > 1000;
确保每个UNION分支都能使用不同索引。
案例4:避免SELECT * 滥用
典型问题查询:
sql复制SELECT * FROM customers
WHERE register_time > '2023-01-01' LIMIT 100;
优化方案:
sql复制SELECT id, name, email FROM customers # 只查询必要字段
WHERE register_time > '2023-01-01' LIMIT 100;
对于大表,字段数量直接影响:
- 内存消耗
- 网络传输量
- 临时表空间使用
4. 高级优化技术与实战案例
4.1 分页查询深度优化
典型的分页性能问题:
sql复制SELECT * FROM large_table ORDER BY create_time DESC LIMIT 10000, 20;
优化方案1:延迟关联
sql复制SELECT t.* FROM large_table t
JOIN (
SELECT id FROM large_table
ORDER BY create_time DESC
LIMIT 10000, 20
) AS tmp ON t.id = tmp.id;
优化方案2:基于游标的分页(要求有序且唯一)
sql复制SELECT * FROM large_table
WHERE create_time < '2023-06-01 00:00:00' # 上一页最后一条记录的时间
ORDER BY create_time DESC LIMIT 20;
4.2 大数据量下的JOIN优化
案例5:小表驱动大表原则
错误写法:
sql复制SELECT * FROM large_table l
JOIN small_table s ON l.key = s.key;
正确写法:
sql复制SELECT * FROM small_table s
JOIN large_table l ON s.key = l.key;
案例6:使用STRAIGHT_JOIN控制连接顺序
当优化器选择不佳时:
sql复制SELECT STRAIGHT_JOIN l.*, s.*
FROM lookup_table l
JOIN large_table s ON l.id = s.lookup_id
WHERE l.category = 'A';
4.3 临时表与排序优化
发现Using temporary和Using filesort时的处理:
- 增大sort_buffer_size:
sql复制SET sort_buffer_size = 4*1024*1024; # 4MB
- 考虑使用SQL_BIG_RESULT提示:
sql复制SELECT SQL_BIG_RESULT * FROM large_table ORDER BY non_indexed_column;
- 对于GROUP BY导致的临时表,尝试调整sql_mode:
sql复制SET sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
5. 性能监控与持续优化
5.1 性能模式(Performance Schema)应用
查看最耗资源的SQL:
sql复制SELECT digest_text, count_star, avg_timer_wait/1000000000 as avg_ms
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC LIMIT 10;
监控索引使用情况:
sql复制SELECT object_schema, object_name, index_name, count_star
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
ORDER BY count_star DESC;
5.2 优化器提示(Hints)实战
强制使用特定索引:
sql复制SELECT * FROM orders USE INDEX(idx_status_date)
WHERE status = 'shipped' AND create_date > '2023-01-01';
限制执行时间:
sql复制SELECT /*+ MAX_EXECUTION_TIME(1000) */ * FROM large_table
WHERE complex_condition = 1;
5.3 执行计划绑定(Plan Binding)
MySQL 8.0+支持执行计划绑定,防止计划退化:
sql复制-- 捕获好的执行计划
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE status = ? AND create_time > ? INTO @plan;
-- 创建绑定
EXECUTE IMMEDIATE 'CREATE PLAN BINDING FOR STATEMENT
USING /*+ INDEX(orders idx_status_date) */';
6. 真实案例分析:电商系统优化实录
6.1 案例背景
某电商平台在促销期间出现数据库负载飙升,主要慢查询是商品搜索:
sql复制SELECT p.* FROM products p
JOIN product_category pc ON p.id = pc.product_id
WHERE pc.category_id IN (5,12,18)
AND p.status = 'on_shelf'
AND p.stock > 0
AND (p.name LIKE '%手机%' OR p.keywords LIKE '%手机%')
ORDER BY p.sales_volume DESC
LIMIT 0, 50;
6.2 问题诊断
EXPLAIN显示:
- product_category表使用全表扫描
- products表虽然使用了索引,但Extra显示"Using filesort"
- 预估检查行数超过10万
6.3 优化方案
- 创建更合适的索引:
sql复制ALTER TABLE product_category ADD INDEX idx_cat_prod (category_id, product_id);
ALTER TABLE products ADD INDEX idx_status_stock_sales (status, stock, sales_volume);
- 重写查询逻辑:
sql复制SELECT p.* FROM products p
WHERE p.id IN (
SELECT product_id FROM product_category
WHERE category_id IN (5,12,18)
)
AND p.status = 'on_shelf'
AND p.stock > 0
AND (p.name LIKE '%手机%' OR p.keywords LIKE '%手机%')
ORDER BY p.sales_volume DESC
LIMIT 0, 50;
- 对于文本搜索,考虑引入Elasticsearch:
sql复制-- 只从ES获取ID列表
SELECT p.* FROM products p
WHERE p.id IN (/* ES返回的ID列表 */)
ORDER BY p.sales_volume DESC
LIMIT 0, 50;
6.4 优化效果
执行时间从原来的3.2秒降低到120毫秒,数据库CPU负载下降60%。
