ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Lance 表格式中的 Data Overlay Files:不重写基文件的低成本单元格级更新机制

Lance 表格式中的 Data Overlay Files:不重写基文件的低成本单元格级更新机制 Lance 表格式中的 Data Overlay Files不重写基文件的低成本单元格级更新机制【免费下载链接】lanceOpen Lakehouse Format for Multimodal AI. Convert from Parquet in 2 lines of code for 100x faster random access, vector index, and data versioning. Compatible with Pandas, DuckDB, Polars, Pyarrow, and PyTorch with more integrations coming..项目地址: https://gitcode.com/GitHub_Trending/la/lanceData Overlay Files数据覆盖文件是 Lance 表格式中用于就地修改 fragment 内部分(物理行偏移, 字段)单元格的机制它通过追加一个只携带变化单元格的小文件避免重写整列或将行迁移到新 fragment从而让仅涉及少量行/列的小幅更新变得廉价。读完本文你将完整掌握 overlay 的覆盖coverage与解析resolution规则、稠密与稀疏两种形态、committed_version驱动的版本排序、与索引的排除集exclusion set协作方式、两种压缩模式以及一个贯穿其生命周期的 6 步工作示例。该特性目前仍处于实验阶段详见下文“特性开关与现状”一节。定位Lance 第三种就地数据修改机制Lance 表格式以不可变 fragment 版本化 manifest 组织数据整体结构见 Table format overview。在 overlay 之前就地修改数据已有两种机制删除文件Deletion Files移除整行参见 deletion files数据演化Data Evolution为整列添加或重写值参见 data evolution。而overlay 改变的是单个单元格cell——即(row offset, field)的交叉点。它“补充”新值而不触碰 fragment 的基数据文件base data files因此当只有一小部分行和/或列发生变化时更新成本与变化量成正比而非与列宽或行数成正比。⚠️实验性特性该特性目前为实验性尚未在任何已发布库版本中正式支持见docs/src/format/table/data_overlay_file.md顶部警告。特性开关与现状feature flag 64overlay 文件依赖 manifest 中的feature flag 641 6data overlay files。不理解 overlay 的读写方必须拒绝使用该特性的数据集——因为静默忽略 overlay 会返回过期的基值这是正确性 bug 而非体验降级。在仓库源码protos/table.proto中该 flag 的定义为// * 1 6: data overlay files are present (see DataOverlayFile). Readers that do // not understand overlays must refuse the dataset, since ignoring an overlay // would silently return stale base values.对应 Rust 侧实现位于 feature_flags.rsFLAG_UNSTABLE_DATA_OVERLAY_FILES: u64 1 6。该文件还揭示了当前实现的“未发布”状态release 构建默认把该 flag 当作未知即发布版读写方会拒绝 overlay 数据集除非设置环境变量LANCE_ENABLE_UNSTABLE_DATA_OVERLAY_FILES以允许基准测试等场景显式开启debug 构建则始终理解该 flag。当 manifest 中任一 fragment 携带 overlay 时reader_feature_flags与writer_feature_flags都会置位该 bit。核心概念覆盖Coverage与解析Resolution覆盖位图与物理偏移每个 overlay 通过一个覆盖位图coverage bitmap声明自己提供了哪些单元格的值稀疏 overlay 则是每个字段一个位图。位图索引的是物理行偏移physical row offsets——即基数据文件中的行位置包含已删除的行并且即使删除向量deletion vectors变化也保持稳定。读取时解析单元格(offset, field)的规则是按 newest → oldest 的顺序遍历 fragment 的 overlay第一个覆盖(offset, field)的 overlay 胜出取其值若没有任何 overlay 覆盖该单元格则回退到基数据文件若基文件中没有该字段则返回NULL。overlay 之间的优先级由两点决定与 DataOverlayFile protobuf 中的committed_version注释一致committed_version——更高者胜出见 版本与排序在DataFragment.overlays中的位置作为并列时的决胜规则——列表中靠后的条目更新后写胜出。在protos/table.proto中DataFragment的overlays字段编号 11对此有明文注释“Order is significant: a later entry is newer than an earlier one. When two overlays cover the same (offset, field) and share acommitted_version, the later entry wins.”Rust 侧对“newest-last”顺序有双重保障加载时sort_overlays_newest_last按committed_version稳定排序稳定排序保留了列表位置作为相等版本的决胜依据写入侧则有verify_overlays_newest_last校验见 overlay.rs拒绝任何乱序组装 overlay 的提交路径并给出错误信息overlay files must be stored newest-last, but committed_version X precedes Y。覆盖决定适用性而非 NULL一个关键语义被覆盖的偏移其值为NULL时将该单元格覆盖为NULL这与“偏移根本不在位图中”截然不同——后者回退到基值。也就是说是覆盖coverage而非值是否为 NULL决定 overlay 是否生效。这一点在 overlay writer 的模块文档中被明确强调为“调用方必须满足、写入方必须强制”的不变式之一Coverage is not value-nullness.A covered offset holding NULL overrides that celltoNULL; an offset absent from the coverage falls through to the base value. Passing a null to mean leave this cell alone silently erases live data.提交路径和读取路径都无法事后捕获这一违规build_manifest只检查目标 fragment 存在、overlay 有序因此这是写入方职责所在。与删除Deletions的交互删除优先于 overlay。如果某行偏移在 fragment 的删除文件中被标记为删除那么该偏移上的任何 overlay 值都是“死”的、被忽略无论提交顺序如何。在protos/table.proto的DataOverlayFile注释中同样有表述“Because deletions take precedence over overlays, an overlay value for an offset that is also marked deleted is dead and is ignored.”物理布局基于秩Rank的取值无偏移键列overlay 的数据文件按data_file.fields的顺序每个字段存储一列值value column并且不存储行偏移键列row-offset key column。被覆盖偏移的值在其列中的位置等于该偏移在字段覆盖位图中的秩rank——即该偏移之下已置位的位数。因此解析一个单元格 一次 rank 查找 一次取值无需存储或搜索单独的偏移列。由于不同字段可能覆盖不同的偏移集合单个稀疏 overlay 的各值列长度可以不同。Lance 文件格式允许同一文件内各列条目数不同因此稀疏 overlay 可以表示为单个文件关于 writer 支持现状见 Writer 支持。从 overlay.rs 的模块文档可印证整套不变式物理偏移覆盖覆盖位图索引物理行偏移含删除行随删除向量变化保持稳定基于秩的值data_file每字段一列值无偏移键列覆盖偏移的值位于其在字段覆盖位图中的秩处稠密 vs 稀疏稠密 overlay 所有字段共享一个位图OverlayCoverage::Shared稀疏 overlay 每字段一个位图OverlayCoverage::PerField一次解析位图在 fragment 加载时从 32 位 Roaring 编码解析一次置于Arc之后共享克隆 fragment 因此廉价newest-last 排序fragment 的 overlay 按 newest-last 存储加载时按committed_version稳定排序列表位置作为相等版本的决胜依据字段墓碑tombstones当某字段写入新的基值DataReplacement 或就地列重写时该字段的旧 overlay 值过时、不得再遮蔽新基值字段在 overlay 的data_file.fields中以TOMBSTONE_FIELD_ID即-2与过时基列使用的哨兵一致标记为废弃而非物理删除从而保持 overlay 的其他字段及覆盖位置不变见tombstone_overlay_fields。稠密Dense与稀疏Sparse两种形态单个 overlay 属于两种形态之一稠密矩形Dense一个shared_offset_bitmap应用于每个字段所有被覆盖偏移在每个字段上都有值。这是普通UPDATE的常见情形——一条SET列表作用于同一组行。稀疏SparseFieldCoverage为每个字段携带一个位图用于不同字段覆盖不同偏移集合的场景——例如带多个WHEN MATCHED分支的MERGE不同行更新不同列。若改用稠密 overlay就必须扩张到边界矩形bounding rectangle并用当前值后像post-images填充未触及的单元格对于 embedding 这类宽列意味着重新存储未变化的数据稀疏 overlay 则只存储真正变化的单元格。在protos/table.proto中DataOverlayFile的coverage字段是一个oneofoneof coverage { // A single 32-bit Roaring bitmap of physical row offsets that applies to // every field in data_file.fields (a dense / rectangular overlay). bytes shared_offset_bitmap 2; // Per-field coverage for a sparse overlay. FieldCoverage field_coverage 4; }而FieldCoverage定义为按data_file.fields顺序每字段一个 32 位 Roaring 位图message FieldCoverage { // One entry per field in the overlays data_file.fields, in the same order. // An offset present in a fields bitmap but mapped to a NULL value // means the cell is overridden to NULL (distinct from an offset that is absent, // which falls through to the base data file). repeated bytes offset_bitmaps 1; }Rust 侧的对应枚举 OverlayCoverage 提供了dense(bitmap)与sparse(bitmaps)两个构造方法并在序列化时对位图调用optimize()后写出对连续大区间可压缩到原始体积的 1% 以下见其测试test_coverage_bitmap_serialized_run_optimized。Protobuf 定义下面给出 overlay 的两个核心 protobuf 消息的字段语义完整定义位于 table.proto 的DataOverlayFileL548与FieldCoverageL587// An overlay file supplies new values for a subset of (row offset, field) cells // within a fragment, without rewriting the fragments base data files. message DataOverlayFile { // The data file storing the overlays new cell values, one value column per // field in data_file.fields. No row-offset key column is stored. DataFile data_file 1; // Which (offset, field) cells this overlay provides values for. oneof coverage { bytes shared_offset_bitmap 2; // dense单一位图应用于所有字段 FieldCoverage field_coverage 4; // sparse每字段一个位图 } // The dataset version at which this overlay became effective: the version of // the commit that introduced it, NOT the version it was read from. uint64 committed_version 3; } message FieldCoverage { repeated bytes offset_bitmaps 1; // 按 data_file.fields 顺序每字段一个 Roaring 位图 }其中committed_version驱动两个排序对索引——dataset_version committed_version的索引已纳入该 overlay否则覆盖单元格须从索引结果中排除并重估。对其他 overlay——覆盖同一(offset, field)时committed_version高者胜出并列时按DataFragment.overlays列表位置靠后更新者胜出。DataOverlayFile的 Rust 结构体overlay.rs在从 protobuf 反序列化时会把位图一次性解析为RoaringBitmap并拒绝缺失data_file或缺失coverage的消息见其测试test_data_overlay_missing_fields_error。版本与排序committed_versionOverlay复用数据集版本作为排序时钟而不是引入独立的代数计数器。committed_version是 overlay生效became effective时的数据集版本——即引入它的那次提交的版本不是它被读取时的版本。它在提交时盖章若提交重试则重新盖章与 created-at / last-updated-at 版本序列的方式一致。这一个值驱动所有排序决策Overlay vs. Overlay读取优先级committed_version更高者胜出。Overlay vs. Index查询正确性索引记录其构建时的dataset_version。若索引的dataset_version committed_version则该索引已纳入该 overlay若 overlay 的committed_version index.dataset_version则 overlay 比索引新其单元格必须从索引结果中排除并重新求值。调度器信号overlay 的committed_version与索引dataset_version之间的差距或 overlay 与基文件之间的差距是陈旧度staleness度量压缩调度器可以利用它。为什么是“生效版本”而非“读取版本”考虑一个场景overlay 读取版本 5在版本 6 提交一个索引读取版本 5在 overlay 之前构建在版本 7 提交且dataset_version 5。若 overlay 存储的是其读取版本5则测试5 5为假该行不会被排除而该索引从未见过 overlay会返回过期结果。存储生效版本6后6 5为真该单元格被排除并重新求值结果正确。committed_version的注释在protos/table.proto与 DataOverlay 事务 一节中均有对应表述。索引集成基于排除集Exclusion Set的查询时调和对带有 overlay 的 fragment 构建索引不需要将 fragment 从索引覆盖中移除。fragment 保持被索引状态查询路径在查询时通过排除集调和 overlay。对字段F上的索引其排除集 所有满足committed_version index.dataset_version的 overlay 的覆盖位图限制到字段F后的并集。排除是**字段感知field-aware**的只触及无关列的 overlay 不会从F的索引中排除任何东西。其中F覆盖索引fields中的每个字段而不仅是作为索引键的字段。携带非键列covering columns见 serving carried columns的索引同样依赖这些列更新一个仅携带carried列会使键值保持正确但携带值过期因此这些行必须被同等排除。查询流程如下照常执行索引搜索产生候选行移除位于排除集中的任何候选其索引值可能已过期重新求值被排除的行对应当前值——复用 fragment 未索引尾部unindexed tail已有的 flat 路径。对标量谓词重新应用过滤器对向量查询则用当前向量重新打分。仍然匹配的行被加回结果。第 3 步正是“排除”之所以正确而非仅仅“安全”的关键不重新求值就从索引候选中移除某行会静默丢掉一个本应在新值下匹配的行。排除始终充分sufficient因为一次写入只会通过追加 overlay 改变单元格而该 overlay 的committed_version引入它的提交的版本必然超过任何既有索引的dataset_version所以写入改变的每个单元格都保证落在该索引的排除集中。压缩只有在没有索引仍依赖某 overlay 进行排除时才能移除它见 压缩。源码级实现staleness 模块排除集计算实现在 staleness.rs核心函数overlay_exclusion_offsets只依赖 fragment 与索引元数据overlay 覆盖位图、committed_version、索引字段 id因此被放在读取路径之外版本门控committed_version index_version的 overlay 已被索引纳入直接跳过stale_offsets_for_fragment先做一次廉价的整体版本门控若所有 overlay 都早于索引版本则整段跳过字段/位图工作字段感知与嵌套通过 schema 的字段谱系field ancestry判断 overlay 字段与索引字段的祖先/后代关系从而对嵌套结构如outer.middle.a也能正确判定“是否影响该索引”见其测试test_exclusion_offsets_matches_nested_fields两级粒度collect_overlay_stale_frags以 fragment 粒度标记陈旧 fragmentcollect_overlay_stale_rows_for_segment则以行级粒度精确计算每个 fragment 内的陈旧行偏移供标量与向量路径“只封锁受影响行、只重估这些行”使开销与 overlay 行数成正比而非 fragment 大小覆盖语义overlaid_fragments只收集真正携带 overlay 的 fragment罕见常见路径下为空表调用方可直接跳过索引加载缺失fragment_bitmap的旧版索引被当作覆盖所有 fragment匹配DatasetPreFilter::new防止 overlay 过期行漏网。相关集成测试见 dataset_overlay_index_masking.rs。压缩CompactionOverlay 会累积读取成本——每个 overlay 都是一个需要测试的位图、一个可能打开的文件、以及交织值所需的额外工作。压缩在两种模式下限制这一成本Overlay → Overlay将若干 overlay 合并为更少的 overlay按 newest-first 遍历被合并的 overlay 计算每个(offset, field)的后像post-image。合并后的 overlay 取其输入的最大committed_version从而保留排除语义。被合并的 overlay 必须在committed_version上连续——例如有 v10、v30、v50 三个 overlay 时不能只合并 v10 和 v50因为给结果盖上 v50 会错误地把 v10 的值提升到中间的 v30 之上对 v30 也覆盖的单元格而言。索引仍可复用但可能现在需要排除更多行。此模式写入廉价不触碰基文件。Overlay → Base将 overlay 折叠进全新的基数据文件为每个被覆盖的单元格计算后像然后清空 overlays。此时基文件完整每个后像都有良好定义。overlay 偏移是物理的因此无法在重排行的重写中幸存折叠因此是物化值而非前移 overlay。⚠️折叠索引字段必须更新其索引overlay→base 折叠移除了 overlay也就移除了维持索引正确的排除信号。折叠覆盖索引字段F的 overlay等价于对F做一次列重写必须在同一提交内要么把索引重建到至少达到被折叠 overlay 的committed_version的dataset_version要么将 fragment 从索引覆盖中移除使这些行落到 flat 路径。否则索引将在无 overlay 可排除的情况下提供过期值。这与既有的“重写索引所依赖的列”规则相同。当带 overlay 的 fragment 被行重写操作RewriteRows生成带新行地址的新 fragment压缩时overlay 会在重写过程中被折叠进新基文件既有的 fragment 复用重映射 会像今天一样处理行地址变化。从 transaction.md 的兼容性表可以补充一个重要的并发视角Rewrite行重写压缩或 overlay→base 折叠改变了物理行地址或消耗了 overlays与行移动型Update任何非REWRITE_COLUMNS列重写的更新会把更新行迁移到新 fragment都与 DataOverlay构成可重试冲突——写入方必须重读新 fragment、重新计算并重试而 DataOverlay 与另一 DataOverlay任意字段、Append、Delete、REWRITE_COLUMNS列重写、DataReplacement 均兼容因为它们都保持物理行地址不变。行血缘Row Lineageoverlay 写入会更新每个被覆盖行的last_updated_at_version因此 change-data-feedCDF和时间旅行查询能观察到该更新。由于 overlay 以物理偏移寻址它们不需要启用稳定行 ID血缘更新仅在相关特性开启时生效。参见 Row ID Lineage。工作示例一个生命周期完整的 users 表以下示例贯穿 overlay 的完整生命周期把上述规则具体化与关联文档中的示例一致。假设表users启用了稳定行 ID字段如下field idnametype1idint32(primary key)2nameutf83ageint324embeddingfixed_size_listf32,4版本 1 创建时是单个 fragment0含一个承载全部四列的基数据文件data/file0.lancephysical_rows 4offsetidnameageembedding01Alice30…12Bob25…23Carol40…34Dave22…版本 1 构建了一个覆盖 fragment0的ageBTree 标量索引dataset_version 1。Step 1 —— 写入一个 overlayUPDATE users SET age age 1 WHERE id IN (2, 4); -- Bob (offset 1), Dave (offset 3)这只触及一行字段age、两行数据因此写入方发出一个稠密 overlay——一个共享位图覆盖两个偏移——并在版本 2 提交。fragment0获得DataOverlayFile { data_file: { path: data/overlay-uuid.lance, fields: [3], column_indices: [0] } coverage: shared_offset_bitmap {1, 3} committed_version: 2 }overlay 文件只存储一列age两个值[26, 23]位于{1,3}.rank(1) 0和{1,3}.rank(3) 1两个秩上。last_updated_at_version对偏移 1 和 3 设为 2。Step 2 —— 读取SELECT id, age FROM users读取基值[30, 25, 40, 22]。对agefield 3overlay 覆盖偏移 1 和 3因此age[1]被替换为 rank{1,3}.rank(1) 0处的值 →26age[3]被替换为 rank{1,3}.rank(3) 1处的值 →23。结果[30, 26, 40, 23]。Step 3 —— 索引查询SELECT * FROM users WHERE age 26;age索引构建于dataset_version 1overlay 的committed_version是 2。由于2 1overlay 对age的覆盖{1, 3}即为本次查询的排除集。索引v1 构建中 Bob 的旧age 25因此查找26索引返回空整个排除集都在 flat 路径上重新求值而不仅仅是索引返回的行。偏移 1 的当前age经 overlay 为 26匹配Bob 被返回偏移 3 的当前age23不匹配被丢弃。镜像情形WHERE age 25展示排除如何防止过期命中索引返回偏移 1过期的 25但偏移 1 被排除、重估为 26并正确丢弃。Step 4 —— 第二次非矩形写入MERGE INTO users USING staged ON users.id staged.id WHEN MATCHED AND staged.kind rename THEN UPDATE SET name staged.name -- Carol(2), Dave(3) WHEN MATCHED AND staged.kind embed THEN UPDATE SET embedding staged.embedding -- Bob(1)name更新偏移{2, 3}embedding更新偏移{1}——不同字段覆盖不同行。这是一个稀疏 overlay在版本 3 提交DataOverlayFile { data_file: { path: data/overlay-uuid2.lance, fields: [2, 4], column_indices: [0, 1] } coverage: field_coverage { offset_bitmaps: [ {2,3}, {1} ] } // name (field 2) ^ ^ embedding (field 4) committed_version: 3 }文件的name列有两个值[Caroline, David]位于{2,3}的秩 0 和 1embedding列有一个值位于{1}的秩 0——同一文件内不同长度的列。Step 5 —— 第二次写入后的读取SELECT name, age, embedding FROM users对每个字段独立解析最新 overlay 优先namev3 overlay 覆盖{2,3}→[Alice, Bob, Caroline, David]agev3 overlay 不覆盖agev2 overlay 仍在偏移 1 和 3 生效 →[30, 26, 40, 23]embeddingv3 overlay 覆盖{1}→ Bob 的向量是新值其余来自基文件。来自不同版本的 overlay 共存并按字段分别生效。Step 6 —— 压缩overlay → base调度器在版本 4 将两个 overlay 折叠进 fragment0为age、name、embedding计算后像并写入含这些列的新基数据文件data/file1.lance。旧文件中字段 2、3、4 被标记为墓碑-2字段 1id保留。fragment 的overlays列表被清空。行地址保持不变这是列重写而非行重写因此稳定行 ID 与删除向量不受影响。由于折叠移除了原本把偏移 1 和 3 从age索引排除的 overlay该提交必须把 fragment0从索引覆盖中移除使age查询落到 flat 路径。实操指南说明本节为实现层面的考量不属于磁盘格式规范的一部分关联文档亦作此声明。何时用 overlay何时重写列何时移动行在 overlay、整列重写数据演化、将更新行迁移到新 fragment 之间做选择取决于变化行占比、变化列占比、列宽、被改列上是否有索引、以及已累积的 overlay 读取成本。粗略的经验法则少数行变化→ 倾向 overlay大多数行、少数列变化→ 倾向列重写大多数列变化→ 倾向将行迁移到新 fragment。关联文档标注此处为待扩展内容具体阈值需等待对 overlay、列重写与行移动之间交叉点的基准测试。Writer 支持现状稠密矩形overlay今天即可用现有的等长文件 writer 写入稀疏 overlay 存储为单个文件需要 writer 支持输出独立长度的列而当前 v2 writer 尚未支持它用单个全局行计数器推进所有列。在该支持落地前writer 可以在一个事务内把一次稀疏更新表达为多个稠密 overlay。值得补充的是overlay 写入侧还有几项前置约束见 overlay writer 的错误类型声明的 schema 若与数据集定义不符字段缺失或类型不同会报SchemaMismatch使用 legacy 文件格式的数据集无法写 overlayLegacyFileFormat因为旧版读取器按批边界配对 fragment 文件没有按字段覆盖的概念Blob v2 列携带 overlay 解析不读取的外部描述符因此相关写入会被拒绝写入方负责保证每字段内偏移必须严格递增rank 寻址要求以及覆盖 ≠ 值 NULL用 null 表达“别动这个单元格”会静默擦除活数据。调度压缩overlay→overlay 与 overlay→base 两种模式成本差异很大成本/收益调度器决定何时值得执行哪种并以版本差距version gap作为陈旧度信号。关联文档标注此处为待扩展内容待压缩实现并基准测试后填充具体策略。相关规范Table format overviewTransactions: DataOverlay operation —— 写入路径与冲突语义Row ID LineageIndex Formats: handling deleted and invalidated rowsFormat Versioning【免费下载链接】lanceOpen Lakehouse Format for Multimodal AI. Convert from Parquet in 2 lines of code for 100x faster random access, vector index, and data versioning. Compatible with Pandas, DuckDB, Polars, Pyarrow, and PyTorch with more integrations coming..项目地址: https://gitcode.com/GitHub_Trending/la/lance创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表