一个真实的数据对比揭示了令人震惊的性能差距:同样的1000万行数据,运行相同的分析查询,PostgreSQL需要12.2秒,而ClickHouse只需要453毫秒——27倍的差距。这不是硬件差异,不是索引设计,而是两种存储范式在数据仓库场景下的必然结果。

2025年的一项基准测试更清晰地量化了这种差距的边界:在5万行以下的数据集上,PostgreSQL甚至更快;但当数据量突破100万行后,ClickHouse的性能优势开始显著扩大。在1000万行数据上,完整的分析工作负载(包括模式发现、数据采样、聚合计算和统计分析)在ClickHouse上只需453毫秒,而PostgreSQL需要12.2秒。

2008年,MIT和耶鲁大学的研究人员在SIGMOD会议上发表了一篇论文,标题是《Column-Stores vs. Row-Stores: How Different Are They Really?》。这篇论文的结论令数据库社区震动:即使将行式数据库的物理设计完全模仿列式布局——垂直分区、覆盖索引、物化视图——性能仍然落后数倍。列式存储的优势不是表面的存储排列问题,而是从存储层到执行引擎的系统性设计。

存储布局:两种哲学的根本分歧

行式存储的逻辑直观:一条用户记录的所有字段——姓名、年龄、地址、电话——在磁盘上连续存储。这种设计服务于事务处理(OLTP)的核心需求:快速定位并修改单条记录。当需要查询"用户ID为12345的所有信息"时,只需一次I/O操作。

列式存储的逻辑相反:将所有用户的姓名存储在一起,所有年龄存储在一起。这种设计服务于分析处理(OLAP)的核心需求:扫描大量数据并计算聚合。当需要查询"所有用户的平均年龄"时,只需读取年龄列,完全跳过姓名、地址、电话等无关数据。

CMU 15-721课程用一个具体例子说明了I/O差异:假设一个包含100列的事实表,每列8字节,共8亿行数据。查询只需要访问其中3列。

行式存储必须读取所有数据:100列 × 8字节 × 8亿行 = 640GB。列式存储只需读取3列:3列 × 8字节 × 8亿行 = 19.2GB。I/O量减少97%。

但这只是冰山一角。

压缩:同质性的数学红利

压缩算法的核心原理是消除冗余。行式存储中,同一行的不同字段类型各异——整数、字符串、日期、浮点数——相邻存储,数据异质性高,压缩效果有限。列式存储中,同一列的数据类型相同,且往往存在大量重复值或相似模式,压缩效率显著提升。

字典编码(Dictionary Encoding)是最常用的列式压缩技术。假设一个"国家"列包含100万行数据,但只有200个不同的国家名称。传统存储需要100万次存储国家名称字符串。字典编码将200个国家名称存储在一个字典中,每行只需存储一个2字节的整数ID——压缩比达到数十倍。

游程编码(Run-Length Encoding, RLE)在排序列上效果惊人。如果数据按"国家"排序,连续100万行都是"中国",传统存储需要100万次写入"China"字符串,而RLE只需存储(China, 1000000)——压缩比达到百万倍。

ClickHouse的官方文档提供了一个实际案例:一个包含8.1亿行的表,按日期列排序后,日期列只有2405个唯一值,平均游程长度接近25万。这一列的存储空间从原始的3.2GB压缩到不到64KB——压缩比超过50000倍。

CMU的研究表明,在Star Schema Benchmark测试中,压缩技术平均带来2倍性能提升;对于访问排序列的查询,性能提升可达10倍以上。

延迟物化:避免不必要的数据搬运

在行式存储中,查询执行的第一步往往是"物化"——从磁盘读取原始行,解析字段,构建内存中的行对象。这个过程消耗大量CPU和内存带宽。

列式存储引入了"延迟物化"(Late Materialization)策略:尽可能长时间地保持列式状态,延迟到查询的最后阶段才组装成行。

考虑一个典型的分析查询:

SELECT country, AVG(salary)
FROM employees
WHERE age > 40 AND department = 'Engineering'
GROUP BY country

行式存储的执行流程:读取所有行的完整数据 → 解析字段 → 应用过滤条件 → 聚合计算。

延迟物化的执行流程:只读取age列 → 应用age > 40过滤,生成位置列表 → 只读取department列(按位置)→ 应用department = 'Engineering'过滤,更新位置列表 → 只读取country和salary列(按最终位置)→ 聚合计算。

CMU论文的实验数据量化了这一优势:延迟物化平均带来3倍性能提升。核心原因有三:

避免无效数据读取:过滤条件可能淘汰90%的数据,延迟物化使这些被淘汰的行从未被完整读取。

保持压缩状态的执行:某些压缩格式(如RLE)可以直接在压缩数据上执行操作,无需解压。一个包含25万相同值的RLE块,聚合操作只需一次加法。

优化CPU缓存:列式数据在内存中连续存储,缓存命中率远高于跳跃访问行数据。

向量化执行:现代CPU的正确打开方式

传统数据库的Volcano执行模型采用"一次一元组"的处理方式:每个操作符处理一条记录后,通过函数调用传递给下一个操作符。这种设计在1980年代的硬件上是合理的,但在现代CPU上存在严重问题——大量函数调用开销、分支预测失败、指令缓存污染。

向量化执行采用"一次一向量"的处理方式:每个操作符处理一批数据(通常1024或8192个值),以数组形式在操作符间传递。这种设计完美匹配现代CPU的特性:

SIMD指令:AVX-512指令集可以同时处理16个32位整数或8个64位浮点数。一个向量化过滤操作可以用一条指令同时判断16个值是否满足条件。

减少分支:向量化操作使用紧密循环,分支预测器可以高度优化。单记录处理的分支模式不可预测,导致大量预测失败。

缓存友好:连续内存访问模式最大化利用CPU缓存行。DuckDB的文档指出,向量化执行使数据以"列式数组"形式在CPU寄存器间流动,L1/L2缓存利用率显著提升。

MonetDB/X100系统在2005年首次系统性地实现了向量化执行。后续的论文表明,向量化执行相比传统元组处理可以带来数倍性能提升。

Zone Map与谓词下推:跳过不相关的数据

列式存储的另一个核心优化是统计信息驱动的数据跳过。主流实现都采用了类似的技术:

Apache Parquet在文件级别维护每个Row Group的统计信息:每列的最小值、最大值、空值数量。查询时,如果过滤条件与某列的最小/最大值范围不匹配,整个Row Group可以被跳过。

ClickHouse为每个数据块(默认8192行)维护主键索引,存储每个Granule的主键范围。范围查询可以通过二分查找直接定位相关数据块,避免全表扫描。

Snowflake称之为"Micro-Partitions":每个微分区维护所有列的统计信息。查询优化器可以利用这些信息跳过大量数据。

DuckDB的基准测试显示,对于选择性高的查询,Zone Map可以使数据读取量减少100倍以上。

写入性能的代价:没有免费的午餐

列式存储的分析性能优势并非没有代价。单行插入和更新操作在列式存储中极其低效——修改一条记录需要定位并更新多个列文件,每个文件可能位于磁盘的不同位置。

这就是为什么列式数据库通常采用"批量写入"策略。ClickHouse的MergeTree引擎将写入操作缓冲在内存中,定期合并成不可变的列式文件。Snowflake采用类似的设计,数据以批量方式加载到微分区中。

对于需要频繁单行更新的OLTP场景,行式存储仍然是更好的选择。这也是为什么现代数据库架构常常采用"HTAP"(混合事务/分析处理)设计——在行式存储上叠加列式投影,或通过数据复制在两个系统间同步。

从学术研究到工业实践

列式存储的商业化历程可以追溯到1990年代。Sybase IQ(现SAP IQ)于1994年发布,是第一个商业化的列式数据库。但真正推动列式存储成为主流的是2005年前后的学术研究:

Michael Stonebraker等人在VLDB 2005发表的C-Store论文系统性地阐述了列式数据库的设计原则,包括压缩、延迟物化、覆盖索引等技术。这篇论文后来催生了Vertica商业产品。

Peter Boncz等人在CIDR 2005发表的MonetDB/X100论文首次详细描述了向量化执行引擎的设计,影响了后来的ClickHouse、DuckDB等系统。

2010年代,云原生数据仓库的兴起将列式存储推向新高度。Amazon Redshift(基于ParAccel)、Google BigQuery(基于Dremel)、Snowflake都采用列式存储作为核心架构。

Apache Parquet和Apache ORC格式确立了列式存储在数据湖架构中的地位。Databricks的Delta Lake、Apache Iceberg、Apache Hudi等表格格式进一步将列式存储与事务语义结合。

性能数据的现实检验

FiveOneFour在2025年发布的基准测试提供了详细的量化数据:在1000万行的飞机追踪数据上,完整的分析工作负载(模式发现、数据采样、聚合计算、统计分析)在ClickHouse上耗时453毫秒,而PostgreSQL需要12.2秒——约27倍差距。

TechWolf Engineering分享了更极端的案例:一个在PostgreSQL上需要5分钟的聚合查询,迁移到ClickHouse后只需300毫秒——1000倍性能提升。

存储空间的差异同样显著。PostHog报告称,同样的数据在PostgreSQL中占用120GB,而ClickHouse只需要8GB——15倍压缩比。这得益于列式存储的高效压缩编码。

选择的艺术:行存还是列存

没有通用的最佳选择,只有特定场景下的最优权衡。

FiveOneFour的基准测试揭示了一个关键临界点:5万行是分水岭。在此之下,PostgreSQL的执行开销更低,性能反而更好;超过5万行,列式存储的优势开始显现;超过100万行,差距变得显著。

行式存储适合:

  • 高频单行读写的事务处理
  • 需要完整记录检索的场景
  • 写入密集型工作负载
  • 数据量在5万行以下的分析查询

列式存储适合:

  • 大规模数据扫描和聚合分析
  • 宽表但只访问少数列的查询
  • 读密集的分析工作负载
  • 数据量超过100万行的场景

现代实践越来越倾向于混合方案:PostgreSQL的列式存储扩展、SQL Server的列存储索引、MySQL的HeatWave加速器,都在尝试在行式架构上叠加列式能力。

数据湖架构更是体现了这种融合:底层存储使用Parquet/ORC列式格式,上层通过Trino、Spark等引擎提供SQL查询接口,实现分析工作负载的高效执行。

References

  1. Abadi, D. J., Madden, S. R., & Hachem, N. (2008). Column-Stores vs. Row-Stores: How Different Are They Really? SIGMOD ‘08.
  2. Stonebraker, M., et al. (2005). C-Store: A Column-oriented DBMS. VLDB ‘05.
  3. Boncz, P., Zukowski, M., & Nes, N. (2005). MonetDB/X100: Hyper-Pipelining Query Execution. CIDR ‘05.
  4. ClickHouse Documentation. Architecture Overview. https://clickhouse.com/docs/academic_overview
  5. Apache Parquet. File Format Specification. https://parquet.apache.org/docs/file-format/
  6. DuckDB Documentation. Columnar Storage Guide. https://motherduck.com/learn-more/columnar-storage-guide/
  7. Snowflake Documentation. Key Concepts and Architecture. https://docs.snowflake.com/en/user-guide/intro-key-concepts
  8. CMU 15-721 Advanced Database Systems. Database Compression. https://15721.courses.cs.cmu.edu/spring2023/slides/05-compression.pdf
  9. FiveOneFour Blog. PostgreSQL vs ClickHouse: What I Learned From My First Database Benchmark. https://www.fiveonefour.com/blog/PostgreSQL-vs-ClickHouse
  10. TechWolf Engineering. How we made real-time analytics 1000 times faster. https://medium.com/techwolf-engineering/how-we-made-real-time-analytics-1000-times-faster-from-postgres-to-clickhouse-8fae773de5f5