
技术博客探讨行式与列式数据存储的本质差异
来源标题:Rows vs. Columns: How different are they, really?
一篇技术博客以纽约共享单车CSV数据为例,分析行式与列式数据组织的根本区别。文章指出,行式存储符合人们按「一个事物及其全部细节」思考的习惯,常见于电子表格、JSON和SQL表。博客随后探讨列式存储在分析场景下的不同取舍。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
返回博客行存储 vs 列存储:它们到底有多大区别?
Connor Tsui LinkedInHacker NewsReddit
当我们想到结构化数据时,通常会以行的形式来思考。看看这个简单的 CSV 文件,它记录了纽约市一个共享单车应用中一些骑行数据:
CSV DoneCopy ride_id, started_at, start_area, bike_type, duration_min 1001, 2026-11-02 07:58, Tribeca, classic, 14 1002, 2026-11-02 08:01, Financial District, electric, 6 1003, 2026-11-02 08:01, SoHo, classic, 21 1004, 2026-11-02 08:03, Financial District, electric, 8 1005, 2026-11-02 08:04, Financial District, classic, 15 1006, 2026-11-02 08:07, Tribeca, electric, 1 1007, 2026-11-02 08:08, Tribeca, electric, 9 1008, 2026-11-02 08:12, Financial District, classic, 31
这个 CSV 文件中的数据按行组织,每一行对应一条数据记录。这里的每条记录都是一次骑行,或者说一个“事物”,关于它的一切信息都在同一行上。我们几乎在所有地方都倾向于这样组织数据:想想电子表格、JSON 对象、数组中的 C 风格结构体、SQL 表等等。这是因为这种方式符合我们的思维方式:一个“事物”,以及这个“事物”的所有细节。
这种思维方式契合我们编写的许多软件。当你登录一个应用、在视频下发表评论、把商品加入购物车、发起银行转账或预订行程时,应用程序只会改动一条记录(最多几条)。因此,支撑这些软件的数据库,比如 Postgres 和 MySQL,也“以行来思考”(或者说围绕行来建模)就顺理成章了:它们把每条记录的细节放在一起,所以查找、更新或创建一条记录都只需要去一个地方。
但我们向数据库提出的请求并不都局限于一条记录。
如果你运营着那个生成了上面 CSV 文件的共享单车服务,你最终会想从它收集的所有数据中学到一些东西。这类工作叫做分析。例如,周一和周五早上哪个街区最繁忙?人们骑电动车的时间是否比骑经典车更长?注意,没有任何单条记录能回答这些问题。答案来自整个数据集,所以唯一的办法就是查看所有记录。¹
不过要注意,这些问题并不需要什么。找出最繁忙的街区只需要 start_area 和 started_at 两列,比较车型只需要 bike_type 和 duration_min。每个问题可能都得查看每一行,但每行只需要其中几列。而一个把每条记录细节放在一起的数据库无法利用这一点:要读两列,它必须读全部五列。我们可能以行来思考,但要快速回答这类问题,我们必须以列来编程。
以列来编程始于数据的存储方式,但这只是开始。一旦数据按列存储,数据库执行查询的方式就会发生巨大变化:读取多少数据、一次处理多少个值、CPU 用一条指令能处理多少个值、数据压缩效果如何,等等。
本文将逐一介绍这些变化,读到最后,你会看到“简单地”选择按列而非按行存储,会带来多么深远的影响。
让我们从不变的地方说起。无论数据库存储行还是列,当你执行 SELECT * FROM rides; 时看到的表都是一样的。你用同样的 SQL 创建它,用同样的 SQL 查询它,得到同样的答案。我们可以说这两张表在逻辑上是等价的。
SQL DoneCopy CREATE TABLE rides ( ride_id BIGINT, started_at TIMESTAMP, start_area TEXT, bike_type TEXT, duration_min INTEGER );
SELECT * FROM rides WHERE ride_id = 1004;
SELECT AVG(duration_min) FROM rides;
行与列是物理层面的区别,而非逻辑层面的:数据表示的是同一件事,但数据在内存和磁盘上实际的排列方式不同。
数据库以其选择的分组方式命名。行存储把每条记录的值放在彼此旁边,就像我们开头那个 CSV 一样。我们前面提到的 Postgres 和 MySQL 都是行存储。它们是为应用程序背后的少量读写而构建的,这被称为联机事务处理(OLTP)。²
列存储则相反:它把每条记录拆开,把每一列的值放在彼此旁边。³ DuckDB 和 ClickHouse 就是列存储的例子。它们是为分析而构建的,这被称为联机分析处理(OLAP)。⁴
具体来说,假设我们必须自己在内存中保存这些骑行数据。这不是真实数据库的实现方式,但它很好地近似了两种存储各自排列数据的方式。在 Rust 中,行存储会是一个结构体的 Vec:
Rust DoneCopy struct Ride { ride_id: u64, started_at: Timestamp, start_area: String, bike_type: BikeType, duration_min: u32, }
type Rows = Vec<Ride>;
列存储会有相同的字段,但它看起来像一个由 Vec 组成的结构体:
Rust DoneCopy struct Columns { ride_id: Vec<u64>, started_at: Vec<Timestamp>, start_area: Vec<String>, bike_type: Vec<BikeType>, duration_min: Vec<u32>, }
下面是这两种数据布局并排对比的样子:
这两种形式表示相同的逻辑值,但它们的布局截然不同。
让我们做个小练习。⁵ 下面是前面展示的两条 SQL 查询。对每条查询来说,哪种布局执行得更好?
SQL DoneCopy SELECT AVG(duration_min) FROM rides;
SELECT * FROM rides WHERE ride_id = 1004;
对于 AVG 查询,计算它只需要每次骑行的 duration_min,别的什么都不需要。在 Columns 布局中,值都存放在一个 Vec<u32> 里,所以查询可以只扫描那一个数组,而不碰其他四个。对于 Vec<Ride>(Rows 布局),根本没法只读
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。