MySQL中单条SQL物理IO页数需通过performance_schema的wait/io/file/innodb/innodb_data_file事件行数统计,每次read()调用对应至少1页磁盘读,而非Handler_read_*或EXPLAIN预估。

怎么看 MySQL 执行一条 SQL 时到底读了多少页(物理 IO)
MySQL 自身不直接暴露单条语句的磁盘页读取数,EXPLAIN 只给预估,SHOW PROFILE 已被弃用,真要抓实时物理 IO,得靠 performance_schema 的底层事件。核心是盯住 events_waits_history_long 或实时聚合表 events_waits_summary_by_thread_by_event_name,过滤出 wait/io/file/innodb/innodb_data_file 和 wait/io/file/innodb/innodb_log_file 这两类等待。
实操建议:
- 先确保
performance_schema已启用(SELECT @@performance_schema返回 1),且相关消费者已打开:UPDATE performance_schema.setup_consumers SET ENABLED = 'YES' WHERE NAME LIKE 'events_waits%'; - 对目标线程(比如刚执行完 SQL 的连接 ID)查历史等待:
SELECT EVENT_NAME, SOURCE, TIMER_WAIT FROM performance_schema.events_waits_history_long WHERE THREAD_ID = (SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_ID = CONNECTION_ID()) AND EVENT_NAME LIKE 'wait/io/file/innodb/%' ORDER BY TIMER_START DESC LIMIT 20; -
TIMER_WAIT是皮秒单位,除以 1000000000 得到毫秒;真正反映“读盘次数”的是匹配行数,不是耗时——每次read()系统调用触发一次该事件,对应至少一页(16KB)的物理读(除非部分页缓存失效后又重读)
为什么 Handler_read_* 状态变量不能代表磁盘 IO
Handler_read_first、Handler_read_key、Handler_read_next 这些值统计的是存储引擎接口调用次数,完全在内存中发生。哪怕所有数据都在 innodb_buffer_pool 里,它们也会涨;反过来,如果 Buffer Pool 命中率低,这些值可能很小,但磁盘 IO 却很高——因为大量页要从磁盘载入。
常见错误现象:
- 看到
Handler_read_rnd_next高,就断定“SQL 在扫磁盘”,其实它只表示“按行指针随机取下一行”,取的可能是刚从磁盘读进来的页里的下一行,也可能是 Buffer Pool 里早已存在的页 - 监控大盘显示
Innodb_buffer_pool_reads每秒几十次,但Handler_read_*几乎为 0——这说明查询逻辑简单、遍历少,但 Buffer Pool 不够,每页都要单独从磁盘拉,IO 密集型瓶颈藏在这里
innodb_buffer_pool_size 设小了,IO 开销会怎么跳变
Buffer Pool 不足时,InnoDB 被迫频繁淘汰页、再从磁盘加载新页,导致 Innodb_buffer_pool_reads 暴涨,同时 Innodb_buffer_pool_wait_free 可能非零(表示前台线程等后台刷脏页腾空间)。这不是线性增长,而常呈阶梯式恶化:一旦活跃数据集超过 Buffer Pool 容量,命中率跌破 85%,IO 请求会指数级上升。
关键参数差异:
-
Innodb_buffer_pool_reads:真正从磁盘读取的页数(每次 read() 调用算一次,不管读多少字节) -
Innodb_buffer_pool_read_requests:逻辑读请求总数(含 Buffer Pool 命中) - 二者比值
Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests就是物理读比例,线上应长期低于 0.01(即 1%) - 注意
innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances必须整除innodb_buffer_pool_size,否则 MySQL 会静默向下取整,实际分配比你设的小——这是容易被忽略的配置陷阱
用 iotop -p $(pgrep mysqld) 辅助交叉验证是否真在读盘
Linux 层的 iotop 能看到 mysqld 进程实时的磁盘读写速率和 IOPS,但它无法区分是读表数据、读日志、还是刷脏页。所以它不能替代 performance_schema 定位具体 SQL,但能快速回答一个关键问题:“现在这台库是不是正在扛真实磁盘压力?”
使用场景:
- 执行一条
SELECT COUNT(*) FROM huge_table后,iotop显示 mysqld 持续 20MB/s 读,而Innodb_buffer_pool_reads每秒涨几百——基本确认在扫盘 - 如果
iotop几乎没读,但SHOW ENGINE INNODB STATUS里FILE I/O部分显示大量pending normal aio reads,那可能是异步 IO 队列积压,或文件系统层(如 ext4 + barrier)拖慢了提交 - 注意:容器环境需加
--cap-add=SYS_ADMIN才能让iotop正常工作,否则看不到进程级 IO 统计
真正难的不是查哪条 SQL 触发了 IO,而是判断这些 IO 是必要开销(比如首次加载热数据),还是可优化浪费(比如没走索引导致全表扫描、Buffer Pool 配太小、或 innodb_random_read_ahead 误触发预读)。这些没法靠一个命令自动识别,得结合执行计划、数据分布、缓存状态一起看。










