MySQL, PostgreSQL 和 DuckDB 的行定位

💡 原文中文,约3900字,阅读约需10分钟。
📝

内容提要

文章比较了MySQL、PostgreSQL和DuckDB的行定位机制:InnoDB二级索引存储主键,通过回主键B树定位记录,页整理不影响索引;PostgreSQL索引存储heap TID(块号加槽号),可直接定位tuple,页内整理保留槽号,索引无需修改;DuckDB使用int64 rowid经ART索引定位RowGroup内行,但Checkpoint合并RowGroup会改变rowid,需同步修改ART索引。

🔎

延伸解读

为什么 PostgreSQL 需要槽号而 InnoDB 不需要

文章指出,PostgreSQL 的普通 heap 表索引都类似二级索引,主键索引也不例外。如果索引只存页号,回表后仍需遍历页内记录才能找到匹配项。因此 PostgreSQL 的 TID 包含块号和槽号,索引可直接定位 tuple。InnoDB 则不同:记录按主键组织在聚簇树中,二级索引拿到主键后可继续查主键树,到叶子页按主键顺序查找,无需存储具体页内位置。

页内整理对索引的影响差异

PostgreSQL 的 PageRepairFragmentation 压紧 tuple 字节区时保留仍在使用的槽号,移动 tuple 后只更新原槽的 lp_off,索引保存的 TID 继续有效。VACUUM FULL 会重写 heap 并重建索引,但普通 VACUUM 不改变存活 tuple 的 TID。DuckDB 的 Checkpoint 可能合并相邻 RowGroup,合并后部分存活行可能得到新行序号,若索引采用 REMAP 策略,需删除旧 (key, rowid) 再插入新引用。

DuckDB 删除洞与整理调度的限制

DuckDB 的 DELETE 在 RowGroup 版本信息中标记删除,普通 INSERT 从表尾追加,删除位置继续留在原组。Checkpoint 根据相邻候选组的活行数判断能否合并,调度条件是输出组数少于输入组数。一个部分删除的 RowGroup 即使只剩很少活行,单组 1→1 整理也不满足该条件;没有合适邻组时,删除洞会继续保留。全部删除的 RowGroup 走单独丢组路径。

❓

Q&A

InnoDB 的二级索引是如何定位到具体记录的?

InnoDB 的二级索引叶子节点存储的是主键值。查询时,先通过二级索引找到主键,再回主键 B-tree 查找对应的记录。因为主键 B-tree 的叶子页按主键顺序组织记录,所以即使记录因页分裂被搬到其他页,只要主键不变,二级索引仍能通过主键找到它。

PostgreSQL 的 heap TID 由什么组成?它有什么好处?

PostgreSQL 的 heap TID 由 6 字节组成,包括 block number(块号)和 line pointer 槽号。索引叶子项保存 TID,通过 TID 可以直接定位到 heap 页中的具体槽位,进而访问 tuple。这样 heap 页可以独立于索引键顺序组织数据,且页内整理时只要槽号不变,索引就无需修改。

PostgreSQL 在页整理时如何保证索引中的 TID 仍然有效?

PostgreSQL 的 PageRepairFragmentation 会压紧 tuple 字节区,但保留仍在使用的槽号。移动 tuple 后,只更新原槽的 lp_off 指向新位置,索引保存的 TID 继续有效。对于有索引引用的死亡槽,先保留为 LP_DEAD,待 VACUUM 清理索引项后再转为 LP_UNUSED。新插入的记录可以复用中间的空槽,但不会改变其他槽的编号。

DuckDB 的 rowid 是如何生成的?ART 索引如何利用它定位行?

DuckDB 的 rowid 是一个 int64_t,由 RowGroup 的起始行号加上组内行序号生成。ART 索引存储 key 到 rowid 的映射。查询时,通过 ART 找到 rowid,再根据 rowid 确定所在的 RowGroup,计算组内行序号,然后对每列找到对应的 ColumnSegment,按段内行序号解码列值。

为什么 DuckDB 在 Checkpoint 合并 RowGroup 时需要修改 ART 索引?

DuckDB 在 Checkpoint 时可能合并相邻的 RowGroup 以清理删除位置,合并后部分存活行的 rowid 会发生变化。如果表上有 ART 索引,索引中存储的旧 rowid 就不再指向正确的行,因此必须同步修改 ART 索引。DuckDB 可以采用 REMAP 策略,收集改号的行,对每个索引删除旧的 (key, rowid) 并插入新的引用。

PostgreSQL 和 DuckDB 在行定位与页整理上的主要区别是什么?

PostgreSQL 使用 heap TID(块号+槽号)直接定位 tuple,页内整理时保留槽号,因此只要记录未被删除,TID 不变,索引无需修改。DuckDB 使用 rowid 定位行,但 Checkpoint 合并 RowGroup 可能导致 rowid 改变,因此必须同步修改 ART 索引。本质区别在于 PostgreSQL 的页整理不改变行标识,而 DuckDB 的组整理可能改变行标识。

🏷️

标签

➡️

继续阅读