为什么理解 Cluster Key,要先理解“数据放在哪里”
很多用户第一次接触 Snowflake 或 Databend 的 Cluster Key,会把它理解成数据库索引,或者简单类比为
ORDER BY
数据更有序以后,查询为什么会扫描得更少?
答案不在 Cluster Key 本身,而在它背后的完整链路:
数据写入物理单元
→ 引擎记录每个物理单元的列统计信息
→ 查询谓词与统计信息比较
→ 排除不可能命中的物理单元
→ 只读取剩余数据
Cluster Key 的作用,是改善这条链路中的第一步:让相关数据在物理上更集中,使后续的 Data Pruning 更有效。
从这个角度看,可以把 Cluster Key 暂时理解为一种由引擎维护、作用在 Micro-partition / Block 层面的自动、动态 Range Partition。这个类比有助于建立直觉,但它并不意味着 Cluster Key 等同于传统硬分区,后文会专门说明两者的边界。
Snowflake 与 Databend 在 Micro-partition / Block、列统计信息、Pruning 和 Clustering 上可以建立相似的心智模型。但两者的存储格式、后台维护机制和操作接口并不完全相同。
传统 Partition:用户先定义边界,引擎再放置数据
假设有一张持续增长的 LLM Eval 结果表:
CREATE TABLE llm_eval_results (
created_at TIMESTAMP,
project_id VARCHAR,
eval_run_id VARCHAR,
trace_id VARCHAR,
score DOUBLE,
passed BOOLEAN
);
随着评测任务不断运行,这张表可能积累数亿甚至数十亿行数据。传统数仓常按月建立 Range Partition:
2026-07 partition
2026-08 partition
2026-09 partition
在对象存储中,也可能表现为目录边界:
/year=2026/month=07/
/year=2026/month=08/
/year=2026/month=09/
这种分区方式有四个典型特征:
-
分区字段由用户选择;
-
分区边界通常由用户预先定义;
-
每个分区都有明确的逻辑范围;
-
一个分区内部仍然可能包含大量文件、Row Groups 或 Blocks。
查询某一天的数据时:
WHERE created_at >= '2026-08-01'
AND created_at < '2026-08-02'
引擎可以先排除 7 月和 9 月,只访问 8 月分区。这就是传统的 Partition Pruning。
问题在于,Partition Pruning 通常只能把范围缩小到一个较粗的边界。进入 8 月分区后,如果缺少更细粒度的统计信息,引擎仍然可能读取大量数据。因此,粗粒度 Partition 不能替代文件、Row Group 或 Block 级别的 Pruning。
传统分区还需要在粒度之间做取舍:
-
分区太粗,分区内部扫描量仍然很大;
-
分区太细,容易产生大量小文件和元数据;
-
新范围到来时,可能需要继续维护分区边界;
-
查询模式变化后,原有分区设计未必仍然合适。
Snowflake 和 Databend 采用了另一种思路:用户不必逐个声明大量细粒度分区,引擎会根据数据量、写入批次和存储策略自动形成更小的物理数据单元。
flowchart LR
subgraph Traditional[传统 Partition:用户预先定义]
P1[2026-07]
P2[2026-08]
P3[2026-09]
end
subgraph Engine[引擎自动形成的物理单元]
B1[Unit 1<br/>07-01 ~ 07-06]
B2[Unit 2<br/>07-07 ~ 07-15]
B3[Unit 3<br/>07-16 ~ 08-02]
end
图 1:传统 Partition 使用用户定义的粗粒度边界;Micro-partition / Block 由引擎自动形成,边界可以更细,也不必与自然月严格对齐。
Micro-partition 与 Block:引擎自动形成的物理数据单元
Snowflake 会自动把表数据组织成 Micro-partitions。Databend 的 Fuse Engine 会把数据写入 Blocks,并使用更高一层的 Segments 组织多个 Blocks。
两者内部实现并不相同,但在理解 Cluster Key 时,可以先使用下面这个共同模型:
Table
├── Physical Unit 1
├── Physical Unit 2
├── Physical Unit 3
└── Physical Unit 4
其中:
-
在 Snowflake 中,Physical Unit 可以理解为 Micro-partition;
-
在 Databend 中,Physical Unit 可以理解为 Block。
与传统 Partition 相比,关键差异不是名称,而是边界由谁管理:
用户通常不需要逐个定义 Micro-partition 或 Block 的范围。引擎会自动形成这些物理单元,并为它们维护可用于查询裁剪的元数据。
最容易理解的一类元数据,是每一列在物理单元中的最小值和最大值,也就是 Min/Max。
例如,某列的物理分布为:
Block A
created_at: [2026-07-01, 2026-07-07]
Block B
created_at: [2026-07-08, 2026-07-14]
Block C
created_at: [2026-08-01, 2026-08-07]
此时查询:
WHERE created_at >= '2026-07-10'
AND created_at < '2026-07-12'
引擎可以在读取实际列数据之前比较查询范围与 Min/Max:
-
Block A 的最大值早于 7 月 10 日,可以跳过;
-
Block C 的最小值晚于查询范围,可以跳过;
-
Block B 与查询范围相交,可能包含结果,需要读取。
这就是 Min/Max Pruning,也常被称为 Data Skipping。
flowchart LR
Q[查询范围<br/>07-10 ~ 07-12]
A[Block A<br/>07-01 ~ 07-07]
B[Block B<br/>07-08 ~ 07-14]
C[Block C<br/>08-01 ~ 08-07]
Q --> A
Q --> B
Q --> C
A -->|范围不相交| SA[跳过]
B -->|范围相交| RB[读取]
C -->|范围不相交| SC[跳过]
classDef skip fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20;
classDef read fill:#fff3e0,stroke:#ef6c00,color:#e65100;
class PA,PC skip;
class RB read;
图 2:谓词与物理单元的 Min/Max 不相交时,引擎可以直接跳过;范围相交只表示“可能命中”。
这里有一个非常重要的边界:
Min/Max 只能判断目标值是否一定不在这个物理单元中,不能证明范围内的某个值一定存在。
例如:
Block A trace_id: [trace_0001, trace_9999]
查询:
WHERE trace_id = 'trace_5000'
即使
trace_5000
Pruning 为什么会失效:问题往往出在范围重叠
Min/Max Pruning 的效果,很大程度上取决于同一个物理单元中的数据是否接近,以及不同物理单元的范围是否大量重叠。
数据接近有序时
假设数据按时间接近有序地写入:
Block A: [07-01, 07-07]
Block B: [07-08, 07-14]
Block C: [07-15, 07-21]
查询 7 月 10 日时,只需要读取 Block B。
数据高度乱序时
如果多个任务并发写入,同时不断回灌历史数据,物理布局可能变成:
Block A: [07-01, 07-21]
Block B: [07-02, 07-20]
Block C: [07-03, 07-19]
查询同样的 7 月 10 日,三个 Blocks 都可能包含结果,因此一个也不能排除。
flowchart TB
Q[查询:07-10]
subgraph Good[范围窄、重叠少]
G1[A:07-01 ~ 07-07]
G2[B:07-08 ~ 07-14]
G3[C:07-15 ~ 07-21]
end
subgraph Bad[范围宽、重叠多]
X1[A:07-01 ~ 07-21]
X2[B:07-02 ~ 07-20]
X3[C:07-03 ~ 07-19]
end
Q -->|只保留 B| Good
Q -->|A、B、C 都可能命中| Bad
图 3:同一个查询面对不同数据布局时,候选 Blocks 数量可能完全不同。
这说明,Pruning 变差不一定是因为引擎没有 Min/Max,而可能是因为:
-
单个物理单元的范围太宽;
-
多个物理单元的范围高度重叠;
-
同一个 Key Value 同时落入过多候选范围。
Overlap 和 Clustering Depth 正是用于描述这种状态的指标。直观地说,同一个值覆盖的物理范围越多,查询需要保留的候选单元通常越多。Snowflake 可以通过
SYSTEM$CLUSTERING_INFORMATION
clustering_information
Cluster Key 要解决的核心问题,就是降低这种无效重叠。
Cluster Key 到底改变了什么
假设设置:
CLUSTER BY (created_at)
它表达的不是“为
created_at
created_at
理想情况下,布局从:
Block A: [07-01, 07-21]
Block B: [07-02, 07-20]
Block C: [07-03, 07-19]
逐步改善为:
Block A: [07-01, 07-07]
Block B: [07-08, 07-14]
Block C: [07-15, 07-21]
它产生效果的因果链是:
定义 Cluster Key
→ 相近的 Key Values 更集中
→ 每个物理单元的值域更窄
→ 不同物理单元的范围重叠减少
→ Min/Max Pruning 排除更多候选单元
→ 实际扫描的数据量下降
因此,Cluster Key 不是查询结果的直接入口,也不会像 B-tree 一样把查询定位到某个 Row ID。它通过改善数据布局,间接增强已有元数据的裁剪能力。
Reclustering 是维护过程,不是免费收益
持续写入、更新和历史数据回灌会再次打乱物理布局。Reclustering 会选择部分数据重新组织,使其更接近 Cluster Key 所描述的顺序。
这个过程需要排序、读取、重写和存储资源,因此存在明确成本:
-
Snowflake 主要通过 Automatic Clustering 在后台维护聚类,Manual Reclustering 已被标记为 Deprecated。后台维护使用服务端计算资源,并产生相应 Credits 与存储影响。
-
Databend 当前也提供后台自动聚类能力,同时保留显式的
,并支持ALTER TABLE ... RECLUSTER、FINAL和WHERE等控制方式。显式 Recluster 同样会消耗时间和计算资源,在 Databend Cloud 中也会产生 Credits。LIMIT
Reclustering 的目标是改善最混乱的数据范围,而不是保证所有 Micro-partitions / Blocks 最终完全不重叠。是否值得维护,取决于减少的查询扫描成本能否覆盖聚类成本。
为什么可以把 Cluster Key 理解为“动态 Range Partition”
传统 Range Partition 的工作方式是:
用户预先定义范围
→ 2026-07 放入 7 月分区
→ 2026-08 放入 8 月分区
→ 2026-09 放入 9 月分区
Cluster Key 驱动的物理组织更接近:
用户指定聚集维度
→ 引擎按数据量自动形成物理单元
→ 每个物理单元根据实际数据得到 Min/Max 范围
→ 后台维护根据数据变化动态调整布局
例如,按时间聚类后形成的边界可能是:
Block 1: [07-01 00:00, 07-03 15:20]
Block 2: [07-03 15:21, 07-08 09:10]
Block 3: [07-08 09:11, 07-19 18:40]
Block 4: [07-19 18:41, 08-02 11:30]
这些边界不是用户提前写在 DDL 中的,而是由真实数据分布与物理单元大小共同决定。
| 对比项 | 传统 Range Partition | Cluster Key 驱动的物理组织 |
|---|---|---|
| 边界由谁定义 | 用户预先定义 | 引擎根据数据自动形成 |
| 常见粒度 | 较粗的业务范围 | Micro-partition / Block 级别 |
| 边界是否固定 | 相对固定 | 随写入和聚类维护变化 |
| 用户声明什么 | 具体 Partition Range | 聚集列或表达式 |
| 主要价值 | 数据管理、生命周期和粗粒度裁剪 | 改善物理局部性和细粒度裁剪 |
这个类比最有价值的地方,是帮助读者理解“按范围组织数据”。它的边界也必须说清楚:
Cluster Key 可以看作 Micro-partition / Block 层面的自动、动态 Range Partition,但它不是传统硬分区,也不保证每个物理单元严格落在某个业务边界内。
一个必要的复合 Key 示例:month, trace_id 代表什么
本文不展开 Cluster Key 的选列方法,但要正确理解其物理语义,必须先澄清复合 Key 的顺序。
如果查询通常先限定月份,再回查
trace_id
CLUSTER BY (
DATE_TRUNC('month', created_at),
trace_id
)
可以把这个 Tuple 按字典序理解:
先按 month 组织
→ month 相同或接近时,再按 trace_id 组织
可能形成类似布局:
Block 1: 2026-07 / trace_0001 ... trace_1000
Block 2: 2026-07 / trace_1001 ... trace_2000
Block 3: 2026-07 / trace_2001 ... 2026-08 / trace_0050
Block 4: 2026-08 / trace_0051 ... trace_1100
Block 5: 2026-08 / trace_1101 ... trace_2200
注意 Block 3 仍可能跨月。这再次说明,
month
这种布局适合同时包含月份和
trace_id
WHERE created_at >= '2026-08-01'
AND created_at < '2026-09-01'
AND trace_id = 'trace_1024'
引擎可以先利用月份范围排除大量物理单元,再使用
trace_id
但如果查询只有:
WHERE trace_id = 'trace_1024'
同一个
trace_id
因此:
不等于同时拥有一套独立的 Month 索引和一套独立的CLUSTER BY (month, trace_id)索引。前导表达式决定主要物理顺序,后续表达式主要在前导值相同或接近的范围内发挥作用。trace_id
至于哪些列应该放在前面、时间应该保留原始 Timestamp 还是降低到 Day / Week / Month,以及高基数
trace_id
Snowflake 与 Databend:相似的思想,不同的产品边界
Snowflake 和 Databend 都通过自动形成的细粒度物理单元、列统计信息和聚类布局减少扫描。对用户来说,两者可以共享“改善物理局部性以增强 Pruning”这一核心心智模型。
但在产品能力与使用方式上,仍有几处值得区分。
| 维度 | Snowflake | Databend |
|---|---|---|
| 基础物理单元 | Micro-partition | Fuse Block,多个 Blocks 由 Segment 组织 |
| Cluster Key 的核心作用 | 将相关记录共同组织到更合适的 Micro-partitions | 按列或表达式改善 Blocks 的物理组织 |
| 默认维护方式 | Automatic Clustering 后台评估并维护;Manual Reclustering 已 Deprecated | 提供后台自动聚类,同时保留显式 |
| 显式维护能力 | 可暂停或恢复 Automatic Clustering,主要由服务管理资源 | |
| 聚类观测 | | |
| 主要用户心智 | 声明聚集维度,由托管服务维护,并关注 Credits 与存储影响 | 声明聚集维度,可依赖自动维护,也可查看 Block 级指标并显式调优 |
Snowflake:强调托管式的自动维护
Snowflake 默认自动创建 Micro-partitions,并维护其列级元数据。对于非常大的表,如果自然写入顺序无法满足主要查询路径,用户可以定义 Clustering Key,之后主要由 Automatic Clustering 评估并执行维护。
这种产品体验的重点是“声明目标,平台维护”。用户不需要指定执行 Reclustering 的 Warehouse,但仍然需要评估 Automatic Clustering 带来的 Credits 和存储成本。Snowflake 官方也明确指出,Clustering Key 并不适合所有表,通常更适用于具有大量 Micro-partitions、查询足够有选择性且访问模式较稳定的大表。
Databend:自动维护之外,保留更直接的物理调优入口
Databend 使用 Fuse Blocks 和 Segments 组织对象存储上的数据。定义 Cluster Key 后,系统可以在后台持续维护数据布局;与此同时,Databend 仍向用户开放
RECLUSTER
clustering_information
这使 Databend 的用户心智同时包含两层:
-
声明式使用:定义 Cluster Key,让系统持续改善物理布局;
-
可观察、可干预:通过 Block Count、Overlap 和 Depth 判断聚类质量,并在需要时显式控制 Recluster 范围。
这不是“自动”与“手动”的简单二选一,而是产品边界不同:Snowflake 更强调由托管服务接管维护;Databend 在提供自动聚类的同时,保留了更直接的 SQL 控制面。对数据工程团队而言,后者便于把聚类维护纳入批处理窗口或特定数据范围,但也要求团队理解显式操作的资源与写入影响。
不要把 Cluster Key 当作通用加速开关
无论使用 Snowflake 还是 Databend,Cluster Key 都不应该默认加到每一张表上。它更适合满足以下条件的场景:
-
表已经足够大,查询确实扫描了大量物理单元;
-
主要查询包含稳定且有选择性的过滤条件;
-
多数高价值查询可以从同一组聚集维度获益;
-
查询节省能够覆盖持续写入和 Reclustering 的维护成本。
对于小表、查询模式高度随机的表,或者自然写入顺序已经与主要过滤维度一致的表,增加 Cluster Key 可能收益有限,甚至只增加维护成本。
建立正确心智:Cluster Key 优化的是扫描路径,不是 SQL 语法
现在可以把 Partition、Micro-partition、Block、Pruning 和 Cluster Key 串成一条完整链路:
传统 Partition
→ 用户定义粗粒度硬边界
Snowflake Micro-partition / Databend Block
→ 引擎自动形成细粒度物理单元
Min/Max Metadata
→ 描述每个物理单元可能覆盖的值域
Pruning
→ 排除不可能命中谓词的物理单元
Cluster Key
→ 改善数据的物理局部性
→ 缩小单元值域并减少范围重叠
→ 让 Pruning 跳过更多数据
Reclustering
→ 在持续写入后维护这种布局
→ 以额外计算和存储成本换取扫描收益
对新用户来说,最重要的认识不是记住某个 DDL,而是把 Cluster Key 看成一项物理布局选择。任何布局都会偏向一类查询,也必然对其他查询做出取舍。
下一步才是实践问题:
-
哪些列最常出现在有选择性的过滤条件中?
-
复合 Cluster Key 的列顺序应该怎么排?
-
时间字段应该保留原始 Timestamp,还是按 Day、Week、Month 降低粒度?
-
、trace_id这类高基数字段是否适合作为 Cluster Key?eval_run_id
-
Boolean、Status 等低基数字段为什么可能没有效果?
-
自然写入顺序已经接近有序时,还有没有必要聚类?
-
如何通过扫描比例、Overlap 和 Depth 验证 Cluster Key 是否真正有效?
这些问题将在本系列第二篇《Cluster Key 最佳实践:列怎么选、顺序怎么排,粒度多大才合适?》中展开。
订阅我们的新闻简报
及时了解功能发布、产品规划、支持服务和云服务的最新信息!






