Stanchion
SQLite 中的面向列的表
为什么选择 Stanchion?
Stanchion 是一个 SQLite 3 扩展,它为 SQLite 这个使用最广泛的数据库带来了面向列存储的强大功能。SQLite 仅支持面向行的表,这意味着它并不适用于所有工作负载。使用 Stanchion 插件可以将面向列存储和数据仓库的所有优势带到 SQLite 已经部署的任何地方,包括您现有的技术栈。
在许多情况下,面向列的存储性能优于面向行的存储:
- 存储和处理指标、日志和事件数据
- 时间序列数据存储和分析
- 对多行少列进行分析查询(例如计算数月每小时天气数据的平均温度)
- 变更跟踪、历史/时态表
- 锚点建模 / 类 Datomic 数据模型
Stanchion 非常适合分析查询和宽表,因为它只扫描给定查询引用的列的数据。它使用运行长度和位压缩编码等压缩技术,大大减少了存储数据的大小,极大地降低了大型数据集的成本。这使其成为存储大型、不断扩展的数据集的理想解决方案。
示例
从 Release 下载适用于您平台的预构建动态库或从源代码构建。
.load /path/to/libstanchion
CREATE VIRTUAL TABLE dnd_monsters
USING stanchion (
id INTEGER NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
size INTEGER NOT NULL,
challenge_rating FLOAT NOT NULL,
SORT KEY (id)
);
INSERT INTO dnd_monsters (id, name, type, size, challenge_rating)
VALUES
(1, 'Beholder', 'ABERRATION', 4, 13),
(2, 'Gelatinous Cube', 'OOZE', 4, 2),
(3, 'Mimic', 'MONSTROSITY', 3, 2),
(4, 'Lich', 'UNDEAD', 3, 21);
-- 因为 `dnd_monsters` 表是面向列的,所以以下查询
-- 只读取 `name`、`type` 和 `challenge_rating` 列的数据。
-- `id` 和 `size` 列中的数据根本不会被访问或扫描!
SELECT name
FROM dnd_monsters
WHERE type = 'UNDEAD' AND challenge_rating >= 18;
状态
Stanchion 处于 alpha 阶段。某些功能可能尚未完全实现。存储格式可能会以不向后兼容的方式发生变化。暂时不要在生产环境中使用。
构建
安装 Zig (master 版本)并克隆 stanchion
仓库。然后运行:
zig build ext -Doptimize=ReleaseFast
SQLite 扩展是 zig-out
目录中名为 libstanchion
的动态库。
运行测试
默认情况下,测试使用系统 SQLite 库。但是,stanchion 的构建可以选择下载并编译特定版本的 SQLite,并在运行测试时使用该版本。通过 -Dsqlite-test-version=$SQLITE_VERSION
参数来构建单元测试和集成测试。例如:
zig build test -Dsqlite-test-version=3.38.5
还可以为任何版本的 SQLite 启动 SQLite shell(这是一个用于调试的便利功能):
zig build sqlite-shell -Dsqlite-test-version=3.43.2
使用方法
加载 Stanchion
Stanchion 是一个使用 SQLite 虚拟表系统的运行时可加载扩展。要从 SQLite CLI 加载扩展,请使用 .load
命令。查看您正在使用的 SQLite 绑定的文档,了解如何在应用程序中加载扩展。以下是不同语言绑定的一些示例:Python 的 sqlite3
、Rust 的 rusqlite
、Ruby 的 sqlite3
和 Go 的 go-sqlite3
。
在加载 stanchion(或任何扩展)之前,您可能需要先启用扩展加载。以下是不同语言绑定的一些示例:Python 的 sqlite3
、Rust 的 rusqlite
和 Ruby 的 sqlite3
。某些绑定默认启用扩展加载(例如 Go 的 go-sqlite3
)。有关更多信息,请参阅 SQLite C API 文档。
Stanchion 既是持久性扩展,也是自动扩展。它只需要在一个进程中的一个连接中加载一次,然后它将自动被同一进程中的所有连接加载。从其他连接再次加载它也无害。连接到同一 SQLite 数据库的其他进程中的连接仍然需要加载 stanchion。
创建表
创建 stanchion 表的方式与在 SQLite 中创建任何表非常相似:
CREATE VIRTUAL TABLE sensor_log USING stanchion (
sensor_id TEXT NOT NULL,
timestamp INTEGER NOT NULL,
value FLOAT NULL,
variance FLOAT NULL,
severity INTEGER NOT NULL,
SORT KEY (sensor_id, timestamp)
)
USING stanchion
短语告诉 SQLite 将 sensor_log
创建为由 stanchion 实现的虚拟表。
所有 stanchion 表都需要 SORT KEY
。它定义了聚集索引,即表中记录的顺序。SORT KEY
不强制唯一性。目前,SORT KEY
只能包含列而不能包含表达式。
Stanchion 表不支持外键、主键、检查或唯一约束。在面向列的表更有用的场景中,这些约束通常不太有用,而且在面向列的数据库中并不广泛支持。但是,将来可能会将部分或全部这些约束作为选项引入 stanchion。
数据类型
下表显示了所有 stanchion 数据类型。布尔值在通过 SQLite 时会转换为整数。这允许它们在查询和 SQLite API 中使用,因为 SQLite API 不支持专用的布尔类型。有关 BOOLEAN
类型的更多信息,请参阅与 SQLite 的区别: BOOLEAN
类型。
Stanchion 类型 | SQLite 类型 | 别名 |
---|---|---|
BOOLEAN | INTEGER | BOOL |
INTEGER | INTEGER | INT |
FLOAT | REAL | REAL , DOUBLE |
BLOB | BLOB | |
TEXT | TEXT | VARCHAR * |
* 不支持字符计数
没有 ANY
类型,所有插入的值必须与声明的列类型匹配。在 stanchion 表中不可能进行动态类型。Stanchion 表大致相当于声明为 STRICT
的 SQLite 表(不包含任何类型)。
支持少量常用类型名称的别名。使用别名声明列与使用规范类型名称声明没有区别。
添加和查询数据
插入和查询数据的方式与 SQLite 中的任何其他表相同。Stanchion 表甚至可以使用 .import
命令等功能来向表中添加记录。
INSERT INTO sensor_log (sensor_id, timestamp, value, variance, severity)
VALUES
(2064, 12433702443, 74.37, 1.06, 1),
(2064, 12433703443, 73.12, 0.96, 1)
插入 Stanchion 表的值必须是列声明的类型。这相当于使用 STRICT
表选项声明 SQLite 表。
目前不支持更新和删除记录。未来将添加对更新和删除的支持。
Stanchion 表可以在 SQLite 中使用原生表的所有地方使用。在编写查询时,将 SORT KEY
视为复合索引以提高查询性能。在以下查询中,使用排序键来减少扫描的数据量。当然,只读取 sensor_id
、timestamp
和 value
列。
SELECT AVG(value)
FROM sensor_log
WHERE sensor_id = 2064 AND timestamp > 12433700000
与 SQLite 的区别
BOOLEAN
类型
Stanchion 有一个专用的 BOOLEAN
类型。布尔值在 stanchion 内部用作每个可空段的一部分,并公开以便也可以直接使用。
SQLite 使用 INTEGER
表示布尔值。Stanchion 在通过 SQLite 传递时将 BOOLEAN
值转换为 INTEGER
值。查询 BOOLEAN
stanchion 列会返回 INTEGER
SQLite 值。
由 SORT KEY
定义的聚集索引
在 Stanchion 中,表中记录的顺序(即聚集索引)由 SORT KEY
控制。目前,stanchion 中的每个表都必须有一个由 1 个或多个列组成的显式 SORT KEY
(目前不支持表达式)。它在创建表时声明,不能更改。与 PRIMARY KEY
不同,它不强制唯一性。
这与 SQLite 不同,SQLite 中的表默认按 ROWID
排序,或者如果表是 WITHOUT ROWID
表,则按 PRIMARY KEY
排序。
无唯一性(UNIQUE
或 PRIMARY KEY
)或外键约束
这可能在未来发生变化。实现这些功能可能需要外部索引。当/如果引入"主键"时,可能会合理地遵循Clickhouse的"MergeTree"引擎的做法,要求"主键"必须是"排序键"的前缀。
无外部索引
目前没有外部索引机制。用于优化查询的唯一索引是使用"SORT KEY"声明的聚集索引。
强制执行列类型
插入Stanchion表的值必须符合列的声明类型。这相当于使用"STRICT"表选项声明SQLite表。
暂不支持更新或删除
未来将在Stanchion中添加对"UPDATE"和"DELETE"的支持。
表结构无法更改
这是Stanchion希望支持的功能,但可能会很困难,因为SQLite没有记录支持虚拟表架构更改的方法。目前仍在调查中。
待办事项
即将推出的功能的高层次路线图(仅粗略排序):
- 重命名表
- 优化表
- 并发安全性和测试
- 更多编码:RLE、字典、Chimp等
- 字节级无损压缩(如zstd)
- 将SQLite原生表转换为Stanchion表的函数
- 发布与SQLite、DuckDB和chDB的基准测试
- "DELETE"和"UPDATE"
- 配置参数和调优
- 在iOS和Android应用中使用Stanchion的指南
- 架构更改(SQLite虚拟表本身不支持)
- 列表数据类型
数据存储内部结构
行组、段和条带
每个表中的记录分组为行组。每个行组最多包含固定数量的记录。行组是逻辑(非物理)分组,并按行组内的最小排序键值进行索引。目前,行组是不可变的,当新插入的记录数量超过阈值时,它们由一批新插入的记录(可能还包括现有行组的值)构建而成。
一个行组由多个段组成。一个段包含单个列的数据。可以将每个行组视为表中的一个数据跨度,而每个段则是列中的一个数据跨度。一个段由单个SQLite"BLOB"值支持。目前,段是不可变的。
每个段由1个或多个条带组成。条带是同一类型的数据块,由段的"BLOB"值的一部分(片段)支持。一个段可以包含以下条带:存在、主要和长度。当段包含空值时,主要条带用于指示每个记录中的值是否不为"NULL"。对于"BOOLEAN"、"INTEGER"和"FLOAT"列,值存储在主要条带中。对于"TEXT"和"BLOB"列,值的字节存储在主要条带中,每个值的长度存储在长度条带中。
待处理插入
当记录添加到Stanchion表时,它们被插入到一个标准的持久化B+树(即原生的、面向行的SQLite表)中,称为待处理插入表。由于它是一个原生的SQLite表,记录以面向行的方式存储。它们按排序键排序,以便能够高效地合并到行组中。
首先将记录存储在待处理插入表中的原因是,创建段需要拥有将进入该段的所有数据。创建段时,使用的编码取决于要存储在段中的值,并非所有编码都支持高效追加。此外,当数据表现出可被编码利用的模式,且每个段有更多数据时,可以实现更高的压缩率。待处理插入表充当缓冲区,在其中存储记录,直到有足够的记录以创建段为止。
当查询按排序键列进行筛选时,Stanchion将该筛选器直接应用于待处理插入,以限制访问哪些待处理插入。按排序键筛选是Stanchion目前唯一支持的索引机制。
行组索引
行组索引是一个原生的、面向行的SQLite表,它按每个行组的起始(最小)排序键对行组进行索引。当查询按排序键列进行筛选时,Stanchion将该筛选器应用于主索引,以限制访问哪些行组。按排序键筛选是Stanchion目前唯一支持的索引机制。