











Lance의 쓰기 연결은 파일 레이아웃, 버전 제출, 삭제 표시, 인덱스 유지 보수 및 compaction과 관련이 있습니다. 전통적인 데이터베이스와 달리 Lance는 원본 파일에 직접 데이터를 수정하지 않고, 새로운 파일을 추가하고 메타데이터를 업데이트하여 새로운 테이블 버전을 생성합니다.
본 글에서는 몇 가지 구현 문제에 대해 논의합니다:
delete, update, merge insert가 파일과 메타데이터에 어떻게 적용되는지. Lance의 기본 쓰기 형태는 새로운 데이터 파일 또는 deletion file을 쓰고, 그 다음 transaction과 manifest를 통해 새로운 버전을 제출하는 것입니다.
Delete:
不立刻重写 data file
-> 记录 fragment 内被删除的 row offset
-> 读时过滤 deletion vector
Update / Merge:
写出更新后的新 rows 或新 columns
-> 对旧 rows 写 deletion vector
-> 提交 Operation::Update
Compaction:
读取旧 fragments 的 live rows
-> 写成新的 fragments
-> 旧 row address 可能失效
-> 索引需要 remap 或重建
Stable Row ID:
让索引指向稳定逻辑行 ID
-> compaction 后只需要维护 row_id -> row_address 映射
-> 降低索引 remap 成本
Lance의 영구 메타데이터에서는 보통 DeletionFile로 불리며, 내부 처리 시 DeletionVector를 사용합니다:
DeletionVector:
内存中的删除集合语义
DeletionFile:
DeletionVector 在表目录下的持久化文件引用
LanceDB API를 보면, 사용자가 호출하는 것은:
table.add(...)
table.delete("id = 42")
table.update(where="id = 42", values={"text": "new"})
table.merge_insert("id").when_matched_update_all().when_not_matched_insert_all()
table.optimize.compact_files()
하지만 Lance의 하부에서 보는 것은 "테이블의 몇 행을 수정한다"가 아니라 한 번의 dataset 상태 전환입니다:
读取当前 manifest
-> 执行 scan / join / filter
-> 写新的 data files / deletion files / index files
-> 构造 Operation
-> 写 transaction
-> 提交新的 manifest version
한 버전의 manifest는 현재 테이블의 완전한 상태를 설명합니다:
Manifest version N
schema
fragments
indices
version metadata
Fragment 1
data files
deletion file
row id metadata
transaction은 이번 커밋에 대한 테이블 상태 변경을 설명합니다. 따라서 인덱스 생성, 컴팩트 파일, 구성 업데이트는 행 수가 변경되지 않더라도 새로운 버전이 생성될 수 있습니다.
Delete는 즉시 data file을 변경하지 않고, 명시된 행을 deleted로 표시합니다.
연결을 간소화하면 다음과 같습니다:
delete where id = 42
-> scan + predicate 找到命中 rows
-> 捕获 row address
-> 按 fragment 聚合成 local row offsets
-> 更新 fragment.deletion_file
-> Operation::Delete
-> CommitBuilder.with_affected_rows(...)
-> 提交新 manifest
Fragment 7에 1000행이 있다고 가정합니다:
Fragment 7
data file: 1000 physical rows
deletion file: {3, 19, 42}
读取 Fragment 7 时:
offset 3、19、42 被过滤
其他行仍然可见
소스 코드에서, Fragment 메타 데이터에는 선택 사항인 deletion_file 필드가 하나 있습니다. 의미는 "이 fragment 내에서 삭제된 로컬 행 오프셋"입니다. DeletionFileType 현재 두 가지 유형이 있습니다:
Array:
适合较稀疏的删除集合
Bitmap:
适合较密集的删除集合
소스 코드 입구:
rust/lance-table/src/format/fragment.rs
DeletionFile
DeletionFileType
Fragment.deletion_file
rust/lance-table/src/io/deletion.rs
write_deletion_file
read_deletion_file
rust/lance/src/dataset/write/delete.rs
apply_deletions
DeleteBuilder
는 다음과 같은 역할을 합니다:
대응 비용은: 읽기 경로에서 deletion vector를 로드하고, 스캔 중 tombstoned 행을 건너뛰어야 한다는 것입니다.
Update의 일반적인 구현은: 업데이트된 새로운 데이터를 쓰고, 이전 행을 tombstone 처리하는 것입니다.
일반적인 update의 주요 경로는 다음과 같습니다.RewriteRows:
update set text = 'new' where id = 42
-> scan where 条件命中的 rows,并带上 row id / row address
-> 对 batch 应用更新表达式
-> write_fragments_internal 写出新的 fragments
-> 对旧 row address 应用 deletion vector
-> Operation::Update { update_mode: RewriteRows }
-> CommitBuilder.with_affected_rows(...)
는 10개의 리스트로 1개의 열만 업데이트하는 예를 들어:
更新前:
Fragment 1
row offset 42 = (c1, c2, c3, ..., c10)
UPDATE SET c3 = c3_new WHERE id = 42
更新后:
Fragment 1
deletion file 标记 offset 42 deleted
Fragment 9
新写入 row = (c1, c2, c3_new, ..., c10)
RewriteRows는 전체 기존 fragment를 다시 작성하는 것이 아니라, 일치하는 rows를 새로운 rows로 작성한다. 각 업데이트된 row에 대해, 이는 완전한 행을 작성한다.
소스 코드 입구:
rust/lance/src/dataset/write/update.rs
UpdateJob::execute_impl
scanner.with_row_id()
write_fragments_internal(...)
apply_deletions(...)
Operation::Update { update_mode: RewriteRows }
Lance와 RewriteColumns 모드는 주로 일부 schema의 merge/update 시나리오에서 나타난다. 이는 "많은 행, 적은 열"의 업데이트를 견디도록 설계되었지만, fragment, 열 파일, 인덱스 커버 범위 및 충돌 검사 유지보수 비용을 증가시킨다.
LanceDB의 merge_insert는 upsert에 사용할 수 있다:
(
table.merge_insert("id")
.when_matched_update_all()
.when_not_matched_insert_all()
.execute(source)
)
여기서id는 source와 target을 매칭하는 join key이며, 데이터베이스 내 강제 제약의 primary key가 아닙니다.
의 단순화된 과정은 다음과 같습니다:
source 与 target 按 key join
matched:
更新 target rows
not matched:
插入 source rows
not matched by source:
keep 或 delete
는 full schema 경로에서 merge insert의 처리 방식이 일반적인 update에 가깝습니다.
matched rows:
写出更新后的完整 rows 到新 fragments
对旧 target rows 写 deletion vector
not matched rows:
作为新 rows 写入新 fragments
commit:
Operation::Update { update_mode: RewriteRows }
는 partial schema 경로에서는 RewriteColumns를 따릅니다:
source 只包含部分列
-> update_fragments(...)
-> Operation::Update { update_mode: RewriteColumns, fields_modified }
소스 코드 입구:
rust/lance/src/dataset/write/merge_insert.rs
MergeInsertBuilder
update_fragments
Operation::Update { update_mode: RewriteRows | RewriteColumns }
삭제 벡터는 삭제된 행을 읽을 때만 필터링하는 것이 아니라, Lance가 일부 쓰기 충돌을 "fragment 수준 충돌"에서 "행 수준 충돌"로 낮추는 데도 도움을 줍니다.
먼저 Deletion Vector / 영향받은 행이 없을 때의 문제를 살펴보겠습니다:
T1:
delete row (Fragment 1, offset 10)
T2:
delete row (Fragment 1, offset 20)
Fragment 수준에서만 보면 두 트랜잭션이 Fragment 1을 수정했으므로 쉽게 충돌로 판단될 수 있습니다. 하지만 실제로는 서로 다른 행을 삭제하므로 병합할 수 있습니다:
T1 deletion vector: {10}
T2 deletion vector: {20}
rebase 后:
Fragment 1 deletion vector: {10, 20}
Lance는 delete/update 커밋 시 맞는 행 주소를 커밋 레이어에 전달합니다:
CommitBuilder.with_affected_rows(RowAddrTreeMap)
이로 인해 충돌 검사는 다음과 같이 판단할 수 있습니다:
两个并发事务是否真的修改了同一批 rows?
동일한 Fragment의 다른 행을 수정하고 다른 트랜잭션이 데이터 파일을 변경하지 않고 deletion file만 변경하는 경우, 현재 트랜잭션은 rebase할 기회가 있습니다. 즉, 새로운 Fragment deletion file을 기반으로 병합된 deletion vector를 다시 작성할 수 있습니다.
일반적인 경우:
可 rebase:
T1 delete F1.offset 10
T2 delete F1.offset 20
-> affected_rows 不重叠
-> 合并 deletion vector
不可 rebase:
T1 update F1.offset 10
T2 delete F1.offset 10
-> affected_rows 重叠
-> 语义冲突
不可简单 rebase:
T1 delete F1.offset 10
T2 compaction/rewrite F1
-> fragment 的 data files 被重写
-> row address / fragment 状态发生大范围变化
소스 코드 입구:
rust/lance/src/dataset/write/commit.rs
CommitBuilder.with_affected_rows
rust/lance/src/io/commit/conflict_resolver.rs
TransactionRebase
check_delete_txn
check_update_txn
따라서 Deletion Vector는 모든 쓰기 충돌을 제거하지 않습니다. 이는 Lance가 fragment 수준의 가짜 충돌을 피할 수 있도록 row 수준의 영향받은 행을 표현할 수 있게 합니다.
대량의 샘플을 포함하는 데이터 세트에서 업데이트, 삭제, merge는 종종 소수의 행만 영향을 미칩니다. 동시성 제어가 fragment 수준의粒度로만 이루어질 경우, 많은 서로 다른 행 수준의 변경 사항이 충돌로 판단될 수 있습니다.
Lance와 Apache Paimon 모두 Deletion Vector를 가지고 있지만, 이들이 해결하려는 문제는 완전히 다릅니다.
| 차원 | Lance | Apache Paimon |
|---|---|---|
| 주요 데이터 모델 | Arrow / Lance fragment을 위한 열식 데이터 세트 | 물빙 테이블, 주 키 테이블, LSM, 버킷, 스냅샷을 위한 |
| DV 입자 크기 | fragment 내 로컬 행 오프셋 | 데이터 파일 내 행 위치 |
| 표준 용도 | 삭제, 업데이트, 병합, 충돌 검사, 읽을 때 필터링, 압축 물리적 삭제 | 주 키 테이블 MOW 모드에서 읽을 때 병합을 피하고, 쓰기 시 DV 파일 생성 |
| 와 업데이트의 관계 | 업데이트 일반적인 경로는 새로운 rows 작성 + 기존 rows의 tombstone 생성 | 주 키 테이블은 LSM을 통해 기존 데이터를 찾아 DV 생성 |
| 컬럼 수준의 진화와의 관계 | Lance는 fragment 여러 data files과 row id 메타데이터를 가지며, 업데이트 후에도 fragment/row address를 중심으로 유지 | Paimon Data Evolution은 컬럼별 overlay 방식을 사용하며, first row id의 파일 읽기 시 합산 |
| 충돌 검사와의 관계 | affected_rows는 동시성 delete/update를 row 수준의 rebase로 처리할 수 있게 합니다 |
는 주 키 테이블 쓰기 연결과 파일 수준의 읽기 필터링에 더 치우쳐 있습니다. |
Paimon의 DV 문서는 명확한 의미를 가집니다: 이는 삭제된 행 위치를 기록한 data file을 기록하며, 파일을 읽을 때 이러한 행을 필터링합니다. Paimon의 Merge On Write 모드는 LSM에 의존하며, 쓰기 단계에서 주 키를 쿼리하여 해당 data file의 deletion vector를 생성하여 읽을 때 완전한 merge를 더 이상 수행하지 않도록 합니다.
Paimon의 Data Evolution 테이블은 다른 경로를 취합니다: 업데이트된 열만 새 파일에 쓰고, 원본 데이터 파일은 변경하지 않으며, 읽을 때 동일한 first row id의 여러 파일을 병합하여 완전한 행을 만듭니다. 따라서 Paimon Data Evolution은 deletion vectors를 비활성화하고 일반적인 Delete / Update statement을 지원하지 않습니다.
아래는 Data Evolution과 DV를 조합할 때의 단순화된 예입니다:
base file:
firstRowId = 100
columns: id, a, b, c
update file:
firstRowId = 100
columns: b_new
读时:
base file + update file
-> 按 firstRowId 对齐
-> 得到完整行
file-level deletion vector를 추가로 도입한다면:
删除 base file 的某一行:
a / c 也被隐藏
但 b_new 的 overlay 文件如何处理?
删除 update file 的某一行:
b_new 不可见
但 base file 的旧 b 是否应该恢复?
두 가지 메커니즘을 모두 지원할 때는 read/write 경로에서 "행 삭제"와 "열 overlay 합병"을 동시에 처리해야 합니다. Paimon은 Data Evolution과 Deletion Vector를 분리하여 두 가지 의미가 서로 영향을 주지 않도록 합니다.
Lance의 표준 업데이트 경로는 다음과 같습니다:
新 rows / 新 columns 写入新 fragment 或新 data files
旧 rows 通过 deletion vector tombstone
manifest 统一描述当前版本的 fragment 状态
따라서 Lance의 Deletion Vector는 단순히 read 필터링 도구가 아니라, 동시에 write rebase와 충돌 판단에 참여합니다.
Deletion Vector는 delete/update의 write amplification을 낮추지만, 두 가지 문제를 남깁니다:
Compaction은 이러한 레이아웃 하락 문제를 처리합니다.
Lance의 compaction은 파일 크기만으로 선택하는 것이 아니라, 주로 fragment의 행 수와 삭제 비율을 보고 합니다:
if deletion_percentage > materialize_deletions_threshold:
选中这个 fragment
目的:把 deletion vector 物化掉,只写 live rows
else if physical_rows < target_rows_per_fragment:
选中这个 fragment
目的:和相邻的小 fragments 合并成更大的 fragment
else:
不参与本轮 compaction
기본 구성에서:
target_rows_per_fragment = 1024 * 1024
materialize_deletions_threshold = 0.1
해당 동작은 다음과 같습니다:
소스 코드 입구:
rust/lance/src/dataset/optimize.rs
CompactionOptions
plan_compaction
CompactionCandidacy::CompactItself
CompactionCandidacy::CompactWithNeighbors
여기 인덱스 제약 조건이 하나 있습니다: compaction은 "특정 인덱스로 덮어쓴 fragment"와 "해당 인덱스로 덮어쓰지 않은 fragment"를 같은 rewrite 그룹에 혼합하지 않습니다.
이유는 Lance의 인덱스 메타데이터에 fragment_bitmap가 있기 때문입니다. 이것은 이 인덱스가 어떤 fragments를 덮어쓴다고 설명합니다. rewrite 그룹에 indexed와 unindexed fragments가 혼합되면, compaction 후 새로운 fragment는 indexed 또는 unindexed로 명확하게 분류될 수 없습니다.
따라서 compaction 플래너는 인덱스 커버리지에 따라 후보 fragments를 bin으로 분류합니다:
F1 indexed by vector_index
F2 indexed by vector_index
F3 not indexed
允许:
compact(F1, F2) -> F10
不允许:
compact(F2, F3) -> F11
컴팩션의 본질은 리라이트:
old fragments:
F1, F2
new fragments:
F10
인덱스에 행 주소가 기록되어 있으면 컴팩션 후에는 인덱스 내 주소를 유지해야 합니다.
Lance의 행 주소는 프래그먼트 ID와 행 오프셋으로 구성됩니다:
row_address = (fragment_id, row_offset)
컴팩션 전:
F1.offset 0
F1.offset 1
F2.offset 0
컴팩션 후:
F10.offset 0
F10.offset 1
F10.offset 2
논리적으로 여전히 동일한 라이브 행들일지라도 물리적 주소는 변경됩니다. 인덱스에 저장된 행 주소는 이에 맞게 업데이트해야 하며, 그렇지 않으면 인덱스가 오래된 프래그먼트를 가리킬 수 있습니다.
Lance는 컴팩션 후 인덱스에 대해 몇 가지 처리 방식을 가지고 있습니다:
1. 同步 remap index
compaction 时生成 old row address -> new row address 映射
重写受影响的 index files
2. defer_index_remap
compaction 时不立即重写所有索引
建立 fragment reuse index
查询时或后续维护时再做 remap
3. stable row id
索引不再直接依赖易变的 physical row address
而是指向稳定 row id
트랜잭션 적용Operation::Rewrite 일 때, Lance는 두 가지 유형의 메타데이터를 처리합니다:
fragments:
old fragments 从 manifest 移除
new fragments 加入 manifest
indices:
更新 fragment_bitmap
必要时替换 index uuid 和 index files
소스 코드 입구:
rust/lance/src/dataset/transaction.rs
Operation::Rewrite
handle_rewrite_fragments
recalculate_fragment_bitmap
handle_rewrite_indices
rust/lance/src/dataset/optimize/remapping.rs
fragment reuse index
deferred index remap
다른 인덱스의 유지 방식은 다릅니다. remap이 가능한지 여부는 인덱스 내부에 old row address에서 new row address로 매핑할 수 있는 세부 정보가 저장되어 있는지 여부에 따라 달라집니다.
인덱스 내부 기록의 내용에 따라 구분할 수 있습니다:
较容易 remap:
能逐条或逐 segment 映射到 row address 的索引
较难 remap:
内部结构强依赖训练结果、聚类结果、posting layout 或 block layout 的索引
예를 들어, 벡터 인덱스는 단순히 key -> row address 매핑이 아닙니다. IVF와 같은 인덱스 내부에는 클러스터링 중심, 파티션, 벡터 리스트, row id 리스트와 같은 구조가 있습니다. compaction 후에도 벡터 값은 변하지만, row address는 변하므로 인덱스 내부의 row id/address 페이로드는 일관되게 업데이트해야 합니다. 인덱스 형식이 저렴하고, 지역적이며, 신뢰할 수 있는 remap 기능을 제공하지 않으면, 재작성하거나 fragment reuse index를 이용해 나중에 처리해야 합니다.
그래서, compaction의 어려움은 오직 데이터 파일 재작성에만 국한되지 않습니다:
데이터 파일 재작성 후에도, 인덱스는 여전히 동일한 논리 행을 찾을 수 있어야 합니다.
안정적인 행 ID는 각 논리 행에 안정적인 ID를 할당하여 인덱스가 변경되는 행 주소에 직접 의존하지 않도록 합니다.
안정적인 행 ID가 없을 때:
row id ~= row address
索引命中:
vector index -> row address -> 回表
compaction:
row address 改变
-> index payload 需要 remap
안정적인 행 ID를 켜면:
row id = 稳定逻辑行 ID
row address = 当前物理位置
索引命中:
vector index -> stable row id
-> RowIdIndex 查 row id 当前对应的 row address
-> 回表
compaction:
stable row id 不变
row address 改变
-> 更新 row_id_meta / RowIdIndex
-> 索引主体可以复用
그렇게 그릴 수 있습니다:
without stable row id
Index payload ------------------> RowAddress(F1, 42)
|
| compaction 后失效
v
RowAddress(F10, 8)
with stable row id
Index payload ---> StableRowId(10086)
|
v
RowIdIndex(version N)
|
v
RowAddress(F10, 8)
RowIdIndex는 사용자가 수동으로 생성한 인덱스가 아니며, 각 행마다 독립적으로 저장되지 않습니다row_id -> row_address 기록입니다. stable row id가 활성화된 후 Lance가 필요할 때 fragment 메타데이터를 기반으로 구축한 버전별 메모리 인덱스로, 메타데이터 캐시에 캐싱됩니다.
구축 입구는:
rust/lance/src/dataset/rowids.rs
get_row_id_index(dataset)
-> 如果 manifest.uses_stable_row_ids()
-> 从 metadata_cache 获取或构建 RowIdIndex
load_row_id_index(dataset)
-> 读取每个 fragment 的 row_id_meta
-> 读取该 fragment 的 deletion vector
-> 构造 FragmentRowIdIndex
-> RowIdIndex::new(...)
각 fragment에는 row_id_meta이 저장되어 있으며, 이는 fragment 내 "물리적 행 순서에 해당하는 stable row id 시퀀스"를 설명합니다:
Fragment 10
row_id_meta:
physical offset 0 -> stable row id 100
physical offset 1 -> stable row id 101
physical offset 2 -> stable row id 105
physical offset 3 -> stable row id 106
deletion vector:
{1}
RowIdIndex를 구축할 때 deletion vector가 적용됩니다:
offset 0 live -> 100 -> RowAddress(F10, 0)
offset 1 deleted -> 跳过
offset 2 live -> 105 -> RowAddress(F10, 2)
offset 3 live -> 106 -> RowAddress(F10, 3)
소스 코드의 핵심 구조는:
pub struct RowIdIndex(
RangeInclusiveMap<u64, (U64Segment, U64Segment)>
);
이를 이해할 수 있습니다:
RowIdIndex
key:
这一段 row id 覆盖的范围
value:
row_id_segment:
这一段实际存在的 row ids
address_segment:
与 row_id_segment 一一对齐的 physical row addresses
즉, RowIdIndex의 한 chunk이 하나의 매핑이 아니라 여러 매핑의 조각입니다:
coverage range:
100..=106
row_id_segment:
[100, 105, 106]
address_segment:
[RowAddress(F10, 0), RowAddress(F10, 2), RowAddress(F10, 3)]
쿼리row_id = 105 때, 프로세스는 다음과 같습니다:
1. 用 105 到 RangeInclusiveMap 中找到覆盖它的 chunk
2. 在 row_id_segment 中找 105 的位置
3. 假设位置是 1
4. 从 address_segment 取第 1 个地址
5. 得到 RowAddress(F10, 2)
에 해당하는 소스 코드는:
rust/lance-table/src/rowids/index.rs
RowIdIndex::get(row_id)
-> self.0.get(&row_id)
-> row_id_segment.position(row_id)
-> address_segment.get(pos)
-> RowAddress::from(address)
U64Segment는 여기서 압축 표현입니다. 이는 row id 시퀀스의 형태에 따라 다른 구조를 선택합니다:
Range:
连续有序,例如 100..200
RangeWithHoles:
大体连续,但有少量空洞
RangeWithBitmap:
大体连续,使用 bitmap 标记哪些位置存在
SortedArray:
有序但比较稀疏
Array:
无序序列
는 다음과 같은 문제를 설명합니다: stable row id를 활성화한 후, Lance는 각 행에 대해 메타데이터를 하나씩 작성하지 않습니다. 일반적으로 연속적으로 삽입된 row id는 범위(range)로 표현할 수 있습니다; update, delete, compaction 후에 공백이나 혼란스러운 순서가 발생하면, 점진적으로 bitmap이나 array와 같은 표현으로 저하됩니다.
소스 코드 입구:
docs/src/format/table/row_id_lineage.md
Row Address vs Row ID
stable row id behavior
docs/src/format/index/index.md
Stable Row ID for Index
rust/lance/src/dataset/rowids.rs
get_row_id_index
load_row_id_index
rust/lance-table/src/rowids/index.rs
RowIdIndex
FragmentRowIdIndex
rust/lance-table/src/rowids/segment.rs
U64Segment
Stable Row ID의 역할에는:
하지만 그것도 비용이 있습니다:
stable row id -> row address의 매핑이 필요합니다. row_id_meta을 유지해야 합니다. 그래서 Stable Row ID는 무차별적으로 기본적으로 켜는 데 적합하지 않습니다. 일회성 임포트, 낮은 빈도 업데이트, 인덱스 재구축을 수용할 수 있는 작은 테이블에는 그렇지 않을 수 있습니다. 인덱스 구축 비용이 높고 compaction이 자주 발생하며 장기간 유지보수가 필요한 데이터 세트에는 더 가치가 있습니다.
Lance 테이블이 하나 있다고 가정해 봅시다.
schema:
id: int64
text: string
vector: fixed_size_list<float32>[768]
indices:
vector index on vector
scalar index on id
초기 상태:
Manifest v1
Fragment 1: rows 0..999
Fragment 2: rows 1000..1999
Vector index:
fragment_bitmap = {1, 2}
한 번 update 실행:
UPDATE table
SET text = 'new text'
WHERE id = 42;
처리 단계:
1. scan 找到 id = 42 的 row address
2. 写出更新后的新 row 到 Fragment 3
3. 给 Fragment 1 写 deletion file,标记旧 offset deleted
4. 提交 Operation::Update
5. affected_rows = {(Fragment 1, offset 42)}
그러나 동시에 다른 트랜잭션이 id = 43을 삭제하는 경우:
T1 affected_rows = {(F1, 42)}
T2 affected_rows = {(F1, 43)}
두 것은 같은 fragment에 속하지만 row는 겹치지 않습니다. 다른 data file rewrite가 없으면 Lance는 deletion vector를 병합하여 rebase를 완료할 기회가 있습니다.
이후 compaction을 수행합니다:
Fragment 1:
deleted rows 超过 threshold
Fragment 2:
小于 target_rows_per_fragment
planner는 검사합니다:
这些 fragment 是否相邻?
它们是否有相同 index coverage?
删除比例是否值得单独 materialize?
최종 rewrite가 발생하면:
old:
Fragment 1
Fragment 2
new:
Fragment 10
인덱스는 처리해야 합니다:
fragment_bitmap:
{1, 2} -> {10}
row address:
old addresses -> new addresses
stable row id가 활성화되어 있으면:
index payload 仍然指向 stable row id
RowIdIndex 更新 stable row id 到新 RowAddress 的映射
이 예제는 Deletion Vector, Compaction, Index Remap 및 Stable Row ID의 관계를 설명합니다.
Lance의 쓰기 연결은 전통적인 데이터베이스의 원자적 업데이트도, 단순한 append-only 로그도 아닙니다. 그것은 버전 관리된 열 형식 데이터 세트 쓰기 메커니즘입니다:
delete deletion vector를 통해 이전 행을 보이지 않게 합니다. update 그리고merge 새로운 행/열을 작성하여 오래된 행에 토밍스톤을 추가하여 수정을 표현합니다. Lance 쓰기 연결 설계의 부담을 요약하면 다음과 같습니다:
파일을 다시 쓰고 난 후에도 deletion, version, index 및 row identity 는 여전히 동일한 로직 행을 나타내야 합니다.
이 콘텐츠는 인셔셔RSS(RSS 리더)가 자동으로 집계한 것으로 읽기 참고용입니다. 원문 출처 — 저작권은 원저작자에게 있습니다.