Lance Stable Row ID 深度解析:从一次 BTree 索引 404 说起
这篇文章以一次真实的 BTree 索引 404 事故为切口,系统解释了 Lance stable row ID 的设计原理、数据结构、执行路径与一次关键修复方案。问题背景与核心症状在 songs 表启用 enable_stable_row_ids=true 后,compact_files() + optimize_in…
Lance Stable Row ID 深度解析:从一次 BTree 索引 404 说起 代码版本:基于 lance feat/static-flow 分支(commit 27cfea6)。 本文所有代码路径以 deps/lance/ 为根,即 StaticFlow 项目中 lance 的本地 fork。目录导航 | 节 | 主题 | 关键词 | |----|------|--------| | §1 | 一次神秘的 404 | 问题现象、触发条件 | | §2 | 两种 Row ID 范式 | Address-style vs Stable | | §3 | Stable Row ID 核心数据结构 | RowIdSequence、U64Segment、RowIdIndex | | §4 | 数据流:写入与查询 | assign_row_ids、Take、Scan | | §5 | Compaction 与 Stable Row ID | rechunk、不排序设计、fragment_bitmap | | §6 | UPDATE / merge_insert | CapturedRowIds、编码退化 | | §7 | 索引更新机制 — optimize_indices | 旧数据过滤、unindexed fragments | | §8 | Bug 详解 | row_id >> 32 的致命假设 | | §9 | 修复方案 | OldIndexDataFilter、精确过滤 | | §10 | 设计哲学与未解决问题 | Trade-off、系统性隐患 | ⏭️ 熟悉 Lance 架构的读者可直接跳到 §8 Bug 详解。§1 一次神秘的 404 StaticFlow 是一个 full-stack Rust 的本地写作、知识管理与媒体平台(Axum + Yew + LanceDB)。其音乐库模块使用 LanceDB 的 songs 表存储歌曲元数据,并在 id 列上建了 BTree 标量索引以加速等值查询。 某天,执行完 compact_files() + optimize_indices() 例行维护后,发现了一个诡异的现象:GET /api/music → 200 OK, 返回 256 首歌曲 ✅ GET /api/music/song-42 → 404 Not Found ❌ GET /api/music/song-42/play → 404 Not Found ❌ 列表接口(全表扫描)完全正常,但详情接口(走 BTree 索引等值查询 id = 'song-42')对每首歌都返回 404。 初步排查:全表扫描能找到数据 — 数据本身完好BTree 索引等值查询返回空 — 索引出了问题问题在 compaction + optimize_indices 之后才出现songs 表开启了 enable_stable_row_ids = true 为什么开启 stable row ID?这需要理解 Lance 中行标识符的工作方式。默认模式下,_rowid 编码了行的物理位置(fragment ID + offset),每次 compaction 或 merge_insert 都会导致行搬迁到新 fragment,_rowid 随之改变。而 BTree 索引存储的是 (value, _rowid) 对——一旦 _rowid 变了,索引中的旧条目就指向了错误的位置,必须重建或 remap 整个索引。 Stable row ID 模式则为每行分配一个跨版本不变的逻辑 ID。无论行的物理位置怎么搬迁,ID 值始终不变,索引数据天然有效——无需 remap。songs 表频繁使用 merge_insert 更新歌曲元数据(如播放次数、标签),开启 stable row ID 正是为了避免每次更新后的索引重建开销。 然而,这个 flag 最终成了问题的关键线索。要理解它为何导致索引失效,我们需要先深入理解 Lance 格式中 Row ID 的完整设计。§2 Lance 中的两种 Row ID 范式 Lance 格式中 _rowid 列的值有两种完全不同的语义,这是理解后续所有内容的基础。Address-style Row ID(默认模式) 编码规则:row_id = (fragment_id << 32) | row_offset// rust/lance-core/src/utils/address.rs:37-39 pub fn new_from_parts(fragment_id: u32, row_offset: u32) -> Self { Self((fragment_id as u64) << 32 | row_offse…
正在初始化 WebAssembly 引擎…
首次编译原生模块可能需要数秒
就绪后,页面交互将以接近原生的速度运行