MySQL执行SELECT时,SQL层通过handler接口(如rnd_next)委托InnoDB读取数据,TableScanIterator调用Read()触发ha_innobase::rnd_next(),InnoDB用游标维护位置并将整行数据拷贝至buf,锁与事务上下文需在handler调用前正确绑定。

MySQL 执行一条 SELECT 时,SQL 层不会直接碰磁盘或 B+ 树,而是通过统一的 handler 接口把“读下一行”这种抽象动作,委托给 InnoDB 实现。这个过程不是函数调用链,而是一层明确的契约——handler 是 SQL 层和存储引擎之间的唯一合法通道。
TableScanIterator 怎么触发 ha_rnd_next?
在 MySQL 8.0 的 Iterator 执行模型中,最底层的扫描行为由 TableScanIterator 承担。它不关心数据在哪、怎么存,只负责反复调用 Read()。每次调用,内部就执行一次 handler::rnd_next(buf) —— 这个 rnd_next 是 handler 抽象类定义的虚函数,InnoDB 在 ha_innobase::rnd_next() 中具体实现。
关键点:
-
rnd_next不是随机读,而是“顺序读下一行”,InnoDB 用内部游标(m_cursor)维护当前扫描位置 -
buf是 SQL 层预分配的一块内存,InnoDB 直接把整行数据(包括隐藏列、事务ID、回滚指针等)按行格式拷贝进去 - 如果表没主键,InnoDB 用隐式自增列作为聚簇索引键,
rnd_next仍走 B+ 树叶子节点遍历
为什么 index_read 不总比 rnd_next 快?
很多人以为加了索引就一定走 index_read,其实不然。是否调用 index_read 取决于执行计划是否生成了 IndexScanIterator,而这又取决于优化器是否选择了索引访问路径(Access Path)。
常见误判场景:
- 查询条件含函数或类型隐式转换,导致索引失效,退化为
TableScanIterator+rnd_next - 覆盖索引查询时,
IndexScanIterator::Read()调用的是handler::index_read_idx或index_read,但只填充索引列,不回表 - 二级索引 + 回表场景:先
index_read拿到主键值,再用该主键调handler::index_read(聚簇索引查找),两次 handler 调用
update_row 和锁是怎么绑定的?
InnoDB 的行锁不是单独加的,而是嵌在 update_row、delete_row 等 handler 方法内部。当你执行 UPDATE t SET x=1 WHERE id=5,SQL 层先用 rnd_next 或 index_read 定位到行,再调 handler::update_row,此时 InnoDB 才真正申请记录锁(record lock)或间隙锁(gap lock)。
注意几个硬约束:
- 没有索引的
WHERE条件 → 全表扫描 + 每行都尝试加锁 → 最终升级为表锁(LOCK_TABLE) -
SELECT ... FOR UPDATE不会调update_row,但它会提前在index_read或rnd_next阶段加锁(通过lock_clust_rec_cons_read_sees等内部逻辑) - 锁的粒度完全由索引决定:主键查 → 记录锁;范围查 → 间隙锁;无索引 → 表锁
真正容易被忽略的是:handler 接口调用本身不带事务上下文,thd(线程描述符)和 trx(InnoDB 事务对象)必须在进入 ha_innobase 方法前就已关联好。一旦 handler::start_stmt 没正确触发,后续所有 rnd_next 或 update_row 都可能读到错误版本或漏加锁——这种问题在线上往往表现为偶发性幻读或更新丢失,而不是报错。


















