列存数据库GROUP BY更快因同字段值连续存储,聚合时仅读取相关列,大幅减少I/O与内存压力;行存需读取整行。实操需注意引擎特性、谓词下推、编码选择等细节。

为什么列存数据库做 GROUP BY 比行存快得多
核心原因是:列存把同一字段的所有值连续存储,聚合时只需读取涉及的几列、跳过无关字段,大幅减少 I/O 和内存带宽压力。行存(如 PostgreSQL)哪怕只查 user_id 和 amount 两列,也得把整行(比如含 created_at、ip_address、user_agent 等)全读出来。
实操中要注意:
- ClickHouse 的
ReplacingMergeTree在未MERGE前可能返回重复聚合结果,别直接信实时GROUP BY结果 - Druid 对高基数维度(如
user_id)做GROUP BY时,内存消耗陡增,容易触发QueryTimeoutException或TooManySegmentsException - Pinot 默认对
GROUP BY字段建倒排索引,但若字段值稀疏(如is_premium只有 true/false),倒排反而拖慢——这时该关掉invertedIndex配置
WHERE 条件下推到列存扫描层的关键条件
列存快的前提是谓词能下推到存储层过滤,否则就退化成“先读全量列再 CPU 过滤”,优势归零。
常见失效场景:
- ClickHouse 中用
toYYYYMMDD(toDateTime(timestamp_str)) = 20240501,字符串转时间函数无法下推;应改用原生时间类型 +partition by toYYYYMM(timestamp),让分区裁剪和谓词下推同时生效 - Druid 的
WHERE不支持嵌套 JSON 字段路径表达式(如event.payload.user.id = '123'),必须提前展平为扁平列或用json_extract_scalar(但后者不走索引) - Pinot 要求
WHERE字段必须出现在segment.column.indexing.enabled白名单里,否则直接报错Column not indexed for filtering
聚合函数在列存里的执行差异:不是所有 sum() 都一样
列存引擎常对聚合函数做向量化优化,但不同函数的加速程度天差地别。
典型表现:
- ClickHouse 的
sum()、count()、min()/max()直接走 SIMD 批处理,百万行聚合通常在毫秒级;但uniqCombined()(近似去重)会触发哈希表构建,内存占用翻倍,且无法跳过 NULL 块 - Druid 的
longSum、doubleSum是列级预聚合,快;但filtered聚合器(如{"type":"filtered","filter":{"type":"selector","dimension":"status","value":"success"},"aggregator":{"type":"count"}})需逐行判断,性能接近行存 - Pinot 的
COUNT(*)走元数据直接返回 segment 行数,极快;但COUNT(col)必须扫描该列非 NULL 值,若列稀疏(大量 NULL),实际耗时可能比COUNT(*)高 5–10 倍
写入时列存格式选择直接影响查询性能
列存不是“存了就快”,压缩格式、编码方式、分块粒度都得匹配查询模式。
几个硬约束:
- ClickHouse 推荐用
Delta编码存单调递增 ID,比DoubleDelta更省空间;但若字段频繁乱序(如session_id字符串哈希值),强行用Delta反而膨胀 20%+ - Druid 的
stringDictionary编码对低基数维度(如country)极高效,但若列唯一值超 100 万,字典构建失败,自动降级为compressed,查询变慢且不可预测 - Pinot 要求
sorted列(如时间戳)必须按升序写入,否则range查询无法利用排序跳过块;而乱序写入后调用SortByTime工具重建,耗时可能超过原始导入
列存的性能红利藏在细节里:一个没设对的编码、一次没绕开的 NULL 扫描、一个没压住的高基数维度,都可能让查询从 200ms 拉长到 2s。这些地方不报错,但慢得毫无征兆。











