"数据库慢"是最模糊的故障描述。它可能是某一条 SQL 突然变慢,也可能是整体响应都变长;可能是磁盘满了,也可能是锁住了;甚至可能数据库根本没问题,是应用自己线程池耗尽在排队。如果一上来就重启数据库或者盲目加索引,大概率既没解决问题,又引入了新风险。
这篇文章给一套可复用的排查体系:先用系统指标和会话状态判断"慢在哪一层",再开慢查询日志把问题 SQL 抓出来,用 mysqldumpslow 和 pt-query-digest 做聚合,然后用 EXPLAIN 逐列分析执行计划,最后落到索引优化、SQL 改写和锁等待处理。每一步都配了可以直接复制执行的命令。
一、先定层级:是数据库慢还是别的地方慢
在动数据库之前,先用几分钟确认瓶颈确实在数据库这一层。
看系统资源
top # 看 CPU、内存,%wa 高说明在等 IO
iostat -x 1 # 看 %util、await、svctm,%util 接近 100% 说明磁盘饱和
vmstat 1 # r 列持续大于核数说明 CPU 排队
free -h # available 是否紧张,是否开始用 swap
如果 %wa 高且 %util 接近 100%,瓶颈在磁盘 IO;如果 CPU 用户态打满,可能是大量排序或扫描;如果内存开始用 swap,数据库性能会断崖式下降(因为本该在内存里的页被换出去了)。
看数据库会话状态
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Slow_queries';
SELECT id, time, state, LEFT(info, 100) FROM information_schema.PROCESSLIST
WHERE command != 'Sleep' ORDER BY time DESC LIMIT 10;
Threads_running 长期高于几十,说明有大量并发在执行;Slow_queries 是慢查询累计计数(需要开了慢日志才有意义)。
一个快速判断
在数据库本机执行一条简单的主键查询并计时:
time mysql -uroot -p -e "SELECT * FROM your_db.your_table WHERE id = 1\G" > /dev/null
如果这条毫秒级的查询也要花几百毫秒,说明是数据库连接或整体资源的问题;如果这条很快但业务 SQL 很慢,那就是 SQL 本身的问题。这一步能帮你省掉很多无用功。
提醒:命令行带密码会被 ps 看到并写入 history。建议配置 ~/.my.cnf 的 [client] 段(权限 600)或用 mysql_config_editor。
二、开启慢查询日志
慢查询日志是排查的起点。它记录执行时间超过阈值的 SQL。
查看当前配置
SHOW GLOBAL VARIABLES LIKE 'slow_query%';
SHOW GLOBAL VARIABLES LIKE 'long_query_time';
SHOW GLOBAL VARIABLES LIKE 'log_queries_not_using_indexes';
SHOW GLOBAL VARIABLES LIKE 'min_examined_row_limit';
几个关键参数:
| 参数 | 说明 | 建议 |
|---|---|---|
slow_query_log |
是否开启 | 生产建议常开 |
slow_query_log_file |
日志路径 | 放在数据盘之外的分区更好 |
long_query_time |
阈值(秒),支持小数 | OLTP 建议 0.5 到 1 秒;精细优化时临时设 0.1 |
log_queries_not_using_indexes |
记录未走索引的查询 | 开启,但会产生大量日志,需配合轮转 |
min_examined_row_limit |
扫描行数低于此值的不记录 | 设 100 左右,过滤掉扫描很少的 |
log_slow_admin_statements |
记录 ALTER 等管理语句 | 排查 DDL 影响时可开 |
log_throttle_queries_not_using_indexes |
每分钟最多记多少条未用索引的 | 开启日志爆炸时用它限流 |
临时开启(重启失效)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;
SET GLOBAL log_queries_not_using_indexes = ON;
SET GLOBAL min_examined_row_limit = 100;
注意:long_query_time 和 min_examined_row_limit 是会话级变量,改完全局后需要新连接才生效。改完后自己重连一次客户端再验证。
永久生效(写配置文件)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
log_queries_not_using_indexes = 1
min_examined_row_limit = 100
log_slow_admin_statements = 1
log_throttle_queries_not_using_indexes = 100
创建日志目录并授权(路径按你的实际配置调整):
sudo mkdir -p /var/log/mysql
sudo chown mysql:mysql /var/log/mysql
重启生效:
# RHEL/CentOS/Rocky
sudo systemctl restart mysqld
# Debian/Ubuntu
sudo systemctl restart mysql
慢日志要轮转,否则会撑爆磁盘
sudo tee /etc/logrotate.d/mysql-slow <<'EOF'
/var/log/mysql/slow.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 640 mysql mysql
postrotate
/usr/bin/mysqladmin flush-logs 2>/dev/null || true
endscript
}
EOF
风险提示:开启 log_queries_not_using_indexes 会让日志量急剧增长,尤其是有大表全表扫描的应用。上线前先评估磁盘空间,并配置好轮转。 磁盘写满会导致 MySQL 无法写入甚至崩溃,这是比慢查询严重得多的事故。
另外,慢日志本身有轻微性能开销(主要是写文件),但在高 IO 能力的盘上通常可以忽略。如果实在担心,可以在排查窗口临时开启,查完关掉。
三、分析慢日志:mysqldumpslow 与 pt-query-digest
mysqldumpslow(MySQL 自带)
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log # 按总耗时排序,取前 10
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log # 按出现次数排序
mysqldumpslow -s at -t 10 /var/log/mysql/slow.log # 按平均耗时排序
mysqldumpslow -g 'SELECT' -s t -t 5 /var/log/mysql/slow.log # 过滤包含 SELECT 的
-s 指定排序依据:t 总时间、at 平均时间、c 次数、r 返回行数、ar 平均返回行数。-t 是取前几条,-g 是正则过滤。
mysqldumpslow 的优点是随 MySQL 自带、零依赖;缺点是输出信息少,没有百分位、没有按模板归类的详细统计。
pt-query-digest(Percona Toolkit,强烈推荐)
# RHEL/CentOS/Rocky
sudo dnf install -y percona-toolkit
# Debian/Ubuntu
sudo apt-get install -y percona-toolkit
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
pt-query-digest --limit 100% --since '2026-01-01 00:00:00' /var/log/mysql/slow.log
报告的核心部分是把相似的 SQL 归成一个"模板"(把具体的参数值替换成占位符),然后给出:
Rank、Response time(总耗时与占比)、Calls(执行次数)R/Call(平均每次耗时)V/M(方差与均值的比,越大说明耗时波动越大)- 最后是该模板的一个代表性样例与完整统计直方图
怎么看这张表:优先处理"总耗时占比"最高的,而不是"单次最慢"的。 一条 10 秒的 SQL 一天只跑一次,和一条 200 毫秒的 SQL 一天跑 50 万次,显然后者更值得优化——它占用了绝大部分的数据库总时间。
常用变体:
# 只分析最近 12 小时
pt-query-digest --since 12h /var/log/mysql/slow.log
# 分析后写入数据库表便于对比(需先建库)
pt-query-digest --review h=localhost,D=test,t=query_review /var/log/mysql/slow.log
# 直接抓 TCP 流量分析,不依赖慢日志
tcpdump -i any -s 65535 -x -nn -q -tttt 'port 3306' > /tmp/mysql.tcp
pt-query-digest --type tcpdump /tmp/mysql.tcp
--type tcpdump 的方式很有用:不用重启、不用开慢日志,就能看到真实的查询分布。缺点是开销比慢日志大,只适合短时抓取。
四、EXPLAIN:逐列读懂执行计划
拿到问题 SQL 之后,用 EXPLAIN 看优化器打算怎么执行:
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 1 ORDER BY created_at DESC LIMIT 20\G
对已有连接也可以看真实执行计划(MySQL 8.0 支持 EXPLAIN FOR CONNECTION):
EXPLAIN FOR CONNECTION 12345;
以及带真实统计信息的分析(8.0.18+):
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 100;
各列含义
| 列 | 关键取值 | 说明 |
|---|---|---|
id |
数字 | 查询的层级,越大越先执行;相同则自上而下 |
select_type |
SIMPLE / PRIMARY / SUBQUERY / DERIVED / UNION | 查询类型 |
table |
表名或 |
访问的表,derived 表示临时派生表 |
type |
system > const > eq_ref > ref > range > index > ALL | 最重要的列,见下表 |
possible_keys |
索引名列表 | 理论上可能用到的索引 |
key |
索引名 | 实际用到的索引,为 NULL 就是没走索引 |
key_len |
字节数 | 实际使用的索引长度,可推断联合索引用了几列 |
ref |
const / 列名 / NULL | 与索引比较的是什么 |
rows |
数字 | 预估要扫描的行数,越小越好 |
filtered |
百分比 | 经过条件过滤后剩余的比例 |
Extra |
见下文 | 额外信息,藏着最关键的性能线索 |
type 从好到坏
| type | 含义 | 评价 |
|---|---|---|
system |
表只有一行 | 最好 |
const |
主键或唯一索引命中一行 | 非常好 |
eq_ref |
JOIN 时用主键/唯一索引匹配 | 好 |
ref |
普通非唯一索引匹配 | 通常可接受 |
range |
索引范围扫描 | 可接受,注意范围大小 |
index |
全索引扫描(扫整棵索引树) | 较差,但比 ALL 好(不回表时另说) |
ALL |
全表扫描 | 大表上基本要优化 |
经验标准:OLTP 业务里,type 至少要达到 range,最好是 ref 或 const。出现 ALL 且 rows 上万,就需要认真对待了。
Extra 里的关键提示
| Extra | 含义 | 处理方向 |
|---|---|---|
Using index |
覆盖索引,无需回表 | 好现象 |
Using where |
在存储引擎返回后又做了过滤 | 正常,但如果配合大 rows 要注意 |
Using filesort |
需要额外排序 | 重点关注,考虑建有序索引 |
Using temporary |
用了临时表 | 重点关注,优化 GROUP BY / DISTINCT |
Using index condition |
索引条件下推(ICP) | 好现象,减少回表 |
Using join buffer |
JOIN 没走索引 | 检查关联字段是否有索引、类型是否一致 |
Using MRR |
多范围读取优化 | 通常没问题 |
Using filesort 并不一定真的在写文件。 它只是表示"需要一次排序操作",数据量小时在内存里完成。真正麻烦的是 Using temporary; Using filesort 同时出现,或者 Copying to tmp table on disk(可以用 SHOW PROCESSLIST 观察到)。
判断排序是否落盘,看临时表配置:
SHOW GLOBAL VARIABLES LIKE 'tmp_table_size';
SHOW GLOBAL VARIABLES LIKE 'max_heap_table_size';
SHOW GLOBAL STATUS LIKE 'Created_tmp%';
Created_tmp_disk_tables / Created_tmp_tables 超过 25% 就偏高。
五、索引失效的典型写法
以下几种写法会让明明有索引的列也走不上索引。
1. 在列上用函数或表达式
-- 失效:对列做了函数运算
SELECT * FROM orders WHERE DATE(created_at) = '2026-01-01';
-- 改写:把函数挪到常量侧,变成范围查询
SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02';
-- 失效
SELECT * FROM users WHERE YEAR(birthday) = 1990;
-- 改写
SELECT * FROM users WHERE birthday >= '1990-01-01' AND birthday < '1991-01-01';
MySQL 8.0 支持函数索引(在表达式上建索引),可以作为一种兜底:
ALTER TABLE orders ADD INDEX idx_created_date ((DATE(created_at)));
但这通常是次优解,优先改写 SQL。
2. 隐式类型转换
-- user_id 是 VARCHAR,用数字查会导致隐式转换,索引失效
SELECT * FROM users WHERE user_id = 12345;
-- 正确
SELECT * FROM users WHERE user_id = '12345';
反过来,如果列是 INT 而传入字符串,MySQL 会把字符串转成数字,通常还能走索引(因为转换发生在常量侧)。所以真正的雷区是字符串列被当成数字查——MySQL 会对列做 CAST,导致整个列上的函数运算,索引彻底失效。
判断方法:EXPLAIN 里看是否出现 Using where 且 key 为 NULL;也可以用 SHOW WARNINGS 看优化器改写后的 SQL:
EXPLAIN SELECT * FROM users WHERE user_id = 12345;
SHOW WARNINGS;
3. 前导通配符模糊查询
-- 失效:% 开头
SELECT * FROM articles WHERE title LIKE '%mysql%';
-- 可用:前缀匹配,能走索引
SELECT * FROM articles WHERE title LIKE 'mysql%';
B+ 树索引按前缀有序组织,% 在开头就无法定位起始位置。'%xxx' 这种需求要么用全文索引(FULLTEXT),要么接搜索引擎(Elasticsearch 等),不要在 MySQL 里硬扛全表扫描。
4. 不满足最左前缀
有联合索引 (a, b, c):
WHERE a = 1 AND b = 2 -- 用到 a、b
WHERE a = 1 AND b = 2 AND c = 3 -- 用到 a、b、c
WHERE a = 1 AND c = 3 -- 只用到 a(c 被跳过,无法继续用)
WHERE b = 2 AND c = 3 -- 完全用不上(缺最左列 a)
WHERE a > 1 AND b = 2 -- 只用到 a,范围之后的列不再用于索引定位
注意最后一条:范围查询(>、<、BETWEEN、LIKE 前缀)之后的索引列只能作为过滤条件,不能继续用于索引定位。 这是联合索引设计时最容易踩的坑。
5. OR 连接不同列
-- 若 name 和 phone 各自单列索引,OR 可能导致索引合并或全表扫描
SELECT * FROM users WHERE name = 'a' OR phone = '13800138000';
-- 改写为 UNION ALL,各自走各自的索引
SELECT * FROM users WHERE name = 'a'
UNION ALL
SELECT * FROM users WHERE phone = '13800138000' AND name <> 'a';
MySQL 有 index_merge 优化(Extra 里显示 Using union(...)),但它的效果往往不如 UNION 改写稳定,且优化器有时会放弃使用它。
6. 其他常见情况
!=、<>、NOT IN、NOT EXISTS通常选择性差,优化器倾向全表扫描IS NULL在部分情况下可以用索引,但IS NOT NULL效果差- 查询结果集占全表比例过高(经验上超过 20% 到 30%)时,优化器会主动放弃索引,因为回表成本高于顺序扫描——这不一定是优化器错了
- 索引列参与了计算:
WHERE price * 2 > 100应改写成WHERE price > 50
六、覆盖索引与联合索引顺序
覆盖索引
如果索引已经包含了查询需要的全部列,就不需要回表查主键行,Extra 里会显示 Using index。这能显著减少随机 IO。
-- 有索引 (user_id, status, created_at)
SELECT user_id, status, created_at FROM orders WHERE user_id = 100;
-- 覆盖索引:Extra = Using index
SELECT * FROM orders WHERE user_id = 100;
-- 需要回表:Extra 无 Using index
实践建议:禁止在业务代码里写 SELECT *,只取需要的列。 这不只是规范问题,它直接决定了能不能用上覆盖索引。
联合索引的列顺序
经验原则:
第一原则:等值条件列在前,范围条件列在后。 因为范围之后的列无法继续走索引。
-- 查询:WHERE user_id = ? AND status = ? AND created_at > ?
-- 索引应该是 (user_id, status, created_at),把范围列放最后
ALTER TABLE orders ADD INDEX idx_uid_st_ct (user_id, status, created_at);
第二原则:区分度高的列尽量靠前(在满足第一原则的前提下)。区分度可以用下面这个查询估算,越接近 1 越好:
SELECT COUNT(DISTINCT user_id) / COUNT(*) AS user_id_sel,
COUNT(DISTINCT status) / COUNT(*) AS status_sel
FROM orders;
第三原则:兼顾排序与分组。 如果查询有 ORDER BY created_at,把 created_at 放进索引的最后一位,让索引本身有序,就能避免 Using filesort:
-- WHERE user_id = ? ORDER BY created_at DESC
ALTER TABLE orders ADD INDEX idx_uid_ct (user_id, created_at);
冗余索引要清理
有了 (a, b, c) 之后,(a) 和 (a, b) 基本就是冗余的(除非有单独按 b 查询的需求)。冗余索引会拖慢写入并占用空间。可以查:
SELECT * FROM sys.schema_redundant_indexes LIMIT 20;
SELECT * FROM sys.schema_unused_indexes LIMIT 20;
sys.schema_unused_indexes 依赖 performance_schema 的统计,需要实例跑了一段时间才有参考价值。删除索引前先确认业务确实没用它,并选在低峰操作;索引删除是 DDL,大表上可能耗时很久。
统计信息不准也会导致选错索引
如果 EXPLAIN 的结果明显不合理(明明有更好的索引却选了全表扫描),可能是统计信息过期:
ANALYZE TABLE orders;
SHOW INDEX FROM orders;
SHOW INDEX 的 Cardinality 列反映基数估计。长期没跑过 ANALYZE 的大表、或者数据分布发生剧变后,统计信息会失真。
必要时可以用索引提示强制走某个索引作为临时手段:
SELECT * FROM orders FORCE INDEX (idx_uid_ct) WHERE user_id = 100;
索引提示是双刃剑:它绕过了优化器的判断,数据分布变化后可能变成负优化。只在紧急止血和验证效果时使用,长期方案还是优化统计信息和索引设计。
七、分页深翻优化
深翻页是 classic 的性能杀手:
SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 100000, 20;
MySQL 需要先扫描并丢弃前 100000 行,再返回 20 行。翻到几百页之后,这个开销是灾难性的。
方案一:延迟关联(子查询先定位主键)
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders WHERE status = 1 ORDER BY created_at DESC LIMIT 100000, 20
) t ON o.id = t.id
ORDER BY o.created_at DESC;
子查询只用覆盖索引取主键,避免了大量回表。通常能快几倍到几十倍。
方案二:游标分页(记录上一页的位置)
-- 第一页
SELECT * FROM orders WHERE status = 1 ORDER BY created_at DESC, id DESC LIMIT 20;
-- 后续页,用上一页最后一行的 created_at 和 id 作为游标
SELECT * FROM orders
WHERE status = 1 AND (created_at, id) < ('2026-01-01 10:00:00', 12345)
ORDER BY created_at DESC, id DESC LIMIT 20;
游标分页可以做到任意深度都是常数开销,是游标式无限滚动(信息流、时间线)场景的标准解法。代价是不能跳页,需要产品上接受"上一页/下一页"而不是"第 N 页"。
方案三:业务上限制深度
很多后台系统的深翻页其实是误操作。可以限制最多翻到前 100 页,或者要求先加筛选条件缩小结果集。这是对成本最低的一种"优化"。
八、锁等待与 sys schema
慢并非都是索引问题,也可能是被锁住
表现是:SQL 本身很简单,EXPLAIN 也走索引,但就是卡住不动,SHOW PROCESSLIST 里显示 Waiting for table metadata lock 或者长时间无状态推进。
SHOW ENGINE INNODB STATUS\G
重点看这几个段:
TRANSACTIONS:列出当前活跃事务,包含事务开始时间、持有的锁、等待的锁LATEST DETECTED DEADLOCK:最近一次死锁的详细信息,包含两个事务各自持有的锁和等待的锁、以及被回滚的是哪一个SEMAPHORES:大量等待通常说明锁争用严重BUFFER POOL AND MEMORY、FILE I/O:辅助看 IO 状态
注意 SHOW ENGINE INNODB STATUS 输出的是最近一次死锁,不是全部。要看更完整的死锁信息,可以开启 innodb_print_all_deadlocks,把死锁写进错误日志。
查当前锁等待(MySQL 8.0)
SELECT waiting_pid, waiting_query, blocking_pid, blocking_query,
wait_age, sql_kill_blocking_query
FROM sys.innodb_lock_waits;
这个视图直接给出了"谁在等、被谁挡住、杀掉阻塞者的语句"。执行 sql_kill_blocking_query 前务必确认阻塞事务的业务含义——它可能是一个正在跑的重要批量任务。
查长时间未提交的事务
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id,
trx_rows_locked, trx_rows_modified, LEFT(trx_query, 100) AS q
FROM information_schema.innodb_trx
ORDER BY trx_started
LIMIT 10;
一个开了几分钟还没提交的事务,很可能就是元数据锁的源头。事务要短,这是 InnoDB 使用的基本原则之一:不要在事务里做远程调用、不要等用户输入、批量操作要分批提交。
sys schema 常用视图
-- 全表扫描的语句
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10;
-- 总耗时最高的语句模板
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile LIMIT 10;
-- 临时表/排序最多的语句
SELECT * FROM sys.statements_with_temp_tables LIMIT 10;
SELECT * FROM sys.statements_with_sorting LIMIT 10;
-- 错误与警告
SELECT * FROM sys.statements_with_errors_or_warnings LIMIT 10;
-- 表维度的 IO 统计
SELECT * FROM sys.schema_table_statistics_with_buffer LIMIT 10;
-- 从未被使用过的索引
SELECT * FROM sys.schema_unused_indexes LIMIT 20;
sys schema 本质上是对 performance_schema 的封装,输出更友好。如果查询报错说 performance_schema 未启用,需要在配置文件里开启 performance_schema = ON 并重启。
九、排查流程小结
一个可复用的顺序:
# 1. 系统层:CPU / IO / 内存
top; iostat -x 1; free -h
# 2. 数据库层:活跃线程、当前长查询
mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running'"
mysql -e "SELECT id,time,state,LEFT(info,120) FROM information_schema.PROCESSLIST WHERE command!='Sleep' ORDER BY time DESC LIMIT 10"
# 3. 开慢日志(如未开),跑一个业务周期
mysql -e "SET GLOBAL slow_query_log=ON; SET GLOBAL long_query_time=0.5"
# 4. 聚合分析,找总耗时占比最高的模板
pt-query-digest /var/log/mysql/slow.log > /tmp/report.txt
# 5. 对 Top SQL 做执行计划分析
mysql -e "EXPLAIN \G"
# 6. 改索引 / 改 SQL / 改业务形态,然后对比验证
验证环节不能省:改完索引后重新跑一遍 EXPLAIN 确认 type 和 rows 的变化,再用 pt-query-digest 对比前后同一时间窗口的耗时。凭感觉说"应该快了"是不合格的。
常见问题
开了慢日志但没有记录,为什么?
几个常见原因:改了 long_query_time 但没重连会话(它是会话级变量);slow_query_log_file 指向的路径 MySQL 进程无权限写;min_examined_row_limit 设得太大把查询过滤掉了;或者查询确实都比阈值快。先用 SHOW GLOBAL VARIABLES 确认实际生效值,再用 SELECT SLEEP(2) 造一条慢查询验证。
long_query_time 设多少合适?
日常生产建议 0.5 到 1 秒。做精细优化时可以临时设到 0.1 秒抓更多样本,但要注意日志量会暴涨、且磁盘压力上升,抓完记得调回去。不要长期设成 0,那等于把所有查询都记下来,开销不可接受。
为什么明明有索引却不走?
最常见的是三种:查询列上加了函数或发生了隐式类型转换;结果集占全表比例太高(优化器认为全表扫描更便宜);联合索引不满足最左前缀。用 EXPLAIN 看 possible_keys 和 key,如果 possible_keys 有值但 key 是 NULL,基本就是优化器的成本判断,可以先 ANALYZE TABLE 刷新统计信息再试。
Using filesort 一定要消除吗?
不一定。小结果集的内存排序开销很小,为了消除它而加索引会增加写入成本和空间。判断标准是:这条 SQL 的总耗时占比高不高,排序的数据量大不大。优先处理 Top SQL,而不是把所有 filesort 都消灭掉。
索引加得越多越好吗?
不是。每个索引都会让 INSERT、UPDATE、DELETE 变慢(要维护多棵 B+ 树),占用更多磁盘和 buffer pool 空间,还会让优化器的选择变复杂。经验上一个表的索引数控制在 5 个以内比较健康,定期用 sys.schema_unused_indexes 清理没人用的。
大表加索引会不会锁表?
MySQL 5.6 之后 InnoDB 支持 Online DDL,加索引通常不会全程锁表,但仍有准备阶段和最后应用增量日志阶段的短暂锁,且大表操作可能耗时很久、产生大量 IO 和主从延迟。生产环境建议用 pt-online-schema-change 或 gh-ost 这类在线改表工具,并先在从库或从库快照上验证耗时。 操作前必须有可用备份。
总结
- 先定层级再动手:用
top、iostat -x 1、free -h区分 CPU、IO、内存瓶颈,再用Threads_running和PROCESSLIST看是否真有长查询。在本机跑一条主键点查计时,能快速判断是数据库慢还是应用侧慢。 - 慢日志要常开且要轮转:
slow_query_log=1、long_query_time设 0.5 到 1 秒、log_queries_not_using_indexes配合min_examined_row_limit和 logrotate 使用,避免日志撑爆磁盘——磁盘满比慢查询严重得多。 - 分析看总耗时占比而不是单次最慢:
pt-query-digest的聚合报告里,总耗时占比最高的模板才是优化收益最大的目标;mysqldumpslow -s t -t 10是零依赖的备选。 - EXPLAIN 重点看四列:
type至少要到range(OLTP 最好ref/const)、key是否为 NULL、rows预估扫描行数、Extra里的Using filesort与Using temporary。MySQL 8.0 的EXPLAIN ANALYZE能看到真实耗时。 - 索引失效有六种典型写法:列上用函数、隐式类型转换、
%开头模糊匹配、不满足最左前缀、OR 连接不同列、以及范围条件后的列无法继续索引定位。改写 SQL 优先于加函数索引,索引提示只用于临时止血。 - 深翻页用延迟关联或游标分页:
LIMIT 100000, 20的开销来自丢弃前 10 万行;游标分页能做到常数开销但不支持跳页,需要产品形态配合。 - 慢也可能是锁:
SHOW ENGINE INNODB STATUS看事务与死锁段,sys.innodb_lock_waits直接给出阻塞关系。事务要短,杀阻塞会话前必须确认它的业务含义。所有索引变更和 DDL 前先确认有可用备份,并在低峰执行。
企业QQ咨询




