ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

行式存储到列式存储:大数据存储格式的技术变革与选型实践

行式存储到列式存储:大数据存储格式的技术变革与选型实践 1. 为什么“行式存储”突然成了大数据圈的热点先抛一个我最近经常被问到的问题同样是跑一张订单表的聚合查询在MySQL里扫全表可能要几分钟换成Hive或Spark之后换了个存储格式同样的SQL十几秒就出结果差距到底出在哪绝大多数人第一反应是“分布式并行计算”觉得是因为节点多、并行度高。这个答案只对了一半。真正拉开数量级差距的还有一个很多人忽略的底层设计数据到底是怎么在磁盘上摆的。这就是我们平常说的存储格式其中最关键的分水岭就是行式存储和列式存储的取舍问题。这也是为什么最近圈子里聊“大数据行式存储的技术变革”越来越多。这个标题听着挺唬人但拆开看就三件事行式存储是什么、为什么在大数据场景下顶不住了、后来又演化出了什么更好的方案。这篇文章我会从底层存储原理讲到实际业务落地把行式存储、列式存储、行列混合方案讲透再结合我实际做过的一个订单明细表改造项目给出可以直接参考的选型思路和避坑清单。无论你是刚入行还在学数据科学、正备考大数据开发面试还是在公司里负责数仓建模这部分内容都值得认真看一遍。2. 行式存储设计逻辑与大数据场景下的先天不足2.1 行式存储最初的设计逻辑行式存储说白了就是一行数据的所有字段连续存放在磁盘上。比如一张用户表有id、name、city、age四个字段那么磁盘上的物理顺序就是[1, 张三, 北京, 25][2, 李四, 上海, 30][3, 王五, 深圳, 28]...传统关系型数据库比如MySQL的InnoDB、PostgreSQL从一开始就是这种思路。为什么这么设计因为在那个年代业务系统的主要操作是“根据主键找一条记录”比如用户登录时查一下用户信息、下单时插入一条订单记录。这些都是典型的点查和写入操作。点查场景下行式存储的优势非常明显我要找id100的用户只要定位到对应的数据页一次I/O就能把这一行的所有字段全部读出来不用做多张表的关联拼装。写入场景同理一条完整记录连续写下去落盘效率很高。这个设计在单机数据库时代是合理的也是经过几十年验证的成熟方案。即使到了今天只要你的核心场景还是登录、下单、改状态这类高频点查行式存储依然是最优解之一。2.2 大数据分析场景下行式存储开始“吃力”问题出在数据分析上尤其是数据量从百万行涨到亿级、十亿级之后。分析型查询和点查的最大区别在于它通常不关心某一行的所有字段而是关心大量行中特定列的聚合结果。举个例子。电商订单表orders字段有order_id、user_id、city、amount、status、create_time等一共20多个字段。现在要跑一条统计SQLSELECT city, SUM(amount) FROM orders WHERE create_time 2024-01-01 GROUP BY city;这条查询实际用到的字段只有city、amount、create_time三个其他十几个字段完全用不上。但行式存储没有“跳过不想读的字段”这个能力。因为一行数据是连续存储的想把某几列取出来就必须把整行读进内存然后丢弃不需要的字段。假设一行记录平均200字节实际需要的三个字段大约40字节那就意味着磁盘上每读200字节只有40字节是能用的剩下的160字节全都白读了。数据量小的时候无所谓但到了1亿行就是另外一个故事了全表物理数据量约20GB行式存储扫描需要完整读取全部20GB。如果能只读三列I/O量理论上可以降到约4GB。如果再算上列式存储的压缩增益实际落盘数据可能连1GB都不到。从20GB到不到1GB差了20倍以上。这个差距靠单纯增加并行节点来补成本会非常难看。这里插一句很多人会问那我给create_time加索引不就行了索引确实能快速定位日期的范围但范围过滤之后如果命中行数很多依然要回到原表读取大量数据行然后做聚合。索引在点查场景下是杀手锏在全表范围扫描和聚合场景下基本帮不上大忙。2.3 行式存储被“吐槽”的另外两个问题CPU浪费和压缩率偏低除了I/O浪费行式存储还有两个隐性负担。第一个是CPU和内存开销。因为行式存储需要把完整字段读到内存再做裁剪内存带宽和CPU周期都消耗在无用的字段解析上。分析型SQL往往要处理上亿行这种无谓的解析开销会被无限放大。第二个是压缩率。行式存储文件里一行数据混合了整数、字符串、日期、浮点数等多种类型。压缩算法在混杂类型数据中很难找到规律。比如ID字段可能趋近随机分布但相邻行的ID没什么相关性city字段虽然重复度高但被穿插在其他字段之间字典编码的收益也会受影响。我实测过一组数据同一份订单表行式存储如CSV/Text格式用gzip压缩一般能达到2到3倍的压缩比但同样的数据转成列式存储格式后用snappy或zstd压缩轻松跑到5到8倍以上。存储成本直接差出一个量级这在按TB计费的大数据平台上是实打实的钱。3. 技术变革的核心从行式到列式再到行列混合3.1 列式存储到底改变什么既然问题出在“一行连续存储”这个基本布局上那自然就有人想到如果反过来把一列的所有值连续存储会怎么样还是上面的用户表列式存储的物理布局长这样id列: [1, 2, 3, ...] name列: [张三, 李四, 王五, ...] city列: [北京, 上海, 深圳, ...] age列: [25, 30, 28, ...]这个改动看着简单但带来了一系列连锁收益。收益一是查询时只读需要的列。你要统计city分布就只读city列其他列完全不用碰。收益二是同类数据放一起压缩率大幅提升。city列里大量重复值字典编码后几乎可以做到一个编码代表一个城市压缩率非常可观。收益三是可以用向量化计算。连续读出来的数据天然就是数组现代CPU的SIMD指令可以批量处理不用逐行解析进一步压榨硬件性能。这就是为什么像ClickHouse能跑得那么快底层就是列式存储加上向量化执行。Parquet和ORC这两种大数据领域的事实标准格式核心也是列式存储只是在具体实现上做了更多工程化处理。3.2 压缩率到底能差多少我们来算一笔账用一个实际算例来感受差距。假设有一张1亿行的订单表每行200字节。行式存储Text格式数据量约20GB。用gzip压缩后实际压缩比通常2.5倍左右落盘约8GB。换成列式存储Parquet格式启用snappy压缩。只需要读三列聚合时city列重复度高字典编码后压缩比能做到10倍以上amount列是数值型delta编码加snappy压缩比也能到4倍以上create_time列是时间戳按单调递增的delta-of-delta编码压缩比同样可观。整体落盘可能只要2到3GB。简单说在同样数据量的前提下存储成本从8GB降到2-3GB省了三分之二。查询I/O从读全表变成只读相关列再叠加压缩实际扫描量可以降到200MB量级。扫描时间从几十秒降到几秒是正常现象。很多公司做数据仓库优化第一条规劝就是“把Text/CSV格式表迁移到Parquet/ORC”不是因为Parquet含有什么黑魔法就是因为它把行式布局改成了列式布局把数据重排了一遍。3.3 纯列式也有短板行列混合布局是怎么来的列式存储这么香那是不是所有场景都用列式就完事了并不是。列式存储有一个明显的弱项点查和频繁更新。因为一列的数据被拆分到不同文件或不同区域要还原一行完整记录就需要从多个列文件中分别读取再进行拼装。对单条主键点查来说列式反而更慢。另外列式存储对单行更新也不友好因为一行数据被打散在不同列文件中更新一行往往要动多个文件这在HDFS这类只支持追加的文件系统上尤其痛苦。所以业界并没有走向“全列式”而是提出了行列混合的概念。最经典的是PAX布局——Partition Attributes Across。思路是在数据页内部保持按行分区但页内的数据按列组织。简单理解就是一个大的“数据块”内部先用行存的方式切分成多个page每个page内部再把字段按列排列。这样的好处是既保留了列式的高压缩比和列裁剪能力又在数据块级别保留了一定的行式局部性点查时可以更快定位数据块。在大数据系统里PAX思想被大量借鉴。比如ORC文件格式的Stripe内部就是按列存储的StreamHudi、Iceberg、Delta这些数据湖格式底层文件可以用Parquet存储分析列同时用Avro存记录级增量数据本质上就是一种行列混合策略。更广义的行列混合是在一个分布式系统里同时支持两种存储引擎。比如StarRocks里可以同时建明细表按列存储和主键表按主键索引做高频更新ClickHouse也有类似机制。这类系统在选型上会让架构师有更多腾挪空间。4. 落地实操从行式表迁移到行列混合方案的完整过程4.1 主流存储格式选型对比先给出一张选型对比表这是我做架构评审时经常画的格式存储布局压缩率点查分析查询适用场景Text/CSV行式低差差临时数据、外部交换AVRO行式中较好差数据写入、消息序列化、增量数据存储Parquet列式高一般很好离线分析、数据仓库、数据湖ORC列式高一般很好Hive数仓、大规模扫描聚合ClickHouse MergeTree列式稀疏主键索引高按主键点查/范围查询较好极好实时OLAP、监控分析这里要注意Avro虽然也是大数据生态里的常见格式但它属于行式存储。很多人在面试时会踩这个坑觉得Avro是列式。判断标准很简单一行数据在物理上是连续存储还是被列拆分开。Avro默认是连续存储一行所以它是行式。4.2 实战案例订单明细表从Text迁移到ORC我之前在一家做电商数据分析的公司优化过一个真实场景。那会儿ods层的订单明细表在Hive里用Text格式存储一天的分区大约100GB全表一共300多天的分区总数据量30多个TB用完怎么查都慢还有存储成本压力。第一步我先整理这张表的字段。一共24个字段其中业务核心字段有order_id、user_id、city_id、shop_id、amount、status、pay_time、create_time等另外一半是各种冗余的扩展字段平时分析很少用到。第二步选格式。考虑到Hive生态兼容性和各种复杂查询的稳定性我最终选了ORC而不是Parquet。原因是Hive对ORC的原生支持最好尤其是矢量化查询和索引裁减的支持比Parquet更成熟。如果你主要用Spark SQLParquet也完全可以两者在底层原理上没有本质差别只是各家引擎的支持程度不同。第三步改造建表语句。核心是STORED AS ORC以及选择合适的压缩算法。CREATE TABLE dwd_orders ( order_id STRING, user_id BIGINT, city_id BIGINT, shop_id BIGINT, amount DECIMAL(12,2), status INT, pay_time TIMESTAMP, create_time TIMESTAMP, ... ) PARTITIONED BY (dt STRING) STORED AS ORC TBLPROPERTIES ( orc.compress snappy );我选snappy而不是zlib或者zstd原因后面专门说。第四步执行数据回填。用INSERT OVERWRITE语句把Text表的数据整批写入新的ORC表同时利用Spark或Tez的并行任务让每个Reducer输出文件尽量在512MB到1GB之间避免小文件堆积。INSERT OVERWRITE TABLE dwd_orders PARTITION (dt) SELECT order_id, user_id, city_id, shop_id, amount, status, pay_time, create_time, ..., dt FROM ods_orders_text;改造完成后我做过一个对比测试。同一时间段三个月的数据原来Text格式压缩后大约90GBORC加snappy压缩后只有约14GB压缩比提升大约6.5倍。一条经典的周维度城市交易额统计SQL原来跑一遍平均37分钟迁移后降到6分钟出头。这里既有列裁剪的功劳也有ORC自带的索引裁减在发挥作用。4.3 压缩算法的选择逻辑压缩算法的选择是大数据行式存储改造里最容易忽略但影响极大的细节。我见过很多人无脑选snappy也有的人只看压缩比选了zstd结果性能不升反降。压缩算法主要看两个维度压缩比和压缩/解压速度。两者往往是矛盾的。snappy的特点是速度快压缩比中等zstd的特点是压缩比高速度也不差zlib偏向高压缩比但解压慢lz4则追求极致的速度。在大数据查询场景中数据在查询时是要被解压的解压速度直接决定扫描吞吐量而扫描吞吐量直接决定SQL跑多快。所以对绝大多数交互式分析场景我不建议一开始就上最高压缩比的算法而是选snappy这种平衡型选手。如果你的存储成本压力极大、查询频率又不高那再考虑zstd。之前测试过一次同一份ORC表用zstd压缩后存储从14GB降到11GB但一次全表扫描查询耗时反而比snappy版涨了12%。原因就是这个集群CPU资源偏紧张解压成了新的瓶颈。4.4 只考虑存储格式还不够分区、排序和表设计要一起改很多人以为把表改成ORC或Parquet就万事大吉了但实际效果往往没有预期好。原因在于存储格式只是底子上层表设计不合理再好的格式也没办法发挥全部实力。第一分区列的选择。大数据量表一定要分区通常按日期分区这个没问题。但要注意分区不是越多越好分区的粒度最好配合查询条件。比如分析任务绝大多数按天或者按月过滤那就按天分区如果经常要跨多年的全量扫描分析也可以考虑只在必要的时间范围上建分区不要无脑把所有维度都做成分区列。第二排序键和桶划分。列式存储并不是完全不支持顺序优化。以ORC为例写入时如果按照某个字段排序那么Stripe内部会生成该字段的min/max索引查询过滤这个字段时可以快速跳过大量Stripe。我的处理方式是把经常用于过滤且基数不太高的字段比如city_id、order_status放在表的排序键前列。数据在写入前先做一次排序再落盘初始成本是高了但后续查询收益非常大。第三数仓分层时给行式存储留位置。这里要强调行式存储并没有完全退出。在我负责的表结构中明细层dws层大部分表是ORC列式但ods层接实时消息数据时依然会用Avro这类行式格式因为Avro在序列化和反序列化上表现更好也更容易兼容schema演化。列式格式在增量更新、单行级变更上天然有短板所以数据湖方案里通常会把Avro用于日志级增量把Parquet用于基线数据两者配合这就是最典型的行列混合落地方式。5. 常见问题排查与技术避坑实录5.1 行式转列式后查询反而变慢问题出在哪我之前帮朋友排查过一个案例他们公司把一张核心业务表从MySQL导到Hive数仓直接选了Parquet格式存储结果发现一条点查SQL在Hive上比MySQL还慢两个数量级。这其实不是Parquet本身的问题而是这张表的查询模式还是“根据主键查一行”典型的点查场景。Parquet读一个主键值时需要读取Parquet文件footer中的元数据定位到对应的RowGroup再扫描该RowGroup中相关的列Chunk。这个过程涉及多个文件的随机读在HDFS分布式架构下随机读的成本远高于连续扫描。所以排查SQL性能问题时第一步先判断查询属于点查还是分析。点查为主老老实实用Redis或者MySQL等行式存储解决方案不要勉强用列式格式硬扛。分析为主才轮到Parquet、ORC这类方案上场。5.2 小文件爆炸问题这是行式转列式时最常踩的坑。不调整并行度直接回填数据时每个Map/Reduce任务都会产出一个Parquet/ORC文件如果任务数有大几千就会产生几千个小文件。小文件对列式存储的伤害比行式更明显。因为每个列式文件都有自己的元数据footer查询引擎要打开每个文件、读footer、定位RowGroup。文件数量一多NameNode内存压力变大查询计划的调度开销也剧增。我见过一个集群数据量只有50GB但小文件有几十万个跑个count(*)都要卡很久就是这个原因。解决办法有几个不是选一个就完事而是结合使用在写入时控制分区和Reducer数量使得每个文件控制在512MB到1GB。定期用文件合并任务把同一分区的多个小文件合并成一个大文件。使用Spark的repartition/coalesce或者Hive的concatenate命令来梳理文件。注意频繁合并文件会产生临时数据建议放到凌晨低峰期执行。合并完之后记得做一次ANALYZE TABLE更新表的统计信息否则查询优化器可能还按旧元数据做预估。5.3 压缩率很高但扫描性能依然拉胯还有一类问题文件已经用ORC了压缩比也很漂亮但查询速度就是上不去。这个问题的根源往往在排序键和统计索引失效上。ORC、Parquet这类格式都默认对每个Stripe或RowGroup维护min/max索引。但如果你写入的数据没有排序而且字段基数很高比如order_id本身几乎每条都不重复那么min/max索引的裁剪效果就约等于零几乎每个Stripe都得扫。解决思路是第一把查询过滤最频繁的字段作为排序键并且保证写入前按排序键排序。第二不要把高基数字段作为唯一的过滤条件比如order_id这种字段用索引裁剪没有意义得靠分区或者hash分桶先缩范围。第三如果业务确实需要primary key级别的唯一性查询那就别指望列式存储的索引机制给这张表专门建一个支持主键的行式存储表或者接宽表引擎。5.4 宽表和窄表到底怎么选最后说一个数据建模里常年的争论一张表字段多好宽表还是拆成多张小表窄表好列式存储出现之前大家倾向于窄表因为要避免扫描时读取大量无关列减少磁盘I/O。但列式存储普及之后这个逻辑发生了微妙变化。因为列式存储按列读取一张宽表只要查询中用到的列不多实际I/O并不会随总字段数膨胀。宽表还有一个好处就是省去了ETL环节大量的join操作。但这不意味着可以无脑堆字段。宽表的主要问题在于列数过多时元数据管理和schema演化的成本上升而且如果频繁往宽表里加列历史分区的文件可能都要重写。我的习惯是层级越高越宽ads层可以直接做宽表dm和dws层拆成主题域模型ods层保持原始结构。不要一刀切。6. 我对行式存储技术变革的一点实践体会聊了这么多最后说点感性的东西。我见过很多刚入门大数据的同学一上来就学各种框架Hadoop、Spark、Flink装了一圈但遇到性能问题往往无从下手因为他们最容易忽略的就是最底层的存储格式和表设计。行式存储到列式存储的这场变革不只是一次技术选型的变化它从根本上改变了数仓工程师分析数据的方式。做行式转列式这件事我最大的体会是不要为了新而新。技术选型一定要回到查询特征上。你的系统是点查多还是扫描聚合多决定你该用行式还是列式两者并不矛盾完全可以配合使用。对大多数公司来说一套理想的数据架构里行式存储和列式存储各有自己的位置Avro在管道入口负责稳健的写入Parquet和ORC在分析层负责高效的扫描ClickHouse和StarRocks这类引擎又在更实时的场景里把行列混合玩出了新花样。另外补充一个面试和学习方向上的小建议。大数据面试里“行式存储和列式存储的区别”几乎必考但多数候选人只会背几句概念。真正能加分的回答一定要带上实际的量化对比和选型思考。比如你能说出“行式适合高频点查、列式适合分析型扫描压缩率能差出几倍PAX解决方案是两者的折中”面试官通常就会眼前一亮。如果你正在规划大数据学习路线建议把存储格式的原理作为重点之一这部分知识收益极高学一次贯穿数仓、数据湖和OLAP引擎多个方向。行式存储并没有消失它只是退回到了更适合它的位置。理解这场技术变革的核心不只是一个知识点更是一种解决问题的视角。希望这篇文章能帮你理清思路。
返回列表