土法炼钢兴趣小组的算法知识备份

【数据湖与开放表格式】湖上 AI 与向量

文章导航

分类入口
databasestorage
标签入口
#lance#vector-search#embedding#feature-store#iceberg

目录

训练一个模型,数据从哪来?越来越多团队的答案是「湖」:原始日志、清洗后的样本、算好的 embedding、特征表,都以列式文件躺在对象存储上。把 AI 数据放进开放表格式,能直接复用前面 20 篇讲的 ACID、快照、时间旅行——尤其是按 snapshot 固定数据版本,让「这次训练用的是哪一批数据」变成一个可回溯的 snapshot id,而不是一句口头约定。

但湖的默认底座 Parquet 是为顺序扫描设计的。AI 负载里有两类访问模式它不擅长:按行随机取(给定一批样本 id 取它们的向量),和向量相似度检索(给一个查询向量找最近邻)。这一篇讲清楚:湖怎么承接 AI 数据、Parquet 的短板在哪、Lance 这类格式补了什么,以及湖侧存储和专用向量引擎的边界在哪。

这是本系列的末篇,只讲湖侧的存储与读取。向量索引算法(HNSW、IVF、PQ)和向量数据库的工程,属于 数据库前沿 系列的范畴,本文只在边界处衔接,不重复。

实测环境:i9-12900K(24 逻辑核)、32 GB 内存、Arch Linux on WSL2(内核 6.6.87.2)。Python 3.14.5、PyArrow 24.0.0、Lance(pylance)7.0.0、DuckDB 1.5.4。实验脚本见 /tmp/lake21_bench.py,下文数字为本机真实执行、各 5 次取中位数。


一、为什么 AI 数据会落到湖上

把数据放进开放表格式,AI 工程拿到三样直接有用的东西:

  1. 数据版本即 snapshot:训练前记下表的 snapshot id,之后任何时候都能用时间旅行(第 16 篇)读回逐字节一致的那批数据。复现一次实验、追查一次数据问题,不再依赖「把数据 cp 一份存着」。
  2. schema 演进:特征表会不断加列、改类型。开放表格式按 field ID 做 schema evolution,加一个特征列不重写历史数据,老训练任务读旧 snapshot 仍然正确。
  3. 存算分离 + 引擎中立:同一份特征数据,Spark 跑批量特征工程、Trino 做即席分析、训练框架直接读 Parquet/Arrow,互不绑定。

这套优势对批量、顺序的访问(全表扫描做特征工程、按分区读一批训练数据)成立。问题出在另外两类访问上。


二、Parquet 存向量的两个短板

短板一:按行随机访问

特征服务、在线推理、数据抽检都要「给一批 id,取它们的向量/特征」。Parquet 的物理结构(row group → column chunk → page,见第 2 篇)是为顺序扫描优化的:要取某几行,最细只能定位到 row group,再解压整个 page 才能拿到目标行。随机取 100 行,可能要解压横跨整个文件的一堆 page,代价接近全表扫描。

本机做了一个对照:200,000 行、每行一个 768 维 float32 向量(约相当于一批文本 embedding),分别写成 Parquet 和 Lance,比较两种访问:

指标 Parquet Lance
文件体积 616.2 MB 614.8 MB
顺序扫描 vec 1283.7 ms 430.0 ms
随机取 100 行 vec 1258.5 ms 1.2 ms

随机点取这一行最说明问题:Parquet 取 100 行花了 1258.5 ms,和全表扫描(1283.7 ms)几乎一样——因为它只能靠扫描+过滤来定位这 100 行。Lance 取同样 100 行只要 1.2 ms,相差约三个数量级。体积上两者几乎相同(随机 float32 本就难压缩),所以差距来自布局和访问路径,不是压缩。

这组数字是随机向量、单机本地盘的微基准,目的是暴露访问模式差异,不代表生产吞吐;换数据分布、对象存储后端、并发度,绝对值都会变,但「随机点取 Parquet ≈ 全扫、Lance ≈ O(1)」这个结构性差异是格式设计决定的。

短板二:向量相似度检索

「找最近的 k 个向量」需要一个 ANN 索引(如 HNSW、IVF-PQ)。Parquet 是纯数据文件,不承载这类索引;在 Parquet 上做向量检索只能把全部向量读出来暴力算,O(N) 一次查询。这对几千万级向量不可接受。


三、Lance:为随机访问与向量而设计

Lance 是一个开源列式格式,定位和 Parquet 不同:它在保留列式扫描能力的同时,把按行随机访问做成一等公民,并原生支持向量索引。关键差异:

  1. 稳定的行寻址:Lance 用 row id / row address 直接定位行,take(row_ids) 是接近 O(1) 的随机访问,不需要扫描过滤——这正是上面 1.2 ms 的来源。
  2. 细粒度的 page/布局:相比 Parquet 以 row group 为最小解压单位,Lance 的布局让随机读只触碰需要的部分。
  3. 原生向量索引:Lance 支持在向量列上建 IVF-PQ、HNSW 等 ANN 索引,把向量检索从 O(N) 暴力降到近似对数级;索引随数据集一起管理。
  4. versioning:Lance 自己也有数据集版本/快照概念,单独使用时能做数据版本管理。

写法上和 Parquet 一样简单:

import lance, pyarrow as pa
# tbl 是一个含 id 与定长 list<float32> 向量列的 Arrow Table
lance.write_dataset(tbl, "emb.lance")

ds = lance.dataset("emb.lance")
ds.take([12, 8893, 150034], columns=["vec"])   # 随机按行取, 约 O(1)

适用判断很清楚:顺序全扫、与现有湖仓引擎深度集成,Parquet 仍是默认;频繁按行随机点取、需要在数据集上直接做向量检索,Lance 这类格式有结构性优势。两者不互斥——很多团队用 Parquet/Iceberg 存主数据,用 Lance 存需要随机访问和检索的向量子集。


四、湖上向量检索的现状与限制

把向量检索做进开放表格式本身(而不是旁挂一个向量库),目前还在演进,几个关键限制要看清:

  1. 索引存放:Iceberg/Delta/Hudi 的元数据模型是为「文件 + 列统计」设计的,ANN 索引(HNSW 图、IVF 倒排)是另一种数据结构,怎么随表格式一起管理、随 compaction 一起维护,规范层还没有成熟统一的答案。Iceberg 的 Puffin 文件能放一些统计/sketch(第 17 篇),但完整 ANN 索引通常还在格式自带的索引(如 Lance 的索引)里,而非表格式元数据里。
  2. 过滤 + 向量的组合下推:实际查询常是「在满足 country='CN' AND ts>... 的行里找最近邻」。标量过滤(表格式擅长,靠 manifest/列统计裁文件)和向量检索(靠 ANN 索引)如何协同下推、先过滤还是先检索,直接决定召回质量和延迟,这一块工程上仍不统一。
  3. 与专用向量库的边界:专用向量数据库(见 db-frontier)在索引算法、增量更新、过滤召回上更成熟。湖侧的价值不是取代它们,而是让向量和它的源数据、特征、标签待在同一套存储和版本体系里,省掉再同步一份的代价。

所以现实架构往往是分层的:主数据与特征在 Iceberg/Parquet,向量子集在 Lance 或专用向量库,靠同一套 id 和 snapshot 关联。


五、边界:与 db-frontier 的分工

本文只覆盖湖侧的存储与读取

一句话收束整个系列:开放表格式把「数据库内核做的事」——ACID、快照、schema 演进、并发控制——拆出来,摊在对象存储上的文件和你的运维作业里,换来存算分离和引擎中立。AI 负载是这套架构的新边疆:版本化、引擎中立这些优势直接可用,而随机访问和向量检索这些新需求,正在催生 Lance 这样的新格式,也在反过来推动表格式规范继续演进。


参考资料

  1. Lance Documentation(lancedb / lance 项目),File FormatVector Search(按行随机访问 take、row address、IVF-PQ/HNSW 向量索引、dataset versioning)。A 级(官方文档)。
  2. Apache Parquet, parquet-format(row group / column chunk / page 的最小解压单位,决定随机访问代价)。A 级。
  3. Apache Arrow Documentation(Arrow Table / FixedSizeList,向量列的内存表示)。A 级。
  4. Apache Iceberg Documentation, Puffin spec(统计/sketch 的承载,及其与完整 ANN 索引的边界)。A 级。
  5. 本机实验:PyArrow 24.0.0 + Lance 7.0.0 + Python 3.14.5,i9-12900K / 32 GB / Arch Linux WSL2 内核 6.6.87.2;200,000 行 × 768 维 float32,顺序扫描与随机 take 各 5 次取中位数,脚本 /tmp/lake21_bench.py
  6. 本系列:第 2 Parquet 文件格式深拆16 时间旅行、Schema 与分区演进17 小文件与 Compaction 篇;衔接 数据库前沿 系列。

返回 系列目录 · 上一篇 选型、迁移与运维 ·(本篇为系列末篇)

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-29 · database / storage

【数据湖与开放表格式】Parquet · Iceberg · Delta · Hudi 内核拆解

拆解 lakehouse 的两层基础:列式文件格式(Parquet/ORC/Arrow)与开放表格式(Iceberg/Delta/Hudi)。讲清没有数据库进程时,如何在对象存储上做 ACID、行级更新、快照与并发,以及 catalog、查询引擎、流式入湖如何拼成可运维的湖仓。面向数据平台工程师与从 OLAP/数仓转型的开发者。

2026-06-30 · database / storage

【数据湖与开放表格式】Lakehouse 全景:从 Hive 表到开放表格式

Hive 目录式分区表把『表』等同于『一组目录加 metastore 里的分区行』,于是没有原子提交、planning 要 LIST 目录、schema 与分区演进常要重写。本文用这三个硬伤切入,讲清 lakehouse 把表拆成『不可变数据文件 + 可变元数据指针 + catalog』三层后各自解决了什么,并给出全系列的分层地图。

2026-06-30 · database / storage

【数据湖与开放表格式】表格式为什么存在

目录式分区表(Hive 表)在对象存储上有三处硬伤:并发写部分提交、list planning 太贵、缺快照隔离与原子提交。本文拆开放表格式补上的四件事——原子提交、快照隔离、文件级统计裁剪、schema 与分区演进,并抽象出三家共有的『元数据指针 + 不可变数据文件』骨架。

2026-06-30 · database / storage

【数据湖与开放表格式】Iceberg 元数据树

拆解 Iceberg 的四层元数据:catalog 指针 → metadata.json → manifest list(snapshot)→ manifest file → data file。讲清 snapshot 与 manifest 里的分区数据和列级 stats(lower/upper bound、null/value count)如何让一次查询不 list 目录就收敛到文件集合,并给出表规范 V1/V2/V3 的版本边界。基于 pyiceberg 0.11.1 真实建表逐层 dump。


By .