列式存储即规范化
摘要
本文将列式存储重新定义为数据库规范化的极端形式,展示了把属性拆分为位置对齐的数组如何与基于隐式序数主键连接的规范化表如出一辙。
暂无内容
查看缓存全文
缓存时间: 2026/04/22 13:13
# 列式存储就是规范化
来源:https://buttondown.com/jaffray/archive/columnar-storage-is-normalization
我以前一直没意识到,把行式数据转成列式数据,并不是数据库领域里什么全新的概念,它仍然属于关系抽象的一部分——或者说,可以被看作是关系抽象。
举个例子,我们有这样一份数据:
```json
data = [
{ "name": "Smudge", "colour": "black" },
{ "name": "Sissel", "colour": "grey" },
{ "name": "Hamlet", "colour": "black" }
]
```
这相当于关系数据库里的一张表。假设它真的存在数据库里,每次访问任意字段都得做磁盘 IO。这种表示法有一些优点:
- **加行方便**:只要拼出一行 `{ "name": "Petee", "colour": "black" }`,追加到列表末尾即可;磁盘上可能只需改几个页。即使这一行有几十列,也依旧如此。
- **查行迅速**:因为同一行的所有列在物理上紧邻,把整行捞出来很快。
反过来,如果想统计宠物颜色的直方图,就得把大量无关数据(比如名字)一起读进来——效率很低。
这就是**行式存储**。换成**列式存储**,数据长这样:
```json
data = {
"name": [
"Smudge",
"Sissel",
"Hamlet"
],
"colour": [
"black",
"grey",
"black"
],
}
```
优缺点正好反过来:只关心 `colour` 时,可以只读这一列,名字一字节都不用碰;但改数据或按行查询就麻烦——要到处跳。如果想拿第二行,就得去每个列数组的第二个位置把字段拼回来。
一种理解是:这只是**编码层**的事,位于数据模型之下;SQL 引擎逻辑上区分不了两种格式,只能通过查询性能感知差异。
另一种理解是:**列式化就是一种极端的数据库规范化**。
不再是一张“宽表”对应一堆向量,而是把每列拆成一张小表,主键再加一个属性:
非规范化表:
```
+----+------+-----+
| id | name | age |
+----+------+-----+
| 12 | Bob | 30 |
| 93 | Tom | 35 |
| 27 | Kim | 28 |
+----+------+-----+
```
规范化后:
### Name 表
```
+----+------+
| id | name |
+----+------+
| 12 | Bob |
| 93 | Tom |
| 27 | Kim |
+----+------+
```
### Age 表
```
+----+-----+
| id | age |
+----+-----+
| 12 | 30 |
| 93 | 35 |
| 27 | 28 |
+----+-----+
```
用 `id` 做 join 就能拼回原表。
在列式存储里,可以把“主键”看成数据在数组里的**下标**。
原数据:
```json
data = {
"name": [
"Smudge",
"Sissel",
"Hamlet"
],
"colour": [
"black",
"grey",
"black"
],
}
```
可以视为:
```
+----+--------+
| id | name |
+----+--------+
| 0 | Smudge |
| 1 | Sissel |
| 2 | Hamlet |
+----+--------+
+----+--------+
| id | colour |
+----+--------+
| 0 | black |
| 1 | grey |
| 2 | black |
+----+--------+
```
但 `id` 其实只是数组下标,可以隐去:
```
+--------+
| name |
+--------+
| Smudge |
| Sissel |
| Hamlet |
+--------+
+--------+
| colour |
+--------+
| black |
| grey |
| black |
+--------+
```
这种视角的价值在于:它把传统的投影、join 等查询操作,与数据格式的转换统一了起来。大多数时候,你确实应该把数据格式当成对查询逻辑透明的实现细节;但心里要明白——**“从列存里拼回一行”不仅看起来像 join,它本身就是 join。**
相似文章
用于分析的纯 Clojure 列式数据库
Flatiron 是一个纯 Clojure 的列式分析库,用于内存表,具有类似 SQL 的 DSL,专为使用原始数组和批处理的高性能而设计。
深入解析Postgres内部:数据库集群、数据库与表
一篇探讨PostgreSQL内部机制的技术文章,涵盖数据库集群、数据库、表、系统目录和对象标识符(OID)的逻辑与物理结构。
@MrCollison: 关于构建游戏引擎最疯狂的事情是,我学到了一个曾经让我最害怕的概念:列式存储。它……
作者分享了自己在构建游戏引擎时对列式存储的个人突破性理解,并将其与Trizen的ECS和ClickHouseDB联系起来。
结构化主键
本文讨论了传统主键设计如何导致表孤立,并介绍了结构化主键作为一种替代方案,以提高SQL查询性能并维护关系完整性。
VictoriaLogs 如何在列式布局中存储日志
深入探讨 VictoriaLogs 如何在列式布局中存储日志,涵盖数据摄入、流标识和压缩。