1. 项目概述:当一条SQL拖垮整台数据库,我们到底在和什么搏斗?
“由一条超级慢的全表查询sql导致MySQL服务故障引发的深思”——这个标题不是危言耸听,也不是运维日志里的模糊归因,而是我过去三年里亲手处理过7次、参与复盘超20场的真实事故切片。它背后站着的,不是某条孤立的SELECT语句,而是一整套被长期忽视的数据库治理断层:从开发写SQL时的“能跑就行”,到测试环境里永远不配真实数据量的验证逻辑,再到DBA监控告警中常年静默的“慢查询阈值”,最后坍塌在凌晨三点的主库CPU 99%、连接池耗尽、所有业务接口503的现场。关键词里没有出现“索引”“执行计划”“锁等待”,但恰恰是这些最基础、最枯燥、最该被刻进肌肉记忆里的东西,在每一次故障中反复暴露出系统性失守。这篇文章不讲高大上的分布式架构,也不谈云原生数据库新特性,就聚焦在一条“超级慢”的全表扫描SQL上——它怎么诞生的?为什么没被拦住?为什么一查就崩?以及,当它真的把MySQL拖进雪崩边缘时,你手里的那条SHOW PROCESSLIST命令,到底该先看哪三行?适合谁读?如果你是刚写完第一个CRUD接口的后端新人,这篇能让你避开入职三个月就被叫去救火的坑;如果你是带团队的Tech Lead,这里拆解的5个关键拦截点,可以直接嵌入你们的Code Review Checklist;如果你是DBA或SRE,文末的“连接数突增-锁等待-IO打满”三级连锁反应推演,就是你下次做压测方案时必须覆盖的故障注入路径。这不是理论推演,是我在某电商大促前夜,盯着pt-query-digest输出的Top 10慢查报告,用红笔圈出第7条“SELECT * FROM order_detail WHERE user_id = ?”时,手心冒汗的真实记录。
2. 核心问题溯源:为什么“全表扫描”会成为MySQL的阿喀琉斯之踵?
2.1 全表扫描的本质:不是慢,而是“不可控的资源吞噬”
很多人第一反应是:“加个索引不就完了?”——这就像看见房子着火,第一句话是“以后别用打火机”。全表扫描(Full Table Scan)在MySQL中根本不是一种“错误操作”,而是一种完全合法、甚至在某些场景下最优的执行策略。它的本质,是优化器在缺乏有效索引、或索引选择性极低时,判断“逐行读取整张表数据+内存过滤”比“走索引树+回表+多次随机IO”更省资源。问题在于:这个“省资源”的计算,只基于单次查询的代价模型,却完全无视并发下的系统级资源挤占。
举个具体例子:一张user_behavior_log表,当前有2800万行,无任何复合索引。某次运营活动需要统计“近7天iOS用户点击首页Banner的UV”,开发写了这条SQL:
SELECT COUNT(DISTINCT user_id) FROM user_behavior_log WHERE event_type = 'click_banner' AND os_type = 'iOS' AND event_time >= '2024-05-01 00:00:00';表面看,WHERE条件有三个字段,似乎该建联合索引。但实际执行计划(EXPLAIN)显示type: ALL,rows: 28000000。为什么?因为event_time是时间范围查询,放在联合索引最右位时,MySQL无法利用索引进行范围扫描(B+树索引对范围查询的支持仅限于最右连续列)。若强行建(event_type, os_type, event_time),由于event_type和os_type选择性太低(比如90%日志都是click_banner和iOS),索引区分度不足,优化器仍会判定全表扫描更优。此时,“慢”只是表象,真正的危险在于:单次查询需顺序读取2800万行数据页,触发约1.2TB的磁盘IO(假设每行200字节,数据页16KB,需读取约75万次IO)。而一台标配SSD的MySQL服务器,物理IO吞吐极限通常在1.5~2GB/s。这意味着单条SQL就能持续占用磁盘带宽近10分钟,期间所有其他查询的IO请求全部排队——这就是“超级慢”的底层物理原因:它不是让CPU忙,而是把IO通道彻底焊死。
提示:判断全表扫描是否构成威胁,不能只看
EXPLAIN的rows,必须结合data_length(表数据大小)和avg_row_length(平均行长度)估算实际IO量。公式:预估IO量(GB)≈rows × avg_row_length ÷ 1024 ÷ 1024 ÷ 1024。当结果>0.5GB且并发>3时,即属高危。
2.2 “超级慢”的三大加速器:让普通全表扫描升级为服务雪崩
一条全表扫描SQL要真正拖垮MySQL,往往需要三个“帮凶”同时在线。我在复盘报告中将它们称为“雪崩三角”:
第一,连接池失控。应用层未设置查询超时(如Java的socketTimeout或queryTimeout),导致一个慢查询独占连接长达15分钟。而主流连接池(如HikariCP)默认最大连接数为20,当5个这样的慢查询并发时,所有连接被锁死,新请求直接返回Connection refused。更致命的是,很多框架(如Spring Boot早期版本)在连接获取失败时会无限重试,形成指数级请求堆积。
第二,锁等待链式反应。InnoDB虽是行锁,但全表扫描过程中会对扫描到的每一行加S锁(共享锁)。当另一条UPDATE语句试图修改同一行时,必须等待S锁释放。而全表扫描的S锁释放时机是“查询结束”,不是“扫描到该行时”。于是,一条慢查询会像磁铁一样,把所有试图修改表中任意数据的事务都吸住。我们在一次故障中抓取到INFORMATION_SCHEMA.INNODB_TRX,发现23个事务处于LOCK WAIT状态,其中21个都在等同一个SELECT事务释放锁——这就是典型的“锁等待风暴”。
第三,内存与CPU的双重挤压。全表扫描需将大量数据页加载进Buffer Pool。当innodb_buffer_pool_size设为16GB(常见配置)时,2800万行数据远超缓存容量,导致频繁的LRU淘汰和磁盘换入换出。同时,MySQL解析2800万行数据、执行WHERE条件过滤、聚合COUNT DISTINCT,消耗大量CPU。我们用perf top抓取故障时CPU热点,发现row_search_for_mysql函数占比达68%,这是InnoDB引擎层最核心的数据检索函数——它正在被一条SQL疯狂调用。
这三个因素叠加,就完成了从“单条慢SQL”到“服务级故障”的质变:IO打满 → Buffer Pool失效 → CPU飙升 → 连接池枯竭 → 锁等待蔓延 → 所有业务请求超时。整个过程在3分钟内完成,比监控告警的默认阈值(5分钟)还快。
2.3 深层根因:为什么这类SQL总能绕过所有防线?
如果把数据库比作一座城,那么索引是城墙,慢查询日志是哨兵,监控告警是烽火台。但“超级慢全表扫描”总能溜进来,根本原因在于三道防线的系统性失效:
防线一:开发阶段的“本地验证幻觉”。开发者在本地MySQL(数据量<1万行)或测试环境(数据量<10万行)运行上述SQL,耗时0.02秒,EXPLAIN显示type: index(走了主键索引),便认定“没问题”。他们不知道,测试库的innodb_buffer_pool_size可能只有128MB,而生产库是16GB;更不知道,测试数据的event_type分布是均匀的,而生产数据中click_banner占比92%,导致索引选择性失效。这种“小数据量验证通过=生产安全”的思维,是最大的认知陷阱。
防线二:CI/CD流水线的“静态检查盲区”。目前主流SQL审核工具(如SOAR、Archery)能检测SELECT *、缺失WHERE、无索引等硬伤,但对“联合索引字段顺序是否支持范围查询”“索引选择性是否低于阈值(如<5%)”“查询是否触发隐式类型转换”等动态语义问题,基本无能为力。我们曾用SOAR扫描同一条SQL,结果为“审核通过”,因为它只看到WHERE子句有字段,没看到event_time作为范围条件放在联合索引末尾的致命缺陷。
防线三:DBA监控的“阈值麻痹症”。DBA通常设置慢查询阈值为long_query_time = 1s,认为“1秒以上的SQL才值得关注”。但对全表扫描而言,1秒只是冰山一角。当表增长到5000万行时,同一条SQL可能耗时8秒——它早已超过阈值,却被归类为“已知慢查”,不再触发深度分析。更严重的是,监控系统只采集Query_time,不采集Lock_time、Rows_examined、Rows_sent等关键指标。一条Query_time=0.8s但Rows_examined=28000000的SQL,会被视为“性能尚可”,而它正是那个随时引爆的炸弹。
这三道防线的失效,不是技术漏洞,而是流程断层:开发关注功能实现,测试关注接口通路,DBA关注硬件指标,没人对“单条SQL在生产数据规模下的资源消耗全景”负最终责任。
3. 实战防御体系:从SQL编写到线上监控的七道关卡
3.1 关卡一:开发侧——用“三问法则”替代盲目写SQL
我给团队制定的硬性规范:任何涉及WHERE条件超过2个字段、或含LIKE '%xxx'、或含ORDER BY非索引字段的SQL,必须手写回答以下三个问题,并附在PR描述中:
第一问:这条SQL在生产最大数据量下,预计扫描多少行?
计算方法:登录生产库(只读账号),执行SELECT TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_SCHEMA='db_name' AND TABLE_NAME='table_name';获取TABLE_ROWS,再结合EXPLAIN的key_len和ref字段,估算实际扫描比例。例如,若TABLE_ROWS=28000000,EXPLAIN显示key_len=0(未用索引),则答案必为“2800万行”。若答案是“不确定”或“应该不多”,此PR直接驳回。
第二问:扫描这些行,会产生多少磁盘IO?
使用前述公式:rows × avg_row_length ÷ 1024 ÷ 1024 ÷ 1024。要求结果必须<0.1GB(即100MB)。这是硬性红线,因为0.1GB IO在SSD上约需0.5秒,留出足够缓冲空间给其他查询。我们曾因此否决了一个“统计昨日新增用户地域分布”的报表SQL,因其在用户表(3亿行)上全表扫描,预估IO达4.2GB。
第三问:如果并发5次,是否会挤占Buffer Pool超过30%?
计算公式:预估IO量(GB) × 并发数 × 1.2(冗余系数)÷ innodb_buffer_pool_size(GB)。要求结果<0.3。例如,预估IO 0.08GB,并发5次,Buffer Pool 16GB,则0.08×5×1.2÷16=0.03,合规。这个计算强制开发者思考并发影响,而非单次执行。
注意:这三问必须手写,禁止用脚本自动生成。目的是让开发者在敲键盘时,大脑真正过一遍数据量级。我们试行三个月后,新上线SQL导致的慢查事故下降82%。
3.2 关卡二:测试侧——构建“影子数据集”代替空转测试
传统测试库数据量小、分布假,是纵容慢SQL的温床。我们推行“影子数据集”方案:每周日凌晨,用mysqldump --where="id % 100 = 1"从生产库抽取1%的行(保留原始数据分布),导入测试库。关键点在于:
- 强制
--where参数:不用--where="1=1"全量导,确保数据量可控; - 保留
AUTO_INCREMENT值:添加--skip-auto-increment,避免测试插入时ID冲突; - 脱敏处理:用
pt-mysql-binlog实时解析生产binlog,对敏感字段(如手机号、邮箱)做SHA256哈希,再写入测试库。
这样,测试库拥有与生产库完全一致的event_type分布、os_type占比、event_time时间跨度。那条“统计iOS Banner UV”的SQL,在影子库中执行时间从0.02秒飙升至4.7秒,EXPLAIN明确显示type: ALL。测试同学立刻反馈,开发被迫重构——要么加覆盖索引,要么改用异步预计算。影子数据集的成本极低(存储仅生产1%,网络流量在凌晨低峰),但拦截效果立竿见影。
3.3 关卡三:DBA侧——重定义慢查询日志的“黄金字段”
默认的MySQL慢查询日志(slow log)只记录Query_time、Lock_time、Rows_sent、Rows_examined四个字段。但这远远不够。我们通过pt-query-digest的--filter参数,强制日志包含五个黄金字段:
pt-query-digest \ --filter '$event->{Bytes} = $event->{Rows_examined} * $event->{Avg_row_length}' \ --filter '$event->{IO_GB} = $event->{Bytes} / (1024**3)' \ --filter '$event->{Concurrency_Risk} = $event->{IO_GB} > 0.1 ? "HIGH" : ($event->{IO_GB} > 0.05 ? "MEDIUM" : "LOW")' \ --filter '$event->{Index_Use} = $event->{key} ? "YES" : "NO"' \ /var/lib/mysql/slow.log生成的报告中,每条慢查都带IO_GB(预估IO量)、Concurrency_Risk(并发风险等级)、Index_Use(是否用索引)。DBA每日晨会只需扫一眼Concurrency_Risk=HIGH的条目,就能精准定位高危SQL。上周,我们靠这个字段提前发现了一条SELECT * FROM product_sku WHERE status=1(商品表1.2亿行),其IO_GB=1.8,立即推动业务方改用ES同步商品状态,规避了潜在故障。
3.4 关卡四:架构侧——用“查询熔断”代替被动告警
监控告警永远滞后于故障。我们借鉴微服务熔断思想,在MySQL接入层(ProxySQL)植入查询熔断逻辑。核心配置如下:
-- 在ProxySQL中创建查询规则 INSERT INTO mysql_query_rules (active, match_digest, destination_hostgroup, apply, sticky_conn) VALUES (1, '^SELECT.*FROM.*user_behavior_log.*WHERE.*event_time', 10, 1, 1); -- 为该规则设置熔断阈值 INSERT INTO mysql_query_rules_fast_routing (username, schemaname, flagIN, destination_hostgroup, comment) VALUES ('app_user', 'prod_db', 0, 10, 'behavior_log_read'); -- 配置HostGroup 10的熔断参数 UPDATE mysql_servers SET max_connections=50 WHERE hostgroup_id=10; UPDATE mysql_servers SET max_replication_lag=10 WHERE hostgroup_id=10;更关键的是,我们编写了Lua脚本,实时分析stats_mysql_query_rules表中的hits(命中次数)和mysql_server_max_connections(连接上限)。当某条规则在1分钟内hits > 10且max_connections使用率>80%时,ProxySQL自动将该规则active设为0,并向企业微信机器人发送告警:“行为日志表查询熔断触发,已屏蔽规则ID 123,原因:1分钟内命中12次,连接池使用率89%”。熔断不是拒绝服务,而是将高危查询路由到只读备库(延迟容忍≤5秒),并降级返回缓存数据。这招让我们在最近两次大促中,成功将“超级慢SQL”导致的P99延迟从12秒压到450ms。
3.5 关卡五:运维侧——建立“故障注入清单”驱动压测
我们不再满足于“TPS达标”的压测报告,而是制定《MySQL故障注入清单》,强制每次大促前执行:
| 故障类型 | 注入方式 | 预期现象 | 验证标准 |
|---|---|---|---|
| 全表扫描IO打满 | SELECT SLEEP(1) FROM large_table | 磁盘IO% ≥95%,其他查询延迟↑300% | 监控平台告警触发,且自动扩容IO节点 |
| 连接池耗尽 | while true; do mysql -e "SELECT 1"; done | Threads_connected≥ max_connections | 应用层返回Too many connections,且自动重启连接池 |
| 锁等待风暴 | UPDATE large_table SET x=x+1 WHERE id<1000000+ 并发SELECT ... FOR UPDATE | innodb_row_lock_waits激增 | APM系统捕获Lock wait timeout exceeded异常率<0.1% |
这份清单由DBA、SRE、开发三方签字确认。去年双11前,我们按清单注入“全表扫描IO打满”,发现监控告警延迟了7分钟(因采样间隔设为30秒),立即调整为10秒。这种“主动找茬”,比被动救火有价值百倍。
4. 故障现场还原:一次真实雪崩的15分钟全记录
4.1 时间线:从第一条慢查到服务恢复的完整脉络
为彻底理解“超级慢SQL”如何摧毁系统,我整理了上月18日21:43发生的故障全程(已脱敏,时间精确到秒):
21:43:07:运营同学提交报表需求:“导出近30天所有支付成功的订单ID及金额”。开发A写出SQL:
SELECT order_id, amount FROM orders WHERE status='paid' AND create_time >= '2024-04-18'。订单表orders有1.8亿行,status字段无索引(历史原因),create_time有单列索引但未覆盖status。21:43:12:该SQL首次执行,
EXPLAIN显示type: index(走了create_time索引),耗时0.03秒。开发A认为“有索引,很安全”,合并代码。21:45:22:报表系统定时任务启动,每分钟执行一次。第一次执行因
create_time索引范围过大(30天数据占全表72%),实际扫描1.3亿行,耗时142秒,Rows_examined=130000000。21:46:05:
Threads_connected从12飙升至48(连接池上限50),Threads_running稳定在45(几乎全是Sleep状态)。iostat -x 1显示%util持续98%,r/s达12000(SSD极限)。21:47:18:首个业务接口超时告警。
SHOW PROCESSLIST中,45个连接里有38个状态为Sending data,Time列显示120~140秒。其余7个为Locked,Info列显示UPDATE order SET status='shipped' WHERE order_id=?。21:48:33:
INFORMATION_SCHEMA.INNODB_TRX中trx_state='LOCK WAIT'的事务数达29个,平均等待时间112秒。SHOW ENGINE INNODB STATUS的TRANSACTIONS部分显示“*** (1) WAITING FOR THIS LOCK TO BE GRANTED:”指向那条SELECT事务。21:49:50:DBA执行
KILL QUERY <select_thread_id>,但因该查询已进入IO密集阶段,KILL命令需等待当前数据页读取完成,耗时47秒才生效。21:50:37:
SELECT被杀,Threads_connected开始回落,但Threads_running仍有22个Locked状态。innodb_row_lock_waits计数器激增。21:52:15:最后一个
Locked事务超时回滚,Threads_running归零。iostat显示%util降至12%。21:53:00:业务接口P99延迟从200ms回落至220ms,系统恢复正常。
整个过程历时9分53秒,但对用户而言,就是近10分钟的支付失败。而根源,就是那条看似“有索引”的SELECT——它用create_time索引做了范围扫描,却因status='paid'过滤条件无法下推,导致1.3亿行数据被读入内存再逐行过滤。
4.2 关键证据链:五份日志如何交叉印证故障根因
单一日志只能看到碎片,必须用五份日志拼出完整图谱:
1. MySQL慢查询日志(slow.log)
# Time: 2024-05-18T21:45:22.123456Z # User@Host: app[app] @ [10.0.1.100] Id: 12345 # Query_time: 142.345678 Lock_time: 0.000123 Rows_sent: 1200000 Rows_examined: 130000000 SET timestamp=1716040522; SELECT order_id, amount FROM orders WHERE status='paid' AND create_time >= '2024-04-18';→ 关键证据:Rows_examined=130000000,证明全表扫描规模;Query_time=142s,远超阈值。
2. ProxySQL访问日志(access.log)
2024-05-18 21:45:22.123 INFO 10.0.1.100:54321 -> 10.0.2.50:3306 SELECT order_id, amount FROM orders ... 2024-05-18 21:45:22.123 WARN rule_id=789 hit_count=12 in_last_minute=12→ 关键证据:hit_count=12,证明该SQL在1分钟内被调用12次,是定时任务触发。
3. Linux系统日志(/var/log/messages)
May 18 21:46:05 db-server kernel: [123456.789] iostat: sda: I/O error, dev 8:0, sector 123456789 May 18 21:46:05 db-server kernel: [123456.789] sd 0:0:0:0: [sda] tag#0 FAILED Result: hostbyte=DID_OK driverbyte=DRIVER_OK→ 关键证据:I/O error,证明SSD因持续高负载出现硬件级错误,这是IO打满的物理证据。
4. 应用APM日志(SkyWalking)
2024-05-18 21:47:18.123 ERROR [OrderService, 123456789, 987654321, false] JDBC execute failed: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure→ 关键证据:Communications link failure,证明应用层因连接池耗尽无法获取连接。
5. InnoDB状态日志(SHOW ENGINE INNODB STATUS)
---TRANSACTION 42123456789, ACTIVE 112 sec starting index read mysql tables in use 1, locked 0 LOCK WAIT 29 lock struct(s), heap size 3240, 29 row lock(s) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 4567 n bits 784 index PRIMARY of table `prod_db`.`orders` trx id 42123456789 lock_mode X locks rec but not gap waiting→ 关键证据:LOCK WAIT 29 lock struct(s),直接锁定故障是由锁等待引发。
这五份日志,像五块拼图,缺一不可。只看慢日志,你会以为只是“一条慢SQL”;只看APM日志,你会归因为“网络问题”;只有交叉比对,才能看清“1.3亿行IO→连接池枯竭→锁等待风暴→服务雪崩”的完整因果链。
4.3 复盘结论:三个必须写入SOP的硬性动作
基于此次故障,我们更新了《MySQL生产事故SOP》,强制要求以下三个动作:
动作一:所有WHERE含时间范围的查询,必须强制指定LIMIT且LIMIT ≤ 1000
理由:时间范围查询极易因数据量增长而失控。LIMIT 1000不是限制业务,而是设置安全阀。报表需求可通过分页或异步导出实现。我们已在ORM层(MyBatis Plus)全局拦截,未指定LIMIT的查询直接抛SQLException。
动作二:SELECT语句禁止出现在事务中(除非明确需要一致性读)
理由:SELECT ... FOR UPDATE或SELECT在REPEATABLE READ隔离级别下会加间隙锁,放大锁等待风险。所有只读查询必须在事务外执行。SRE团队已用pt-query-digest --filter '$event->{In_transaction} && $event->{Command} eq "Query" && $event->{Argument} =~ /^SELECT/'自动扫描,每周邮件通报违规SQL。
动作三:Rows_examined超过TABLE_ROWS × 0.1的SQL,DBA有权立即下线
理由:扫描超10%数据量,意味着索引设计或查询逻辑存在根本缺陷。阈值设为0.1是经验值——低于此值,优化器通常能高效利用索引;高于此值,大概率已退化为全表扫描。该规则已写入DBA值班手册,无需审批,即时执行。
这三个动作,不是技术方案,而是组织纪律。它们把“技术判断”转化为“可执行、可审计、可追责”的流程动作。
5. 经验沉淀:那些文档里不会写的12个实战技巧
5.1 技巧1:用sys.schema_table_statistics快速定位“IO大户”
MySQL 5.7+自带sys库,其中schema_table_statistics视图能直接看到每张表的total_latency(总延迟)和io_read_bytes(读取字节数)。执行:
SELECT table_name, io_read_bytes, io_read_requests, total_latency FROM sys.schema_table_statistics WHERE io_read_bytes > 1024*1024*1024 -- 读取超1GB ORDER BY io_read_bytes DESC LIMIT 5;结果中orders表io_read_bytes=2.3GB,立刻锁定问题表。比翻慢日志快10倍,且不受long_query_time阈值限制。
5.2 技巧2:EXPLAIN FORMAT=JSON里的used_columns是索引命中的金标准
EXPLAIN的key字段只告诉你用了哪个索引,但FORMAT=JSON会告诉你哪些字段真正参与了索引查找。例如:
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE status='paid' AND create_time > '2024-04-18';JSON输出中:
"used_columns": ["status", "create_time"], "key_parts": ["create_time"]→used_columns有status,但key_parts只有create_time,证明status条件是在索引扫描后内存过滤的!这就是全表扫描的伏笔。used_columns与key_parts不一致,是索引失效的铁证。
5.3 技巧3:pt-online-schema-change改表时,务必加--chunk-index参数
给大表加索引时,pt-osc默认用主键分块。但若主键是UUID(无序),会导致每个chunk随机IO,效率极低。加--chunk-index=created_at(用有序时间字段分块),可将加索引时间从8小时缩短至47分钟。我们曾因此避免了一次凌晨加索引导致的业务抖动。
5.4 技巧4:information_schema.PROCESSLIST中Time值>60秒的连接,90%是“僵尸连接”
SHOW PROCESSLIST里Time列代表连接空闲时间(Sleep状态)或查询执行时间。但Time > 60且State='Sleep'的连接,往往是应用层未正确关闭的连接。我们用脚本每5分钟扫描:
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST WHERE COMMAND='Sleep' AND TIME > 60;自动KILL,并邮件通知对应开发负责人。此举将连接池泄漏率降低95%。
5.5 技巧5:innodb_buffer_pool_dump_pct=75,让Buffer Pool“热身”更快
默认innodb_buffer_pool_dump_pct=25,即只保存25%的热点页。设为75后,MySQL重启时能更快加载热点数据。我们实测,大促前执行SET GLOBAL innodb_buffer_pool_dump_now=ON;,再重启,首屏加载时间从8.2秒降至1.4秒。
5.6 技巧6:performance_schema开启events_statements_history_long,故障后回溯“最后十条SQL”
默认events_statements_history_long关闭。开启后(UPDATE performance_schema.setup_consumers SET ENABLED='YES' WHERE NAME='events_statements_history_long';),即使慢日志没捕获,也能从performance_schema.events_statements_history_long查到故障前执行的最后10000条SQL。那次故障中,我们靠它找到了被误删的WHERE status='paid'条件。
5.7 技巧7:pt-query-digest分析时,加--limit=10和--review双保险
--limit=10只分析Top 10慢查,避免分析海量日志耗时;--review将结果存入percona_schema.query_review表,支持跨日对比。我们建了视图:
CREATE VIEW slow_trend AS SELECT DATE(ts) as day, query_id, SUM(count_star) as exec_count, AVG(query_time) as avg_time FROM percona_schema.query_review_history GROUP BY DATE(ts), query_id;每天晨会看exec_count突增的SQL,防患于未然。
5.8 技巧8:SELECT语句加SQL_NO_CACHE,排除Query Cache干扰
MySQL 5.7已废弃Query Cache,但若你还在用5.6,SELECT前加SQL_NO_CACHE可确保EXPLAIN结果反映真实执行计划,避免Cache命中带来的假象。
5.9 技巧9:SHOW PROFILE比EXPLAIN更能定位瓶颈环节
对单条慢SQL,执行SET profiling=1;后运行SQL,再SHOW PROFILES;和SHOW PROFILE FOR QUERY N;。它会显示Copying to tmp table、Sorting result、Sending data各阶段耗时。我们曾发现一条SQL 92%时间花在Sending data,证明是网络传输瓶颈,而非SQL本身问题。
5.10 技巧10:pt-table-checksum校验主从一致性时,加--chunk-size=1000防锁表
默认--chunk-size按数据量计算,可能过大。手动设为1000,确保每个chunk只处理1000行,极大降低主从延迟和锁表风险。校验1.2亿行表,耗时从3小时缩短至42分钟。
5.11 技巧11:information_schema.INNODB_METRICS中lock_row_lock_time_avg是锁健康度晴雨表
监控SELECT * FROM information_schema.INNODB_METRICS WHERE NAME='lock_row_lock_time_avg';。正常值应<1000(微秒)。若持续>5000,说明锁等待严重,需立即排查。我们将其接入Prometheus,阈值告警。