#51 大数据计算 中等

行式存储和列式存储有什么区别?数仓为什么常用 Parquet?

列式存储行式存储Parquet列裁剪

🎯 考察意图

考察候选人会不会把「列存更快」落成存储布局和查询形态的匹配:分析查询通常只读少列、扫很多行,所以同一列挨在一起才能做列裁剪、编码压缩和谓词下推。只背 Parquet/ORC 名词,却说不清什么查询更快、什么查询更慢、小文件为什么会把优势吃掉,这题就浅了。

📐 回答框架

  1. 先对查询形态:OLTP 点查整行、频繁更新;OLAP 扫大量行、只用少列。存储布局要匹配访问模式。
  2. 行存:一行的所有列物理上挨在一起,适合按主键取整行、就地更新。
  3. 列存:同一列的值挨在一起,适合列裁剪、高压缩、谓词下推、向量化扫描。
  4. 点出代价:整行更新、宽表点查、超小文件,列存更亏。
  5. 落到 Parquet:行组 / 页 / footer 统计,以及和 ORC、Avro 的分工。

💬 参考答案(口述版)

一句话:行存按「一行」排数据,适合点查和更新;列存按「一列」排数据,适合分析扫描。数仓常用 Parquet,不是因为它名字时髦,而是因为看板和 SQL 大多是「读几列、滤一批分区、聚合很多人」。

先看访问模式,再谈格式。
- OLTP(订单库、用户库):一次拿某一行的很多字段,还可能改状态、改库存。行存(InnoDB 页、Heap)把一行的列放在相邻字节里,一次 I/O 就能拼出整行。
- OLAP(数仓、明细宽表):一次扫昨天几千万行,但 SELECT 往往只有 user_id, gmv, dt,WHERE 也只碰到两三列。行存必须把整行从盘上读出来再扔掉不用的列;列存只读被点名的那些列文件。

列存靠四件事把扫描量打下去。
1. 列裁剪(projection):查询没用到的列,文件都不用打开。宽表 200 列、查询 5 列,I/O 可以掉一个数量级。
2. 编码压缩:同一列类型相同、重复值多,字典编码、RLE、delta 比行存的通用压缩狠得多。字符串状态码、低基数字段尤其明显。
3. 谓词下推(predicate pushdown):footer / 行组里记 min/max、null 计数。WHERE dt='2026-10-22' AND city='北京' 能跳过整块行组,不必解压每一页。
4. 向量化:一次处理一列的一批值,CPU 缓存和 SIMD 更友好。

口述对比可以用一张订单宽表:

-- 典型分析:只碰 4 列,过滤一个分区
SELECT user_id, SUM(pay_amt) AS gmv
FROM dwd_trade_order
WHERE dt = '2026-10-22'
  AND pay_status = 'success'
GROUP BY user_id;
-- 列存:几乎只读 dt / pay_status / user_id / pay_amt
-- 行存:每一行的 sku 名、地址、备注也会被读进来再丢掉

列存不是免费午餐。
- 改一行要重写多个列文件,OLTP 更新用行存或先写行式日志再批量转列。
- SELECT * 点查主键,列存要拼很多列,未必更快。
- 超小文件让 footer、行组统计比数据还重,列存优势会被小文件打没——这和「小文件治理」是同一条链。

为什么数仓落地常见 Parquet。
Parquet 是列存文件格式:文件按 row group(行组,一批行,常见几十到几百 MB)切,组内再按列拆成 column chunk,chunk 里是 page。文件末尾 footer 有 schema、各列统计。Spark/Hive/Presto 读的时候先看 footer,再决定打开哪些列、跳过哪些行组。和它并列的是 ORC(Hive 系更常见,也有索引和统计);Avro 更偏行式序列化,适合 Kafka 传输和 schema 演进,不适合当仓里的分析主格式。实践上:入仓明细用 Parquet/ORC 分区表,流式管道用 Avro/Protobuf,业务库继续行存。

收尾三句:匹配访问模式、列存赢在少列扫描、Parquet 用行组统计做跳读。能主动说「更新和点查看行存、分析看列存、小文件会让列存变慢」,这题就过了。

🔍 关键词

列裁剪谓词下推行组 row group编码压缩OLAP vs OLTP

💭 面试官可能的追问

追问 1:一张 200 列的用户宽表,查询只用 5 列,行存和列存差在哪?

行存按行页存放,要满足谓词几乎得把命中行的 200 列都读进内存,再投影出 5 列。列存只打开那 5 列对应的 chunk,另外 195 列的文件可以完全不碰。列越多、查询越「瘦」,差距越大。反过来如果这个查询是按 user_id 取整行画像,列存要随机拼 200 个列块,行存一次页读可能就够,这时行存不一定差。

追问 2:Parquet 的 row group 是什么?和谓词下推有什么关系?

Row group 是文件内部的一行切片:几千到几十万行捆成一组,组内按列存,组级 footer 记各列 min/max、行数、压缩大小。引擎先读 footer:city 的 max 都小于过滤值,整组跳过,这就是谓词下推。行组太大,跳过粒度粗、内存峰值高;行组太小,元数据比例上升,又接近小文件。落地常按 128–512MB 行组、按查询过滤列排序或聚簇,让 min/max 更「分离」,跳过才有效。没有统计信息的裸列存,谓词下推做不起来。

追问 3:流式写入为什么不直接每条都 append 一个 Parquet?和 Avro 怎么分工?

Parquet 要攒够一个行组才值得写:太早 flush 会制造海量小文件,footer 比数据还贵,查询和 NameNode/metastore 都会炸。流式侧用 Avro/JSON/Protobuf 做行式、可追加、schema 友好的传输;落到湖/仓再 compaction 成较大的 Parquet/ORC 分区文件。口诀:传输用行式可追加,分析用列式可跳读;中间必须有小文件合并,不是换个后缀就算列存数仓。

⚠️ 常见踩坑

  • 把「列存更快」说成绝对真理,忽略主键点查和频繁更新其实更适合行存
  • 把 Avro、Parquet、ORC 混成一种「大数据文件」,不区分行式传输和列式分析
  • 只吹压缩率,不提列裁剪和行组级谓词下推才是扫描量下降的主因
  • 流式每条都写 Parquet、或不做 compaction,用小文件把列存优势抵消掉

🧠 记忆锚点

分析查询读少列扫多行,所以列存;点查整行和频繁更新,才用行存。Parquet 靠行组统计跳读,小文件会把这优势吃掉。