LanceDB 物化视图定义接口 MaterializedViewDefinition 完全解析:SQL 语义、存储布局与刷新机制
2026/9/23 11:17:56 网站建设 项目流程

LanceDB 物化视图定义接口 MaterializedViewDefinition 完全解析:SQL 语义、存储布局与刷新机制

【免费下载链接】lancedbDeveloper-friendly OSS embedded retrieval library for multimodal AI. Search More; Manage Less.项目地址: https://gitcode.com/gh_mirrors/la/lancedb

物化视图(Materialized View)是 LanceDB 中将"查询结果"固化为可检索、可索引、可搜索的表的关键能力。本文围绕 Node.js 客户端中的MaterializedViewDefinition接口展开,深入讲解物化视图定义查询的 SQL 语法(SELECT ... FROM [ns.]table [, function(args) AS alias | , UNNEST(column) AS alias] [WHERE predicate] [LIMIT n])、它在 schema 元数据中的存储布局(mv.definition与 format 版本化机制)、读取解析流程,以及结合 Rust 核心源码的增量刷新原理。读完本文,你将掌握如何定义、创建、读取、刷新和删除物化视图,并理解其底层实现约束。

接口概览:什么是 MaterializedViewDefinition

MaterializedViewDefinition是描述物化视图"定义查询"的接口,它回答了一个核心问题:这个视图的每一行是从哪张源表、按什么规则算出来的?在 nodejs/lancedb/materialized_view.ts 中,接口定义如下:

export interface MaterializedViewDefinition { /** The defining query, in the canonical spelling the server stores. */ query: string; }

接口只有一个属性query,它是一个 SQL 字符串,且是"服务器存储的规范拼写(canonical spelling)"——也就是说,无论你创建视图时怎么写 SQL(大小写、引号、空格),读回来的query都是经过解析-渲染归一化后的标准形式。这一点在测试 nodejs/test/materialized_view.test.ts 中有直接验证:创建视图时只传where: "age >= 18",读回的定义是"SELECT name, age FROM people WHERE age >= 18"

这个接口并非孤立存在,它对应 Rust 核心层的同名结构体 rust/lancedb/src/materialized_view.rs,后者用强类型字段描述同一份定义:

pub struct MaterializedViewDefinition { pub source_table: String, // 源表名,与视图同库 pub source_namespace: Vec<String>, // 源表所在命名空间路径,空表示根命名空间 pub lateral: Option<ViewLateral>, // 每源行计算的 FROM 项(UNNEST 或函数) pub projections: Vec<ViewProjection>, // 投影输出列,视图 schema 顺序 pub filter: Option<String>, // WHERE 谓词 pub limit: Option<u64>, // 行数上限 }

Rust 层的MaterializedViewDefinition::from_sql(sql)负责把 SQL 文本解析成这个结构,to_sql()渲染回规范拼写,to_json()生成用于存储的 JSON 布局。Node 侧的MaterializedViewDefinition.query字符串,正是这一整套结构化定义在语言绑定边界的最终落点。

定义查询的 SQL 语法详解

物化视图的定义查询遵循一个严格受限的 SELECT 形状(rust/lancedb/src/materialized_view.rs 的from_sql文档):

SELECT <column | expr AS name | *>, ... FROM [ns.]table [, function(args) AS alias | , UNNEST(column) AS alias] [WHERE predicate] [LIMIT n]

各部分语义如下:

  • SELECT 子句:投影列可以是裸列名、表达式 AS 别名*(表示源表全部列)。投影可以重命名列,例如SELECT upper(name) AS Shout FROM people
  • FROM 子句:必须恰好是一个源表,可带命名空间前缀(如ns.docs)。在源表之后可以通过逗号追加两类"横向项"(lateral item),每个横向项在每一源行上计算、每返回一个元素生成一行:
    • function(args) AS alias:一个函数(Function)出现在 FROM 位置,其参数是作用于源表的 SQL 表达式,返回值为行集合;
    • UNNEST(column) AS alias:将源表的一个列表列(list column)展开,别名用于在投影中读取元素,例如SELECT c.chunk FROM docs, UNNEST(chunks) AS c
  • WHERE 子句:布尔谓词,筛选视图保留的行。
  • LIMIT 子句:视图持有行数的上限,按物化顺序截断。

Rust 层用ViewLateral/LateralSource枚举建模横向项,其中LateralSource::Unnest表示UNNEST(column)LateralSource::Function表示name(args)。需要注意的是:函数形态的视图(Function in FROM)仅在 LanceDB Cloud 与 Enterprise 上受支持——本地数据库在刷新时若遇到函数形态却没有 staging 绑定,会直接报NotSupported错误(见physical_unnest实现)。

除上述子句外,任何其他 SQL 子句(JOIN、GROUP BY、ORDER BY 等)都会被拒绝。理由在源码注释中写得很明确:引擎无法维护它不完全理解的查询,一份无法完整理解的定义绝不能被物化。CROSS JOIN [LATERAL]与逗号形式的横向项表达的是同一关系,会归一化为同一条规范查询。

底层存储布局:mv.definition 元数据与格式版本

物化视图在物理上就是一张普通表,其"视图身份"与定义查询一起存放在schema 元数据中,键名为mv.definitionDEFINITION_META_KEY,定义于 nodejs/lancedb/materialized_view.ts 与 rust/lancedb/src/materialized_view.rs)。

当前版本写入的布局(DEFINITION_FORMAT = 1)是一个 JSON 对象:

{"kind": "query", "format": 1, "query": "SELECT name, age FROM people WHERE age >= 18"}

其中queryto_sql()渲染出的规范拼写,kind: "query"用于让比 format 编号更老的读者把它识别为"不可刷新的视图"而非读元数据失败。

在引入 format 编号之前,旧版本写入的是结构化布局(legacy layout),按kind分为两种:

  • {"kind": "select", ...}:根命名空间下的源表;
  • {"kind": "namespaced_select", ...}:带命名空间路径的源表。

结构化布局包含source_tablesource_namespaceprojections(每项含outputexpression)、filterlimit等字段。例如:

{ "kind": "namespaced_select", "source_table": "people", "source_namespace": ["ns"], "projections": [ {"output": "name", "expression": "`name`"}, {"output": "Shout", "expression": "upper(name)"} ], "filter": "age >= 18", "limit": 42 }

这条旧布局会被读取器渲染成SELECTname, upper(name) ASShoutFROM ns.people WHERE age >= 18 LIMIT 42——测试 nodejs/test/materialized_view.test.ts 对该还原逻辑做了逐字符断言。旧布局只读不写:视图一旦被刷新,就会以当前 format 重写。

除了定义本身,物化视图还会使用其他mv.*元数据键来维护运行状态,它们全部声明于 rust/lancedb/src/materialized_view.rs:

元数据键常量含义
mv.definitionDEFINITION_META_KEY视图定义查询(format 1 布局)
mv.incarnationINCARNATION_META_KEY视图物理创建的化身令牌,用于区分"同名同定义被删除重建"的视图
mv.source_versionSOURCE_VERSION_META_KEY上次刷新到的源表版本,首次刷新前不存在
mv.refreshed_at_msREFRESHED_AT_MS_META_KEY上次刷新的墙上时钟时间(毫秒)
mv.stagingSTAGING_META_KEY函数形态视图的输出暂存表绑定
mv.view_versionVIEW_VERSION_META_KEY上次成功刷新后的视图表版本,视图上任何其他提交都被视为漂移
mv.source_version_tsSOURCE_VERSION_TS_META_KEY水印所对应源清单的提交时间戳,用于识别源表被删除重建

读取与解析流程:从元数据到 MaterializedViewDefinition

Node 端读取定义的入口是definitionFromMetadatadefinitionFromJson(见 nodejs/lancedb/materialized_view.ts),MaterializedView.definition()方法最终调用它们:

async definition(): Promise<MaterializedViewDefinition> { return definitionFromJson( await this.inner.materializedViewDefinition(), this.name, ); }

解析逻辑的关键点有三:

  1. 带 format 的读取:若 JSON 含format字段,读取器检查value.format > DEFINITION_FORMAT(即 >1)。若更新的版本写入了一个新 format,当前版本拒绝猜测,直接抛错:"materialized view '...' is stored in format X, which this version of lancedb cannot refresh"。
  2. 旧布局还原:若 JSON 无format字段,则检查kind是否为select/namespaced_select,否则同样视为"无法刷新"。旧布局按legacyQuery渲染成规范 SQL。若旧布局带limit,还会校验其是否能被 JS Number 精确表示——JSON.parse会把超过 2^53 的整数取整,因此!Number.isSafeInteger(limit)时抛出 "stored limit too large to represent exactly"。
  3. 非视图识别:如果表元数据中根本没有mv.definition键,则抛出 "Table 'X' is not a materialized view"。这是把"物化视图"与"普通表"区分开来的关键防线——Rust 侧的read_definition返回Ok(None)即普通表,任何"无法解析的定义"都会报错,因为把视图当普通表处理会允许它被覆写。

前向兼容性测试覆盖了这些分支:{"format":2,...}{"kind":"select_v3",...}都会被拒为 "cannot refresh";limit: 9007199254740993会被拒为 "too large to represent exactly"。

实战:创建、读取与刷新一个物化视图

前置条件:源表必须启用稳定行 ID

物化视图依赖源表的稳定_rowid作为行级溯源(视图内部列__source_row_id记录每行来自哪个源行)。因此源表创建时必须传newTableEnableStableRowIds: "true"存储选项,且该选项在表创建后无法再开启。测试中直接验证了这一点:对未启用稳定行 ID 的表创建视图,会抛出 "stable row ids" 错误。

import { connect } from "@lancedb/lancedb"; const db = await connect("./data"); // 源表必须启用稳定行 ID await db.createTable("people", [ { name: "ada", age: 36 }, { name: "kid", age: 7 }, { name: "grace", age: 85 }, ], { storageOptions: { newTableEnableStableRowIds: "true" } });

创建视图

Connection.createMaterializedView(name, source, options)(声明于 nodejs/lancedb/connection.ts)创建视图,在创建返回前即完成首次填充(除非withNoData: true)。options 支持selectwherelimitwithNoData

const view = await db.createMaterializedView("adults", "people", { select: ["name", ["shout", "upper(name)"]], // 裸列名 + [别名, 表达式] 对 where: "age >= 18", });

select参数的类型是MaterializedViewSelect,支持三种形态(见 nodejs/lancedb/materialized_view.ts):

type MaterializedViewSelect = | (string | [string, string])[] // 裸列名投影自身;[别名, SQL 表达式] | Record<string, string>; // { 别名: SQL 表达式 }

裸列名会被normalizeSelect自动用反引号转义(内部替换为 ``),因此任何合法列名(包括含空格的列名)都能安全使用——测试 "quotes bare select names" 用"order item"列名验证了这一点。而[别名, 表达式]` 对的右侧是表达式,按原样保留。

limit与后续refresh({ sourceVersion })都会经过validateNonNegativeInteger校验:InfinityNaN、负数、小数(1.5)在到达 Rust 之前就被拒绝——这是为了规避 N-API 会把Infinity静默转成 0、1.5转成 1 的隐患。测试对[-5, 1.5, Infinity, NaN]四种非法值逐一断言。

读取定义

创建后即可通过view.definition()读回规范查询:

const view = await db.openMaterializedView("adults"); const definition = await view.definition(); console.log(definition.query); // "SELECT name, age FROM people WHERE age >= 18"

视图本身就是一个普通表句柄——查询、建索引、向量搜索全部照常可用:

const rows = await view.table().query().toArray();

刷新视图

MaterializedView.refresh(options?)从源表重新计算视图内容(nodejs/lancedb/materialized_view.ts):

const result = await view.refresh(); // 或强制全量重建 / 刷新到指定源版本 await view.refresh({ full: true }); await view.refresh({ sourceVersion: 42 });

refresh返回RefreshMaterializedViewResult,其字段在 Rust 侧定义(rust/lancedb/src/materialized_view/refresh.rs):

字段含义
mode刷新方式:Rebuild(全量重建)、Incremental(增量追加/重算)、NoOp(已是最新)
rows_written写入行数:重建时为全部行;增量时为新增行与被重算行的总和
source_version视图现在反映的源表版本
version刷新后的视图表版本

RefreshMode三态在测试 "refreshes incrementally after an append" 中有完整演示:首次refresh()后追加一行,第二次refresh()返回mode: "incremental"rowsWritten: 1;再次刷新则返回mode: "no_op"

生命周期管理:列出与删除

  • db.listMaterializedViews():返回库中所有物化视图名。注意实现是"逐个读取每张表的 schema"来判断,因此代价是每个表一次 open(见 nodejs/lancedb/connection.ts 注释)。
  • db.openMaterializedView(name):打开视图;若表存在但不是物化视图则拒绝。
  • db.dropMaterializedView(name, namespacePath?):删除视图。视图可能在物理清理完成前就不可用。
  • db.dropMaterializedViewAsync(name, namespacePath?):异步删除,返回Job,可保留句柄等待清理完成。测试中job.idnulljob.wait()后视图确实消失。

并发刷新语义

同一视图的并发刷新不会重复写入行:两次刷新若规划到相同的源行,会在提交时冲突,失败方抛错而非二次写入。Rust 侧通过进程内按 URI 分片的refresh_lock互斥锁(rust/lancedb/src/materialized_view/refresh.rs 的refresh_lock),保证同一进程内同一视图同一时刻只有一个刷新在跑。

增量刷新原理:增量、重建与 NoOp 的判定

增量刷新是物化视图的核心价值。Rust 实现(rust/lancedb/src/materialized_view/refresh.rs)的判定逻辑分两层:

  1. 事务日志走查(transaction walk):从上次水印(mv.source_version)起读源表的 delta,识别出新增 fragment(append)、被Rewrite移动的 fragment、被更新原地修改的 fragment。此路径精确且高效。
  2. fragment 签名兜底(fragment-signature check):当事务走查读不了某些 delta 时,退化为纯追加检查——对比每个旧 fragment 在"视图所读列"上的签名(数据文件 + overlay + 删除文件)是否原样保留。压缩(compaction)、删除、更新都会破坏签名从而强制全量重建;而视图未读的列发生变化不影响签名,这正是兜底路径能放过事务走查读不到的 delta 的原因。

WHERE/投影中的表达式在规划时必须满足不可变性约束:ensure_immutable会拒绝任何非 immutable 的函数(如now()),也拒绝versionarrow_typeof等"标记为 immutable 但不随行值确定"的函数,否则增量维护会把不同次求值的结果混在同一视图里。测试 "rejects an invalid expression at create time" 验证了创建时的静态校验(missing + 1引用了不存在的列会立即报错)。

另外还有一些静态约束在规划期(plan)检查,而不是留到刷新期:

  • LIMITUNNEST不能同时使用——扫描的 limit 按源行计数,而 UNNEST 会把一行展开成多行,二者语义冲突;
  • limit超过i64::MAX被拒绝,保证创建与刷新对视图有效性的判断一致;
  • 视图列名__source_row_id_rowid为保留名;
  • 投影输出重名报ColumnAlreadyExists
  • WHERE 表达式必须是布尔谓词,否则报 "view filter must be a boolean predicate"。

使用限制与注意事项

  • 仅本地表支持刷新execute_refresh明确要求本地表(materialized views are supported only on local tables),且视图表不能处于 MemWAL/LSM 未压缩状态。
  • 函数形态视图依赖云端FROM位置的函数(Function)输出会暂存到隐藏表(mv.staging),本地数据库无法刷新该形态,仅 LanceDB Cloud 与 Enterprise 支持。
  • 旧布局兼容:format 1 之前的select/namespaced_select布局仍可读取并还原为 SQL,但只读不写;比 format 1 更新的布局一律拒绝并报告 "cannot refresh",绝不猜测。
  • 源表必须启用稳定行 ID,且创建后不可补开;否则创建视图直接报错。
  • listMaterializedViews成本较高:逐个 open 表读取 schema 元数据。

相关源码与测试索引

  • 接口定义与读取解析:nodejs/lancedb/materialized_view.ts
  • 连接层视图 API(创建/打开/列出/删除):nodejs/lancedb/connection.ts
  • Node 原生绑定:视图创建 nodejs/src/connection.rs、刷新与定义读取 nodejs/src/table.rs
  • Rust 核心:定义结构体、解析、规划与校验 rust/lancedb/src/materialized_view.rs
  • Rust 核心:刷新模式与增量算法 rust/lancedb/src/materialized_view/refresh.rs
  • 端到端测试(定义往返、增量刷新、非法输入、并发语义):nodejs/test/materialized_view.test.ts

综上,MaterializedViewDefinition虽只有一个query字段,其背后却串联了完整的"解析—规范渲染—元数据存储—格式版本化—增量维护"链路。理解这条链路,你就能准确预测视图定义读回的形式、识别哪些查询可以物化、以及刷新时每一步的结果与代价。

【免费下载链接】lancedbDeveloper-friendly OSS embedded retrieval library for multimodal AI. Search More; Manage Less.项目地址: https://gitcode.com/gh_mirrors/la/lancedb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询