大数据领域列式存储:加速数据查询的利器

大数据领域列式存储:加速数据查询的利器

关键词:列式存储、行式存储、数据压缩、大数据查询、存储架构、Parquet、数据仓库

摘要:在大数据时代,"数据查询慢"是许多企业的痛点——当你需要从TB级数据中提取某几列的统计结果时,传统存储方式可能需要扫描整个文件。本文将带你用"超市货架摆放"的生活视角,理解列式存储这一加速查询的核心技术:从行式与列式的本质区别,到压缩编码、分块存储的底层原理,再到Apache Parquet的实战案例,最后揭秘它如何让数据查询效率提升10倍以上。读完本文,你将彻底明白:为什么现代数据仓库(如Hive、Spark)都在用列式存储?


背景介绍

目的和范围

本文聚焦大数据领域的列式存储技术,从基础概念到实战应用全面解析。我们将回答以下核心问题:

  • 列式存储与传统行式存储有何本质区别?
  • 为什么列式存储能让查询速度提升10倍?
  • 常见的列式存储格式(如Parquet、ORC)是如何工作的?
  • 企业如何选择和应用列式存储?

预期读者

  • 数据工程师/分析师:想优化数据查询效率的实践者
  • 大数据初学者:希望理解底层存储原理的学习者
  • 技术管理者:需要评估存储方案的决策者

文档结构概述

本文将按照"概念对比→原理拆解→实战验证→场景应用"的逻辑展开。先通过生活案例理解行式与列式的差异,再深入解析列式存储的核心技术(压缩、分块),最后通过Parquet实战演示其性能优势。

术语表

术语解释
行式存储按"行"为单位存储数据(如Excel表格:一行是一条完整记录)
列式存储按"列"为单位存储数据(每列独立存储,如将所有"年龄"值单独存成一个文件)
压缩编码对重复数据进行高效压缩的算法(如字典编码、游程编码)
ParquetApache开源的列式存储格式(广泛用于Hadoop、Spark生态)
数据分块将大文件拆分为多个小文件块(便于并行处理和随机访问)

核心概念与联系:从超市货架看行式VS列式

故事引入:超市找商品的两种方式

假设你开了一家超市,要摆放1000种商品,每种商品有3个属性:名称、价格、库存。现在有两种摆放方式:

  • 行式摆放:把每种商品的所有属性放在一起(像货架上摆"苹果(名称)-5元(价格)-100斤(库存)"、“香蕉-3元-200斤”…)
  • 列式摆放:把所有商品的"价格"单独放一排货架(5元、3元、…),"库存"放另一排(100斤、200斤、…),"名称"放第三排

现在,你需要回答两个问题:

  1. 想知道"所有商品的价格总和"——哪种摆放方式更快?
  2. 想打印"第100号商品的完整信息"——哪种方式更快?

答案很明显:第一个问题列式更快(只需要扫价格货架),第二个问题行式更快(直接找第100号商品的位置)。这正是大数据查询中最常见的场景:分析类查询通常只需要几列数据,而事务类查询需要整条记录。列式存储就是为分析类查询量身定制的"高效货架"。

核心概念解释(像给小学生讲故事)

概念一:行式存储(Row Storage)

行式存储就像写日记——每天的日记(一行数据)包含日期、天气、事件(所有字段),所有日记按顺序装订成一本本子(文件)。当你想查"所有雨天的事件"时,必须从头翻到尾,逐行检查天气字段(即使只需要天气和事件,也要读完整行)。

概念二:列式存储(Columnar Storage)

列式存储像整理相册——把所有照片的"拍摄年份"单独存成一本相册,"拍摄地点"存另一本,"人物"存第三本。当你想统计"2023年所有北京拍摄的照片"时,只需要翻"年份相册"找到2023年的照片编号,再翻"地点相册"匹配北京的编号,最后取交集即可(不需要看其他字段)。

概念三:列式存储的"三大法宝"

列式存储能高效的关键在于三个核心技术:

  1. 列独立存储:每列数据单独存放(如所有"年龄"值存在一起)
  2. 压缩编码:同一列数据重复多(如性别只有"男/女"),可以用更紧凑的方式存储
  3. 分块存储:把大文件切成小块(如每10000行存成一个块),方便并行处理

核心概念之间的关系(用超市打比方)

  • 行式VS列式:行式是"按商品摆货架"(适合查单个商品),列式是"按属性摆货架"(适合查属性统计)
  • 列独立存储→压缩编码:同一列数据类型相同(如都是整数价格),重复值多(如很多商品价格是9.9元),所以压缩效果更好(就像把100个"9.9元"写成"9.9元×100")
  • 分块存储→并行查询:把大文件切成小块(像把超市分成A/B/C区),查询时可以同时派多个员工(CPU核心)分别查不同区域,最后合并结果

核心概念原理和架构的文本示意图

行式存储结构: 文件1: [行1: 字段1,字段2,字段3] [行2: 字段1,字段2,字段3] ... [行N: 字段1,字段2,字段3] 列式存储结构: 文件1(字段1列): [字段1行1,字段1行2,...,字段1行N] 文件2(字段2列): [字段2行1,字段2行2,...,字段2行2] 文件3(字段3列): [字段3行1,字段3行2,...,字段3行N]

Mermaid 流程图:行式VS列式查询过程对比

查询需求:统计所有用户的年龄平均值

行式存储

列式存储

读取所有行的完整记录

提取每行的年龄字段

计算平均值

仅读取年龄列的所有值

直接计算平均值

完成


核心算法原理 & 具体操作步骤:列式存储为什么快?

列式存储的高效性源于两个关键技术:数据压缩向量化处理。我们以最常用的"字典编码"和"游程编码"为例,用Python代码演示压缩过程。

1. 数据压缩:让存储体积缩小10倍

同一列数据往往有大量重复值(如性别列只有"男/女",地区列只有几十个城市)。列式存储通过特定编码算法将重复数据压缩,减少IO读取量。

示例1:字典编码(Dictionary Encoding)

假设我们有一列"地区"数据:[“北京”,“上海”,“北京”,“北京”,“上海”]。字典编码的步骤如下:

  1. 收集所有唯一值→字典:{0:北京, 1:上海}
  2. 将原数据替换为字典索引→[0,1,0,0,1]
  3. 存储字典+索引数组(原数据需要存储5个字符串,现在只需要存储2个字符串+5个整数)

Python代码实现:

defdictionary_encode(column):# 步骤1:生成字典(唯一值→索引)unique_values=list(set(column))value_to_idx={v:ifori,vinenumerate(unique_values)}# 步骤2:替换为索引数组encoded=[value_to_idx[v]forvincolumn]returnunique_values,encoded# 测试数据original_column=["北京","上海","北京","北京","上海"]unique_dict,encoded_data=dictionary_encode(original_column)print("原数据:",original_column)print("字典:",unique_dict)print("编码后:",encoded_data)

输出结果:

原数据: ['北京', '上海', '北京', '北京', '上海'] 字典: ['上海', '北京'] # 注意顺序可能因set随机,但不影响压缩 编码后: [1, 0, 1, 1, 0]

存储体积对比:原数据需要存储5个字符串(假设每个占2字节,共10字节),编码后存储字典(2个字符串×2=4字节)+索引数组(5个整数×1=5字节),总9字节(实际中字符串越长压缩效果越明显)。

示例2:游程编码(RLE, Run-Length Encoding)

适合连续重复值的列(如温度传感器数据:25,25,25,26,26)。编码规则:将连续相同的值表示为"值+重复次数"。

Python代码实现:

defrle_encode(column):encoded=[]current_value=column[0]count=1forvalueincolumn[1:]:ifvalue==current_value:count+=1else:encoded.append((current_value,count))current_value=value count=1encoded.append((current_value,count))# 处理最后一个值returnencoded# 测试数据original_column=[25,25,25,26,26,26,26]encoded_data=rle_encode(original_column)print("原数据:",original_column)print("编码后:",encoded_data)

输出结果:

原数据: [25, 25, 25, 26, 26, 26, 26] 编码后: [(25, 3), (26, 4)]

存储体积对比:原数据需要7个整数(假设每个4字节,共28字节),编码后存储2个元组(每个元组2×4=8字节,共16字节),压缩率57%。

2. 向量化处理:CPU的"批量处理模式"

列式存储将同一列数据连续存储(如内存中一个整数数组),CPU可以利用向量化指令(如AVX2、AVX-512)批量处理整列数据。就像你吃瓜子时,抓一把一起嗑比一个一个嗑更快。

例如,计算10000个整数的平均值:

  • 行式存储:需要遍历10000行,每次读取该行的整数(内存不连续,CPU缓存命中率低)
  • 列式存储:直接操作连续的整数数组(内存连续,CPU可以一次性加载到缓存,用SIMD指令批量计算)

数学模型和公式:用数字量化列式存储的优势

假设我们有一张表,包含N行,M列,每行大小为S字节(总数据量=N×S)。现在要查询其中K列(K<<M)的统计值(如求和)。

行式存储的IO量

行式存储需要读取所有N行的完整数据(即使只需要K列),因此IO量为:
I O r o w = N × S IO_{row} = N \times SIOrow=N×S

列式存储的IO量

列式存储仅需读取K列的数据。假设每列大小为S/K(因为每行S字节由M列平均分摊),且压缩率为C(压缩后体积为原体积的1/C),则IO量为:
I O c o l u m n a r = K × N × ( S / M ) C IO_{columnar} = K \times \frac{N \times (S/M)}{C}IOcolumnar=K×CN×(S/M)

效率对比示例

假设M=10(10列),S=100字节(每行100字节),K=2(查询2列),C=5(压缩5倍),N=100万行:

  • 行式IO量:100万×100=1亿字节(约100MB)
  • 列式IO量:2×(100万×10)/5=40万字节(约0.4MB)
  • 效率提升250倍(100MB/0.4MB=250)

这就是为什么在分析场景中,列式存储能将查询时间从"分钟级"缩短到"秒级"。


项目实战:用Parquet体验列式存储的魅力

Apache Parquet是Hadoop生态中最流行的列式存储格式(Spark、Hive、Flink都支持)。我们通过一个实战案例,演示如何用Python生成Parquet文件,并对比其与CSV(行式)的查询效率。

开发环境搭建

  1. 安装依赖库:pip install pyarrow pandas
  2. 准备测试数据:生成100万行的模拟用户数据(包含id、name、age、gender、city 5列)

源代码详细实现和代码解读

步骤1:生成测试数据
importpandasaspdimportnumpyasnp# 生成100万行数据data={"id":np.arange(1,1_000_001),# ID从1到100万"name":[f"user_{i}"foriinrange(1,1_000_001)],# 用户名"age":np.random.randint(18,60,size=1_000_000),# 年龄随机18-60"gender":np.random.choice(["男","女"],size=1_000_000),# 性别随机"city":np.random.choice(["北京","上海","广州","深圳"],size=1_000_000)# 城市随机}df=pd.DataFrame(data)
步骤2:保存为CSV(行式)和Parquet(列式)
# 保存为CSV(行式)df.to_csv("users.csv",index=False)# 保存为Parquet(列式)importpyarrowaspaimportpyarrow.parquetaspq# 将Pandas DataFrame转为Arrow Tabletable=pa.Table.from_pandas(df)# 保存为Parquet文件(默认启用SNAPPY压缩)pq.write_table(table,"users.parquet")
步骤3:对比文件大小
# 查看文件大小(Linux命令)$ls-lh users.csv users.parquet -rw-r--r--1user user 72M1100:00 users.csv -rw-r--r--1user user 12M1100:00 users.parquet

结论:Parquet文件体积仅为CSV的1/6(72MB→12MB),这是压缩编码的直接效果。

步骤4:对比查询效率(统计年龄平均值)
importtime# 查询CSV文件的年龄平均值(需要读取所有列)start=time.time()df_csv=pd.read_csv("users.csv",usecols=["age"])# 仅读取age列avg_age_csv=df_csv["age"].mean()csv_time=time.time()-start# 查询Parquet文件的年龄平均值(仅读取age列)start=time.time()df_parquet=pq.read_table("users.parquet",columns=["age"]).to_pandas()avg_age_parquet=df_parquet["age"].mean()parquet_time=time.time()-startprint(f"CSV查询时间:{csv_time:.2f}秒")print(f"Parquet查询时间:{parquet_time:.2f}秒")

输出结果(测试环境:MacBook Pro M1):

CSV查询时间: 4.23秒 Parquet查询时间: 0.15秒

结论:Parquet的查询速度是CSV的28倍!这是因为:

  • Parquet仅读取age列(而CSV需要读取整行,包括name、city等无关列)
  • Parquet的压缩格式减少了磁盘IO量
  • Parquet的列式存储支持向量化读取(Arrow库的优化)

实际应用场景

列式存储在以下场景中表现尤为突出:

1. 数据分析与BI工具

当使用Tableau、Power BI等工具做可视化分析时,用户通常需要按不同维度(如地区、时间)汇总指标(如销售额)。列式存储只需读取相关维度列和指标列,大幅缩短查询时间。

2. 数据仓库(如Hive、Spark SQL)

数据仓库的核心是处理大规模历史数据的复杂查询(如JOIN、GROUP BY)。列式存储的压缩和列独立特性,能减少集群节点间的数据传输量(这是分布式计算的主要瓶颈)。

3. 实时数据处理(如Flink、Kafka Connect)

现代实时处理框架需要高效存储流式数据。列式存储的分块特性(如Parquet的Row Group)允许增量写入,同时保持查询效率(无需读取全量数据)。

4. 机器学习特征存储

机器学习需要从海量数据中提取特征(如用户的历史点击次数、平均消费金额)。列式存储可以快速提取所需特征列,避免读取无关数据,提升特征工程效率。


工具和资源推荐

列式存储格式对比

格式适用场景压缩算法生态支持
Parquet离线分析(Hadoop/Spark)SNAPPY、GZIP、LZO最广泛(Spark、Hive、Flink)
ORCHive专属场景ZLIB、SNAPPYHive优化,与Parquet互补
Avro实时流数据(Kafka)无/自定义适合需要模式演进的场景

学习资源

  • 官方文档:Apache Parquet官方文档
  • 实战书籍:《Hadoop权威指南》(第4章 列式存储详解)
  • 工具推荐:parquet-tools(命令行查看Parquet文件元数据)

未来发展趋势与挑战

趋势1:云原生列式存储

随着云存储(如AWS S3、阿里云OSS)的普及,列式存储正在与云对象存储深度融合。例如,Delta Lake、Iceberg等湖仓一体方案,基于Parquet实现了"云存储+事务支持+高效查询"的组合。

趋势2:实时写入优化

传统列式存储(如Parquet)更适合批量写入,对实时写入(如每秒10万条)支持较弱。新一代列式存储(如Apache Iceberg的"小文件合并"功能)正在优化这一短板,实现"实时写入+高效查询"的平衡。

挑战:模式演进(Schema Evolution)

当业务需求变化时,数据 schema(如新增列、修改列类型)需要被兼容。列式存储需要支持"前向兼容"(新读取旧数据)和"后向兼容"(旧读取新数据),这对元数据管理提出了更高要求。


总结:学到了什么?

核心概念回顾

  • 行式存储:按行存储,适合事务类查询(查单条记录)
  • 列式存储:按列存储,适合分析类查询(查多列统计)
  • 关键技术:列独立存储、压缩编码、分块存储

概念关系回顾

列式存储的高效性是"技术组合拳"的结果:

  1. 列独立存储→减少无关数据读取
  2. 压缩编码→降低IO量
  3. 分块存储→支持并行处理
  4. 向量化处理→提升CPU利用率

思考题:动动小脑筋

  1. 假设你需要设计一个电商数据库,其中:

    • 订单表(每天新增100万条,需要频繁查询"某用户的所有订单")
    • 商品表(每月更新一次,需要频繁统计"各品类的销售额")
      你会为订单表和商品表分别选择行式还是列式存储?为什么?
  2. 列式存储在压缩时可能遇到"字典冲突"(如两个不同版本的数据使用了不同的字典索引),如何解决这个问题?(提示:参考Parquet的"冗余字典存储"机制)


附录:常见问题与解答

Q:列式存储适合所有场景吗?
A:不。如果业务需要频繁读取整行数据(如电商的"查看订单详情"),行式存储(如MySQL)更高效。列式存储是分析场景的优化解,不是万能解。

Q:Parquet和ORC有什么区别?
A:Parquet由Twitter和Cloudera开发,更侧重跨生态兼容性(Spark、Flink都支持);ORC由Hive团队开发,针对Hive查询做了优化(如更细粒度的索引)。实际中两者性能接近,选择主要看生态适配。

Q:列式存储如何处理缺失值?
A:通过**有效性位图(Validity Bitmap)**存储每列的缺失情况。例如,用一个二进制数组标记该位置的值是否有效(1=有效,0=缺失),避免因缺失值破坏数据连续性。


扩展阅读 & 参考资料

  1. 《大数据存储技术实战》—— 王磊(机械工业出版社)
  2. Apache Parquet官方文档:https://parquet.apache.org/docs/
  3. 论文《Dremel: Interactive Analysis of Web-Scale Datasets》(Google列式存储的经典设计)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1190883.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript对象深浅拷贝及解析

JavaScript对象深浅拷贝及解析下面这段代码使用了 JavaScript 中的对象展开运算符(...),核心作用是创建一个 obj1 的浅拷贝对象 obj2。1. 基础用法示例先看一个完整的可运行示例,帮你理解效果:// 原始对象 const …

sfda

hello world

大数据毕设项目:基于django的电子产品电商平台主数据管理系统(源码+文档,讲解、调试运行,定制等)

java毕业设计-基于springboot的(源码LW部署文档全bao远程调试代码讲解等) 博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、…

[豪の算法奇妙冒险] 代码随想录算法训练营第三十四天 | 62-不同路径、63-不同路径Ⅱ

LeetCode62 不同路径、LeetCode63 不同路径Ⅱ代码随想录算法训练营第三十四天 | 62-不同路径、63-不同路径ⅡLeetCode62 不同路径题目链接:https://leetcode.cn/problems/unique-paths/ 文章讲解:https://programmer…

第 471 场周赛Q1——3712. 出现次数能被 K 整除的元素总和

题目链接&#xff1a;3712. 出现次数能被 K 整除的元素总和&#xff08;简单&#xff09; 算法原理&#xff1a; 解法&#xff1a;哈希表 3ms击败54.15% 时间复杂度O(N) 思路很简单&#xff0c;先利用哈希表把数组中出现的元素对应的次数统计出来&#xff0c;然后遍历判断其次数…

C++课后习题训练记录Day71

1.练习项目&#xff1a; 题目描述 小蓝正在学习一门神奇的语言&#xff0c;这门语言中的单词都是由小写英文字母组 成&#xff0c;有些单词很长&#xff0c;远远超过正常英文单词的长度。小蓝学了很长时间也记不住一些单词&#xff0c;他准备不再完全记忆这些单词&#xff0c…

【Android 美颜相机】第十天:YUV420SP和RGB

YUV420SP转RGB及位图 本文将详细解析yuv-decoder.c文件中的JNI代码&#xff0c;该代码运行在Android平台&#xff0c;通过C语言实现YUV420SP格式图像数据到RGBA/ARBG格式的转换&#xff0c;以及位图像素调整功能&#xff0c;借助JNI完成Java与C层的交互&#xff0c;兼顾图像处理…

fpga 低频模块和高频模块之间单脉冲信号传输 verilog

User: fpga 低频模块和高频模块之间单脉冲信号传输 verilog Kimi: 在 FPGA 里,低频时钟域(<几十 MHz)和高频时钟域(>100 MHz)之间如果只用一根线传“单脉冲”,最核心的问题是: 高频域对这根线的采样窗口非…

CAD一键批量标注线长度——CAD c#二次开发

效果如下&#xff1a;支持直线、多段线、圆、圆弧、椭圆、椭圆弧、样条曲线、云线等曲线。

【Android 美颜相机】第十一天:GPUImageFilter解析

GPUImageFilter解析 GPUImageFilter 是 Android 平台基于 OpenGL ES 2.0 实现的 GPU 图像滤镜核心基类&#xff0c;为各类图像滤镜&#xff08;如美颜、风格化特效&#xff09;提供了统一的初始化、绘制、资源管理、参数配置能力。 本文将逐行解析该类的代码结构、实现逻辑及每…

microblaze是怎么通过把数据通过axi总线给到ip的

User: microblaze是怎么通过把数据通过axi总线给到ip的 Kimi: MicroBlaze 把数据“送”给 IP 并不是靠一根信号线,而是靠“AXI 总线协议”在地址空间上完成一次标准读写。只要 IP 核在 Vivado 里被挂到同一根 AXI 总线…

强烈安利8个AI论文软件,本科生搞定毕业论文!

强烈安利8个AI论文软件&#xff0c;本科生搞定毕业论文&#xff01; AI 工具让论文写作不再难 在当前的学术环境中&#xff0c;越来越多的本科生开始借助 AI 工具来辅助完成毕业论文。这些工具不仅能够帮助学生高效地完成内容创作&#xff0c;还能在降低 AIGC 率、保持语义通顺…

强烈安利8个AI论文软件,本科生搞定毕业论文!

强烈安利8个AI论文软件&#xff0c;本科生搞定毕业论文&#xff01; AI 工具让论文写作不再难 在当前的学术环境中&#xff0c;越来越多的本科生开始借助 AI 工具来辅助完成毕业论文。这些工具不仅能够帮助学生高效地完成内容创作&#xff0c;还能在降低 AIGC 率、保持语义通顺…

solaris vlan网卡设置

首先,查看当前机器的网卡信息: if config -a ls /etc/hostname* 确认当前机器有几块网卡?网卡名? 哪些网卡已经设置了 vlan子网卡。方式一,通过配置文件设置 假如在 e1000g3 网卡上,添加 vlan.107 子网卡,…

第 470 场周赛Q2——3702. 按位异或非零的最长子序列

题目链接&#xff1a;3702. 按位异或非零的最长子序列&#xff08;中等&#xff09; 算法原理&#xff1a; 解法&#xff1a;枚举 2ms击败95.80% 时间复杂度O(N) 先把数组中所有数全部异或在一起&#xff0c;如果异或结果不为零&#xff0c;就直接返回数组长度&#xff0c;如果…

文字标注旋转角度设置(防止文字倒立)

对CAD中曲线进行文字标记时&#xff0c;当文字角度必须随曲线角度时&#xff0c;为避免字头朝下&#xff0c;可采用如下方式ang ang % Math.PI;while (ang < 0) { ang Math.PI; }// 确保文字不会倒立&#xff08;阅读方向从左到右&#xff09;while (ang > Math.PI / 2…

换根 DP 简介

​【换根 DP 简介】● 换根 DP 是树形 DP 的一种重要技术,用于解决需要以树中‌不同节点为根‌分别计算答案的问题。其核心思想是在一次动态规划后,通过‌推导出换根时的状态转移公式‌,高效地计算出所有节点作为根…

文字标注旋转角度设置(防止文字倒立)

对CAD中曲线进行文字标记时&#xff0c;当文字角度必须随曲线角度时&#xff0c;为避免字头朝下&#xff0c;可采用如下方式ang ang % Math.PI;while (ang < 0) { ang Math.PI; }// 确保文字不会倒立&#xff08;阅读方向从左到右&#xff09;while (ang > Math.PI / 2…

【毕业设计】基于机器学习的网络购物平台的智能推荐(源码+文档+远程调试,全bao定制等)

java毕业设计-基于springboot的(源码LW部署文档全bao远程调试代码讲解等) 博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、…

储能辅助电力系统调峰的容量需求研究 Matlab代码

✅作者简介&#xff1a;热爱数据处理、建模、算法设计的Matlab仿真开发者。&#x1f34e;更多Matlab代码及仿真咨询内容点击 &#x1f517;&#xff1a;Matlab科研工作室&#x1f34a;个人信条&#xff1a;格物致知,完整Matlab代码获取及仿真咨询内容私信。&#x1f447; 关注我…