Menu

SQLite 生成列:VIRTUAL 与 STORED 详解及示例

SQLite 生成列的工作原理:如何声明,如何在 VIRTUAL 和 STORED 之间选择,以及如何给它们建立索引来加快查找。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

生成列就是计算列

生成列的值来自一个表达式,而不是来自 INSERT。你在 CREATE TABLE 中声明一次公式,剩下的交给 SQLite。你永远不会写入它:试图写入会报错。

最简短的例子:

total 从未被插入,却出现在每一行中。每次读取这一行时,SQLite 都会根据 price + tax 重新计算它。更新其中任何一列,total 都会跟着变。

关键字短语 GENERATED ALWAYS AS 是必须写的。其中的 ALWAYS 只是 SQL 标准的形式要求:在 SQLite 中没有别的选项。

VIRTUAL 与 STORED

每个生成列都属于两种类型之一。默认是 VIRTUAL:

可以这样理解:

  • VIRTUAL:磁盘上占零字节,每次读取都消耗 CPU。添加成本低,以后修改也容易。
  • STORED:占用磁盘空间,读取时没有额外开销。当表达式开销大,或者这一列的读取远多于写入时,它更划算。

如果不写关键字,得到的就是 VIRTUAL。这几乎总是正确的默认选择。

为什么要用?可以建索引的派生值

最强大的特性是:你可以在生成列上建立索引。这样就能对“派生”值进行快速查找,而不必改写每一条查询。

假设你想按邮箱查找且不区分大小写:

索引覆盖的是小写形式。按 email_lower 过滤的查询会直接使用这个索引。SQLite 也支持表达式索引(CREATE INDEX ... ON users(lower(email))),但生成列把派生值变成了一个真实的列,你可以 SELECT 它,在视图中引用它,也可以在应用代码中复用它。

从 JSON 中提取值

生成列和 JSON 搭配使用时尤其出色。SQLite 的 JSON 支持提供了 ->> 来提取标量值;把它包进一个生成列,你就在灵活的数据块之上得到了一个有类型、可索引的字段。

在你的查询看来,user_id 和 kind 就像普通列一样,但数据实际存放在 payload 中。修改 JSON,这些列也会随之更新。user_id 上的索引让查找变得很快。

规则与限制

SQLite 会强制执行以下几条规则,最好在碰到之前就了解:

  • 表达式必须是确定性的。random()、datetime('now') 等非确定性函数都不允许使用。值必须能从这一行重新算出来。
  • 表达式只能引用同一行中的列。不能用子查询,不能用聚合,也不能引用其他表。
  • 你不能直接对生成列执行 INSERT 或 UPDATE。INSERT INTO products (total) VALUES (5) 会报错。
  • 不能用 ALTER TABLE ... ADD COLUMN 添加 STORED 列。事后只能添加 VIRTUAL 列。
  • 生成列可以带有 NOT NULL、CHECK、UNIQUE,甚至 FOREIGN KEY 约束。在这些方面,它们和其他列没有区别。

快速演示一下写入规则:

sqlite> INSERT INTO products (price, tax, total) VALUES (10, 1, 999);
Runtime error: cannot INSERT into generated column "total"

解决办法是把生成列从 INSERT 的列列表中去掉,让 SQLite 自己计算。

选择 VIRTUAL 还是 STORED

这个决定通常取决于读写比例和表达式的开销:

经验法则:

  • 默认使用 VIRTUAL。它在写入时没有开销,几乎适用于所有场景。
  • 当你在写入频繁的表上给这一列建立索引(索引本来就需要持久化这个值),或者表达式确实开销很大时,改用 STORED。
  • 不必为此纠结。类型是表结构的一部分,但如果你改变主意,可以删除列再重新创建,至少 VIRTUAL 列可以这样做。

生成列与视图

生成列和视图有重叠之处:两者都能在不存储的情况下暴露计算出的值(嗯,有时候是这样)。通常的分工是:

  • 生成列属于某一行、某一张表。用它做逐行的派生:格式化邮箱、提取 JSON 字段、计算总额。
  • 视图是保存下来的查询。当计算涉及连接、聚合或跨行过滤时使用它。

两者可以混用。视图可以从带有生成列的表中 SELECT,并连接其他表补充上下文。生成列位于存储层,视图位于查询层。

下一篇:ATTACH DATABASE

生成列让一张表自己计算出值。下一页则反过来:用 ATTACH DATABASE 同时连接多个 SQLite 数据库,让一条查询可以跨越多个文件。

常见问题

SQLite 中的生成列是什么?

生成列的值由一个表达式计算得出,这个表达式引用同一行中的其他列。你在 CREATE TABLE 中用 GENERATED ALWAYS AS (expression) 声明它。你从不直接写入它:每当读取或保存这一行时,SQLite 会替你计算。

VIRTUAL 和 STORED 生成列有什么区别?

VIRTUAL 列在每次读取时计算,不占磁盘空间,它是默认类型。STORED 列在写入时计算一次并保存到数据库文件中,读取更便宜,写入稍微贵一些。两者都可以建立索引,但当表达式开销大,或者这一列的读取远多于写入时,STORED 通常是正确的选择。

SQLite 中可以给生成列建立索引吗?

可以。CREATE INDEX 对生成列有效,VIRTUAL 和 STORED 都行。这正是使用生成列的主要原因:你可以给派生值(比如 lower(email),或用 ->> 提取的 JSON 字段)建立索引,让查询规划器使用这个索引,而不必改写每一条查询。

可以用 ALTER TABLE 添加生成列吗?

可以,但只限于 VIRTUAL 列。ALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS (...) VIRTUAL 完全可行。不支持用 ALTER TABLE 添加 STORED 生成列,你只能重建整张表。如果想在已有的表上使用存储列,请提前规划。

Coddy 编程语言插图

和 Coddy 一起学编程

开始