老顾是社区图书馆的档案管理员。最初,每一份读者档案都装在一个透明文件袋里:袋子里依次放着姓名卡、住址卡、年龄卡、电话卡。老王觉得这样很合理——想了解一个人,抽出一个袋子就看全了。可馆长总让他统计“全馆读者的平均年龄”或“按街道统计人数”。每到这时,老王就要把几千个袋子挨个抽出来,翻到年龄卡,抄下来,再放回去,累得腰酸背痛。 后来,一位新来的实习生提议:为什么不在书架的另一侧,按“信息类别”重新归档?把所有“年龄卡”装进一个盒子,所有“住址卡”装进另一个盒子。老王照做了。神奇的事情发生了:统计平均年龄时,他只需要搬一个盒子;统计年龄分布时,他只需要翻一盒卡片。而偶尔查某个人的完整信息时,他只要从五个盒子里各抽一张卡拼起来。 老图书馆的这次“档案室大改造”,正是数据仓库从行式存储走向列式存储的缩影。行式存储像“一人一袋”,简单直观;列式存储像“按类别分盒”,把同一种属性放在一起。大多数数据仓库的分析查询,只关心少数几个属性。把同类数据聚在一起,就能少搬用不到的“袋子”,还能把相同的内容压得更紧。本文就用这个故事,讲清列式存储的原理:它如何省 IO、如何压缩、什么场景下反而吃亏,以及向量化执行如何让它如虎添翼。
对 AI 从业者来说,理解列式存储意味着理解数据仓库“为什么快、何时快”,是数仓建模、查询优化与特征工程性能调优的底层直觉。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
存储布局差异
老顾的档案室改造,看起来只是“换了个整理柜”,但本质是把“信息摆放的位置”重新规划了一遍。改造前,一个透明袋子里装着某个人的全部卡片,袋子和袋子一个接一个排在书架上——这就是行式存储:一条记录的所有字段在物理空间上连续存放。改造后,书架另一侧的盒子按属性分类,所有“年龄卡”排成一组,“住址卡”排成另一组——这就是列式存储:一列的所有取值在物理空间上连续存放。
在数据库里,这种差异落到数据块(Block/Page)层面。行存写入时把一行塞进同一数据块,列存写入时把各列分别写进各自的块区域。逻辑上它们都是二维表,物理上却天差地别。数据块是磁盘和内存交换的最小单位,所以“同一类数据挨得近不近”,直接决定了读取效率。
我们用一个抽象模型描述两种布局:假设表有 $N$ 行、$m$ 列,第 $r$ 行第 $j$ 列的值为 $a_{rj}$。
物理布局的差异,就像档案室的两种摆法:一种以“人”为单位聚拢,一种以“属性”为单位聚拢。摆法本身没有绝对好坏,却决定了后面所有性能故事的走向。
D_{\text{row}} = \bigcup_{r=1}^{N}\,[\,a_{r1},a_{r2},\dots,a_{rm}\,]\qquadD_{\text{col}} = \left\{\,[\,a_{11},a_{21},\dots,a_{N1}\,],\; [\,a_{12},a_{22},\dots,a_{N2}\,],\;\dots\right\}- 行存连接第 $r$ 条记录的 $m$ 个字段按顺序写在一起,就像一套档案装进同一个袋子。
- 列存分组把第 1 列的所有行的值集中到一段空间,第 2 列再集中到另一段空间。
- 物理含义列存中读取“年龄列”只需访问一段连续区间,而行存需要扫过所有袋子才能挑出年龄卡。
列存IO优势
改造后的一个清晨,馆长又要老王统计“全馆读者的平均年龄”。老王不慌不忙,径直走向新书架,抱起那个贴着“年龄卡”的盒子,一本一本地记录。整个过程没有动过任何一个透明袋。他心里清楚:如果是以前,得把几千个袋子全搬出来。
在数据库里,“搬袋子”就是磁盘 IO。行存执行 SELECT AVG(age) FROM users 时,存储引擎只能以行为单位把数据块读入内存,记录中那些与 age 无关的 name、address、phone 字段也跟着一起被搬了进来。列存则只需要读取 age 列所在的连续区域,其余列的数据块根本不会被触达。
假设表有 $N$ 行、$m$ 列,第 $j$ 列平均行宽(每行该列的字节数)为 $L_j$,一次分析查询只用到列集合 $\mathcal{S}$,扫描 IO 量的对比如下:
直观地说,一张 50 列的宽表,分析报表只用到 5 列,列存可以少搬 90% 的数据。数据少搬了,内存带宽、磁盘吞吐、乃至网络传输(在分布式数仓中)全线受益。老顾的腰不酸了,查询也肉眼可见地变快了,这就是“按需取料”的力量。
- 行数 N无论哪种布局,要统计全表平均值,都要“经过”每条记录。
- 列宽求和列存只累计选中列的宽度 $\sum_{i\in\mathcal{S}}L_i$,行存必须累计全部 $m$ 列的宽度。
- 远小于号当 $\mathcal{S}$ 只是全集的一个小子集时,二者差距成倍放大。
列存压缩优势
又过了一周,老王在整理“性别卡”时发现一个有趣的现象:一盒上千张卡片,翻来覆去只有“男”“女”两个词。他灵机一动,在盒子外贴了一张说明——“0 代表男,1 代表女”,然后把盒子里所有卡片都换成 0 或 1 的数字。盒子瞬间瘦了一大圈。
这正是列存压缩的核心逻辑:同一列的数据类型一致、值域集中,所以编码手段可以非常激进。最常用的是字典编码(Dictionary Encoding):为列中的不同值建立一个“字典”,每行只存一个紧凑的整数编号。行存中一行的字段五花八门,很难用统一编码压缩;而列存中一段连续区域全是同类数据,压缩率往往能达到 10:1 甚至更高。
设一列有 $N$ 行,每个原始值占 $L_c$ 字节,去重后有 $V$ 种不同取值,字典编码后每行只需 $\lceil \log_2 V \rceil$ 比特来存储编号,压缩率如下:
以“性别”列为例:$V=2$,编号只需 1 比特,原始值若用 UTF-8 中文存储则每行 3 字节即 24 比特,压缩率接近 24 倍。即使考虑字典开销,也远比行存划算。除了字典编码,还有运行长度编码(RLE,对“连续相同的值”只记值和重复次数)、位图编码等。同质数据聚在一起,是这一切压缩魔法的前提——这正是列存的天然优势。
- 原始大小$N$ 行每行 $L_c$ 字节,换成比特就是 $N \times 8L_c$。
- 字典大小$V$ 个不同值各存一份原始值,占 $V \times 8L_c$ 比特。
- 编号大小$V$ 种取值需要 $\lceil \log_2 V \rceil$ 比特表示,$N$ 行共占 $N\lceil\log_2 V\rceil$ 比特。
- 压缩率原始总量除以压缩后总量,大于 1 就说明有效省空间。
场景对比分析
档案室改造完成后,老王也发现了一些不便。某天一位读者来查“张三的完整资料”,老王得从五个盒子里各抽一张卡,再拼成一套档案;而在改造前,他只需要抽一个袋子。两种归档方式各有各的舒服区:查“一个人”行存快,统计“一类属性”列存快。
这个规律在数据库中同样成立。点查(Point Query)如 SELECT * FROM users WHERE id = 42,需要返回一条记录的多个字段。行存只需一次 IO 定位并读出整行;列存却要分别访问多个列文件,再把对应位置的值拼接起来。反之,聚合扫描(Aggregation Scan)如 SELECT SUM(amount) FROM orders,只关心一列,行存却要把整张表的全部字段都搬出来,IO 浪费严重。
两种场景的 IO 特征可以浓缩为:
所以列存并非万能灵药。真实的数据仓库往往负载混合:既有大范围聚合统计,也有少量明细点查。优秀的列存引擎会为点查保留主键索引或一跳(skip)索引,把劣势尽量补平。老顾的结论是:先看清图书馆的主要业务是“查个人”还是“做统计”,再决定书架怎么摆。
IO_{\text{point}}^{\text{row}} \approx L_{\text{row}} \quad;\quadIO_{\text{point}}^{\text{col}} \approx L_{\text{row}} + m \times \deltaIO_{\text{agg}}^{\text{row}} = N \times L_{\text{row}} \quad;\quadIO_{\text{agg}}^{\text{col}} = N \times \sum_{i \in \mathcal{S}} L_i- 点查行存按主键定位到数据块后,读一次整行即可。
- 点查列存一行数据分散在 $m$ 个列文件中,每个文件都要一次额外定位。
- 聚合行存全表扫描时每行都完整读取,行数与整行宽相乘。
- 聚合列存只读选中列,IO 与选中列宽成正比。
查改性能特征
老王很快又遇到新麻烦。一位读者搬家了,需要同时修改住址、电话和紧急联系人。在改造后的小盒子模式下,老王得在三个盒子里各翻出对应的卡片改一遍;而在旧档案袋模式下,抽出一个袋子,改完放回,一次搞定。
列式存储的更新代价,正是“一行数据被拆散到了多个列文件”带来的。执行 UPDATE 时,扫描阶段还能享受列存优势,但写入阶段必须定位并重写涉及的所有列:一是要找到该行在每列中的物理位置,二是每个列文件的对应片段都要重写。列越多、单行字段越宽,写放大(Write Amplification)越严重。
设字段数为 $m$,单列平均更新代价近似为“读取定位 + 写入重写”:
两者的“读改写总字节”看起来相同,但列存每列都是一次独立的随机 IO,定位成本 $m \times \delta$ 会额外叠加,并发更新时锁的分散也加剧冲突。
正因如此,以高并发小事务更新为主的 OLTP 系统(如订单履约、账户余额)普遍采用行存加索引,而 OLAP 数据仓库以批量导入和不可变分区为主,天然规避了列存的更新短板。老顾最后在档案室立了条规矩:需要频繁改动的读者信息,保留透明袋归档;只读的分析卡片,全部放进属性盒。
C_{\text{update}}^{\text{row}} \approxL_{\text{row}} + L_{\text{row}} = 2L_{\text{row}}- 读旧值更新前必须把旧行/旧列值读出来,列存要分别读 $m$ 个列片段。
- 写新值写入新值同样要落盘到 $m$ 个列文件中。
- 额外定位每个列文件独立寻址,新增 $m \times \delta$ 的随机 IO 开销。
向量化执行
周末,老王又要统计“各年龄段人数”。他面对一整盒年龄卡,发现卡片上的数字排得整整齐齐。他突然想到:与其一张一张地看,不如把这一摞卡片放进扫描仪,一次扫过一整页,电子表格直接算出每一档的数量。这种“一次处理一整组”的思路,在 CPU 里叫做向量化执行。
列存为向量化提供了天然土壤:同一列的数据在内存里连续且类型一致,CPU 可以用 SIMD(单指令多数据)指令一次对多个数执行相同的运算。比如一次加法指令同时处理 4 个或 8 个整数。此外,连续内存访问对 CPU 缓存高度友好:读第一列时,后续值大概率已在缓存行里等你了。行存中字段类型混杂、值分布零散,很难让 SIMD 高效工作。
设查询要对 $n$ 个值做同样的运算,标量模式需要执行 $n$ 条指令,向量模式每条指令同时处理 $V_L$ 个数,理想加速为:
结合压缩后的数据,向量化还能直接在编码/压缩数据上做部分运算,进一步省去解压。列存、压缩、向量化三者环环相扣:列存提供连续同质数据,压缩减小数据量,向量化让每个 CPU 周期干更多活。老顾的扫描仪,让档案室真正步入了“自动化时代”。
- 标量执行$n$ 个数据逐个处理,共执行 $n$ 条运算指令。
- 向量执行每条指令处理 $V_L$ 个数据,总指令数降为 $n / V_L$。
- 加速比两者相消后,加速比就是 $V_L$。
工程实践落地
几年后,老顾的图书馆声名远扬,隔壁几家图书馆都来取经。老王总结出了一套“书架哲学”:需要频繁增删改的读者登记表,用透明袋逐人归档;承载年度统计、读者画像分析的大宽表,按属性分盒存放。两种归档方式共存,各司其职。
这套哲学映射到数据库选型上,就是 OLTP 与 OLAP 的分工。现代数据仓库引擎(ClickHouse、Doris、以及 Hive/Spark 中的 Parquet、ORC 文件格式)普遍采用列式存储,搭配压缩、索引和向量化执行,把分析查询的 IO 和 CPU 开销压到极致。而 MySQL、PostgreSQL 等事务型数据库坚持行存加 B+ 树索引,保证点查与更新的事务性能。数仓建模时的“宽表”设计,正是为了让报表查询命中尽量少的列文件。
工程上可以定义一个简单的收益评估,帮助判断列存是否适配某个负载:假设聚合类查询占比 $w_{\text{agg}}$,点查类占比 $w_{\text{point}}$,
老顾的故事到此圆满:先分类,再选择合适的位置存放,最后用对的工具批量读取。列式存储不是取代行式存储,而是让数据仓库在分析世界中找到自己的主场。数据从业者掌握这套原理后,再看数仓建模和查询优化,就能做到“心中有一张书架图”。
- 聚合收益项聚合类查询从行存切换为列存后,IO 节省的倍率作为收益。
- 点查损失项点查类查询在列存下的 IO 劣势倍率作为损失。
- 加权求和按负载占比加权,净值为正说明列存整体划算。
AI 应用场景
电商数仓的经营分析报表
阶段:数据分析与决策支持(OLAP)
解决的问题:传统行存执行“按省份、按品类统计今日 GMV”这类聚合查询时,需要全表扫描订单明细的所有字段,几十亿行数据带来巨大的 IO 开销,报表分钟级都出不来。列存只读取“金额、省份、品类”三列,IO 减少一个数量级,配合向量化聚合,秒级出数。
具体案例:某电商平台大促期间,运营看板需要实时查看“各品类在各渠道的销售趋势”。订单明细表按天分区存储在 ClickHouse 列式表中,查询只扫描必需的列,配合主键索引跳过无关分区,看板刷新时间从 3 分钟降到 2 秒。
用户行为日志的特征工程
阶段:特征提取与特征工程
解决的问题:推荐系统每天要处理 TB 级埋点日志,日志包含上百个字段,但训练特征只用 user_id、item_id、行为类型、时间戳等少数几列。若按行存读取,每次特征流水线都要搬运全部字段,存储和 IO 成本极高。列式文件格式(Parquet/ORC)按列读取 + 高压缩,让特征抽取又快又省。
具体案例:某短视频推荐团队用 Spark 读取近 7 天用户点击日志构建训练样本,日志以 Parquet 列存存储。特征工程只取 4 列进行过滤和 join,扫描数据量下降了约 70%,特征任务从 40 分钟压缩到 12 分钟。
监控指标与异常检测
阶段:在线监控与时序异常检测
解决的问题:云平台监控系统每秒写入数百万条指标,查询又经常是“过去 5 分钟所有机器的平均 CPU 使用率”。指标数据按“时间 + 标签”组织,同标签的指标值天然连续存放,列式时序数据库能高效压缩并加速聚合,让异常检测模型快速拿到聚合后的指标序列。
具体案例:某云计算厂商使用列式时序数据库存储百万级机器的指标,同一指标序列连续存储并做增量压缩,支撑“集群平均负载”的秒级聚合查询,为 AIOps 异常检测模型提供了实时特征输入。
Python 演示
# -*- coding: utf-8 -*-"""列式存储原理模拟: IO对比与字典编码压缩"""import random random.seed(42)N = 10000 def gen_rows(n): cities = ["北京", "上海", "广州", "深圳", "杭州"] genders = ["男", "女"] rows = [] for i in range(n): rows.append({ "id": i, "city": random.choice(cities), "age": random.randint(18, 60), "gender": random.choice(genders), "score": random.randint(0, 100), }) return rows def to_columnar(rows): cols = {} for col in rows[0]: cols[col] = [r[col] for r in rows] return cols def io_row_scan(rows): """行存扫描: 每行读出所有字段""" bytes_read = 0 for r in rows: for v in r.values(): if isinstance(v, int): bytes_read += 8 else: bytes_read += len(v.encode("utf-8")) return bytes_read def io_col_scan(cols, need): """列存扫描: 只读需要的列""" bytes_read = 0 for c in need: for v in cols[c]: if isinstance(v, int): bytes_read += 8 else: bytes_read += len(v.encode("utf-8")) return bytes_read def dict_encode(values): code_map = {} code_list = [] for v in values: if v not in code_map: code_map[v] = len(code_map) code_list.append(code_map[v]) return code_map, code_list def main(): rows = gen_rows(N) cols = to_columnar(rows) need = ["age", "gender"] row_io = io_row_scan(rows) col_io = io_col_scan(cols, need) print("行存扫描全部字段: {:,} 字节".format(row_io)) print("列存只读 age+gender: {:,} 字节".format(col_io)) print("IO 节省比例: {:.1%}".format(1 - col_io / row_io)) code_map, code_list = dict_encode(cols["gender"]) raw = sum(len(v.encode("utf-8")) for v in cols["gender"]) encoded_bits = len(code_list) encoded_bytes = (encoded_bits + 7) // 8 + 20 print("gender 原始大小: {:,} 字节".format(raw)) print("字典编码后大小: {:,} 字节".format(encoded_bytes)) print("压缩率: {:.1f}x".format(raw / encoded_bytes)) if __name__ == "__main__": main()
rows = gen_rows(N)生成 1 万条用户记录,每条包含 id、city、age、gender、score 五个字段。to_columnar(rows)把行式数据转成列式结构:每个字段对应一个列表,模拟列存的物理布局。io_row_scan(rows)模拟行存扫描:遍历每条记录的全部字段并累计字节数。io_col_scan(cols, need)模拟列存扫描:只遍历“age“和”gender”两个列表。dict_encode(cols["gender"])对性别列做字典编码:为不同取值分配整数编号。encoded_bytes = (encoded_bits + 7) // 8 + 20每行只需 1 比特编号,按字节对齐后再加上字典本身的存储。
常见误区
误区列式存储不就是把一张表竖着摆吗?行式横着摆、列式竖着摆,反正逻辑上都是同一个二维表,查询结果也没区别。
正解逻辑表确实都是二维表,但列式存储的关键在物理布局:同一列的值在磁盘上连续存放。这决定了扫描时只需要搬动对应列的数据块,也决定了同质数据可以被高效压缩。画成横着还是竖着只是表象,数据在磁盘上的“邻居是谁”才是性能差异的根源。
为何常见:我们平时用 Excel 或关系型数据库,看到的就是行列表格,容易忽略存储引擎在物理层如何排列数据。
误区列式存储读取效率这么高,那订单系统、账户系统这种高频读写的场景也应该换成列存,肯定更快。
正解列存的优势集中在“大范围扫描少数列”;点查一条完整记录时,列存要跨多个列文件拼行,更新一条记录要写多个列文件,随机 IO 和写放大反而更严重。OLTP 以高频点查和小事务更新为主,行存 + 索引才是主场。
为何常见:看到列存在 OLAP 报表上的惊艳表现后,容易把“快”归因于存储本身,而忽略负载类型与 IO 模式的匹配。
误区既然列存压缩率这么高,那就把所有列都压到极致,存储省了,查询也会更快。
正解压缩节省磁盘和 IO,但解压需要 CPU 开销。压缩率过高可能让 CPU 成为新瓶颈,尤其在与向量化执行结合时,需要在“少读数据”和“解压开销”之间找平衡。实际引擎还会选择不压缩或轻量压缩热点列。
为何常见:存储成本的压力让初学者本能地追求高压缩率,忽略了计算与 IO 之间的权衡关系。
误区行存有 B+ 树索引,列存数据排得整整齐齐,应该不需要索引了吧。
正解列存同样需要索引加速,只是形态不同。常见的有 Zone Map(每个数据块记录列的最大/最小值,跳过不符合条件的块)、稀疏索引、布隆过滤器等。它们帮助列存在聚合查询时快速跳过无关数据块。“无需索引”是误解,列存只是把索引换成了更适合列式扫描的形态。
为何常见:列存的顺序存储在视觉上给人一种“天然有序”的错觉,让人忽略了海量数据下仍需辅助结构来剪枝。
误区把一张宽表按字段拆成几张子表,每张子表存一部分列,逻辑上不就是列式存储了吗?
正解逻辑上的垂直分表只保证了“按列读取的表结构”,并不保证物理上各列连续存放,也不具备列存配套的压缩、向量化、区段索引等能力。真正的列存储是从数据页布局、编码格式到执行引擎的端到端重设计。
为何常见:从 SQL 逻辑视角看,垂直分表和列存很相似,容易忽略存储引擎物理层与执行层的协同设计。
面试题
Q1. 列式存储最核心的物理特征是?
- A.一行记录连续存放
- B.一列数据连续存放← 正确答案
- C.数据按主键有序存放
- D.数据按访问频次存放
答案:B。列式存储把同一列的所有值连续放在一起,就像把全部“年龄卡”装进同一个盒子。A 是行式存储的特征,C 是聚簇索引的特征,D 是冷热分层的思路,都与列存核心定义无关。
Q2. 以下哪个查询场景,列式存储的优势最明显?
- A.按主键查一条订单
- B.汇总全国订单总金额← 正确答案
- C.插入一条新订单记录
- D.修改一条订单的备注
答案:B。汇总总金额只需读取金额一列,列存只搬一个“盒子”即可完成。点查、插入、单行更新都是行存的擅长领域,列存反而要付出跨列拼装或写多列文件的代价。
Q3. 列存执行“查一条记录的多个字段”通常比行存慢,主要原因是?
- A.列存无法保存主键信息
- B.列存需要读取多个列文件再拼接← 正确答案
- C.列存支持的记录行数更少
- D.列存不支持数据压缩
答案:B。一行数据在列存中被拆散到各个列文件中,点查要分别访问多个列文件并拼接,随机 IO 次数更多。A、C、D 均不符合列存的事实:列存有主键与各类索引,行数由容量决定,压缩也是列存的强项。
Q4. 字典编码特别适合压缩哪一类列?
- A.高基数的用户 ID 列
- B.低基数的性别标签列← 正确答案
- C.唯一的时间戳标记列
- D.浮点型的评分值列
答案:B。字典编码用整数编号映射取值,去重后取值越少(基数越低),编号越短,压缩率越高。性别列只有两种取值,编号仅需 1 比特。高基数列的编号会变长,字典体积大,收益有限。
Q5. 列存能获得高压缩率的根本原因是?
- A.同列数据同质且值域接近← 正确答案
- B.列存使用了加密算法
- C.列存省略了字段类型
- D.列存仅保存不重复的行
答案:A。同一列的取值类型一致、取值范围集中,因此可以用字典编码、运行长度编码等方法高效压缩。B、C、D 都不符合事实:列存不加密、不省略类型、也不只保存不重复的行。
Q6. 向量化执行相比标量执行更快的本质是?
- A.提高了 CPU 时钟频率
- B.一条指令同时处理多个数据← 正确答案
- C.用 GPU 完全取代了 CPU
- D.减少了磁盘 IO 的次数
答案:B。SIMD 向量指令让 CPU 在单个周期内对一组数据执行相同运算。A 是硬件频率问题,C 不是列存向量化的核心(很多场景仍用 CPU SIMD),D 是列存本身的 IO 优势而非向量化本质。
Q7. 列存中 UPDATE 写放大的根源是?
- A.一行数据分布在多个列文件中← 正确答案
- B.列存不支持随机写入
- C.列存没有预写日志
- D.列存的行锁粒度太粗
答案:A。更新一行需要定位并重写该行涉及的所有列文件,列越多写放大越明显。B、C、D 都不是本质原因:列存可以随机写、也有日志与并发控制,只是代价结构不同。
Q8. 下列哪些场景更适合列式存储?(多选)
- A.大范围的聚合统计← 正确答案
- B.高并发的单行点查
- C.只取少数列的宽表扫描← 正确答案
- D.频繁的小事务提交
答案:AC。聚合统计和只取少数列的宽表扫描正是列存的“主场”:少搬数据、同质压缩、向量化加速。高并发点查和频繁小事务提交则是行存的优势场景,列存会面临跨列拼装和写放大。
Q9. 下列哪些技术常与列存协同来提升查询性能?(多选)
- A.向量化执行← 正确答案
- B.字典编码← 正确答案
- C.运行长度编码← 正确答案
- D.行级锁优化
答案:ABC。列存为向量化提供了连续同质数据,字典编码与运行长度编码利用列内重复值大幅压缩。行级锁是事务并发控制手段,与列存性能提升没有直接协同关系,更多属于 OLTP 范畴。
Q10. 分析型数据仓库普遍采用列存的原因包括?(多选)
- A.聚合只读所需列减少 IO← 正确答案
- B.同质数据的压缩率更高← 正确答案
- C.向量化利于批量计算← 正确答案
- D.单行插入的延迟更低
答案:ABC。列存让聚合查询只搬必要列,同质数据带来高压缩率,连续内存让 SIMD 向量化高效执行,这三者环环相扣。D 恰恰相反:列存单行插入需要分散写入多个列文件,延迟通常高于行存。