慢查询的发现与定性
慢查询优化是数据库运维中最高频的工作。发现慢查询的途径有三个:慢查询日志(Slow Log)、information_schema.processlist实时捕获、以及Performance Schema的事件记录。慢查询日志是最基础的手段,开启后记录所有执行时间超过long_query_time的SQL。
# 开启慢查询日志并配置参数
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
# 查看当前配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
long_query_time的默认值是10秒,这对大多数业务来说太宽松了。线上环境建议设为0.5-1秒,配合log_queries_not_using_indexes捕获全表扫描。需要注意,降低阈值会增加日志量,需要配套日志轮转策略。
EXPLAIN执行计划深度解读
EXPLAIN是分析SQL执行计划的核心工具,但很多开发者只关注type列是否为ALL。实际上需要综合多个列才能判断执行计划是否合理。
关键列解读
type列(访问类型,从优到差):system > const > eq_ref > ref > range > index > ALL。ALL是全表扫描,必须优化。index是全索引扫描,看起来比ALL好,但如果扫描行数很大,实际性能可能和ALL差不多。
key列:实际使用的索引名。如果为NULL说明没有使用索引。possible_keys列显示可选索引,key列显示实际选中索引——如果possible_keys有值但key为NULL,说明优化器评估后放弃了索引,需要关注rows估算值来判断原因。
rows列:预估扫描行数。这个值是优化器基于统计信息的估算值,不一定精确,但量级是可靠的。rows乘以filtered百分比就是最终参与计算的数据行数,越小越好。
Extra列:额外信息。需要重点关注的值:
– Using filesort:额外排序操作,需要优化
– Using temporary:使用临时表,常见于GROUP BY和DISTINCT
– Using index:覆盖索引,性能好
– Using where:Server层过滤,检查是否可以下推到存储引擎
# 分析慢查询的执行计划
EXPLAIN SELECT o.order_id, o.amount, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at BETWEEN '2026-07-01' AND '2026-07-31'
AND o.status = 'PAID'
ORDER BY o.amount DESC
LIMIT 20;
# 问题:rows=50000且filtered=10%,实际匹配5000行
# filesort说明排序没有利用索引
索引设计实战:避免常见反模式
反模式1:索引列上使用函数
# 错误:索引列上使用函数,索引失效
SELECT * FROM orders WHERE DATE(created_at) = '2026-07-29';
# 正确:范围查询,可以利用索引
SELECT * FROM orders
WHERE created_at >= '2026-07-29 00:00:00'
AND created_at < '2026-07-30 00:00:00';
反模式2:联合索引列顺序错误
联合索引遵循最左前缀原则。索引(a, b, c)可以支持a、(a,b)、(a,b,c)的查询,但不能支持(b,c)或(c)的查询。
# 场景:按状态和时间范围查询订单
# 常见查询模式:
# WHERE status = 'PAID' AND created_at BETWEEN ... AND ...
# WHERE status = 'PAID' AND user_id = 123
# 错误索引:把范围查询列放在前面
# INDEX idx_wrong (created_at, status, user_id)
# 正确索引:等值查询列在前,范围查询列在后
CREATE INDEX idx_order_query ON orders (status, user_id, created_at);
反模式3:索引选择性低却建索引
选择性低的列(如性别、状态只有2-3个值)不适合单独建索引。MySQL优化器在这种情况下会选择全表扫描,因为回表代价高于顺序扫描。
# 评估索引选择性
SELECT
COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
COUNT(DISTINCT customer_id) / COUNT(*) AS customer_selectivity
FROM orders;
# status_selectivity: 0.00003 (状态只有5个值,不适合单列索引)
# customer_selectivity: 0.65 (适合建索引)
覆盖索引优化:减少回表次数
回表(Bookmark Lookup)是InnoDB查询的重要性能瓶颈。二级索引找到主键后需要回聚簇索引获取完整行数据,每次回表都是一次随机IO。覆盖索引让查询所需的所有列都包含在索引中,避免回表。
# 原查询:需要回表获取amount和status
SELECT order_id, amount, status FROM orders WHERE customer_id = 12345;
# 创建覆盖索引
CREATE INDEX idx_customer_covering ON orders (customer_id, order_id, amount, status);
# EXPLAIN结果中Extra列出现"Using index"说明覆盖索引生效
# 性能提升:从扫描5000行+5000次回表,到直接从索引返回结果
覆盖索引的代价是索引体积增大,影响INSERT/UPDATE性能。在写入频繁的表上使用覆盖索引需要权衡读写比例。一般来说,读写比超过10:1时覆盖索引收益明显。
线上优化操作的安全规范
在正在运行的数据库上执行DDL需要格外谨慎。MySQL 8.0的Online DDL虽然支持INSTANT和INPLACE算法,但并非所有ALTER操作都能在线执行。
# 安全的索引创建方式
# 1. 使用ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE
ALTER TABLE orders
ADD INDEX idx_order_query (status, user_id, created_at),
ALGORITHM=INPLACE, LOCK=NONE;
# 2. MySQL 8.0的不可见索引(先建后验证)
ALTER TABLE orders
ADD INDEX idx_test (customer_id, created_at) INVISIBLE;
# 验证无性能问题后设为可见
ALTER TABLE orders ALTER INDEX idx_test VISIBLE;
# 3. 大表索引创建使用pt-online-schema-change
# pt-online-schema-change --alter "ADD INDEX idx_xxx(col)" # --host=127.0.0.1 --user=dba --ask-pass # D=production,t=orders # --chunk-size=1000 --max-load=Threads_running=50
慢查询治理的长效机制
单次优化只能解决当前问题,长效机制才能防止慢查询反复出现。建立三层防护:
第一层:代码审核。在Merge Request中增加SQL审查环节,使用SQL解析工具(如Soar/SQLCheck)自动检测常见反模式。对新上线的SQL要求提供EXPLAIN结果。
第二层:慢查询告警。对slow query log做实时解析,5分钟内扫描行数超过10万或执行时间超过5秒的查询触发告警。
第三层:定期巡检。每周分析Top 20慢查询,识别新增的慢查询和执行次数上升的已有慢查询。使用pt-query-digest做汇总分析:
# pt-query-digest分析慢日志
pt-query-digest /var/log/mysql/slow.log --since "7 days" \
--limit 20 --output slowlog-report.txt
通过三层防护,慢查询在开发阶段被拦截、运行时被监控、长期被治理,数据库性能才能保持稳定。优化不是一锤子买卖,而是持续运营。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-man-cha-xun-zhen-duan-yu-suo-yin-you-hua-shi-zhan/