文章目录
- 一、一句话区分
- [二、图解:Sharding + Partitioning 组合](#二、图解:Sharding + Partitioning 组合)
- 三、详细对比
- 四、分片的完整设置流程
-
- [4.1 第一步:集群配置(一次性配置)](#4.1 第一步:集群配置(一次性配置))
- [4.2 第二步:建表时指定分片键](#4.2 第二步:建表时指定分片键)
- [4.3 分片键不可修改](#4.3 分片键不可修改)
- [五、在 ClickHouse 中的具体实现](#五、在 ClickHouse 中的具体实现)
-
- [5.1 分区(Partitioning):本地表的物理切分](#5.1 分区(Partitioning):本地表的物理切分)
- [5.2 分片(Sharding):分布式表的跨节点路由](#5.2 分片(Sharding):分布式表的跨节点路由)
- [5.3 两者配合的查询执行流程](#5.3 两者配合的查询执行流程)
- 六、生产环境组合案例
-
- [6.1 场景:用户行为日志表](#6.1 场景:用户行为日志表)
- [6.2 完整建表流程](#6.2 完整建表流程)
- [6.3 效果](#6.3 效果)
- 七、关于轮询写入的问题(你的场景)
- 八、常见误区与澄清
- 九、选择建议
- 十、总结
在 ClickHouse 集群中,"分片"(Sharding)和"分区"(Partitioning)是两个极易混淆的核心概念。很多人误以为分区就是分片,或者认为两者是互斥的------实际上,它们是不同维度、可以协同工作的数据切分策略。本文将系统梳理两者的区别、联系,并通过生产案例展示如何组合使用,帮助你彻底理解并正确应用于实际项目。
一、一句话区分
| 概念 | 核心作用 | 数据范围 | 存储位置 |
|---|---|---|---|
| Sharding(分片) | 水平扩展,解决单机存不下、算不动的问题 | 不同数据在不同节点 | 跨节点(集群级别) |
| Partitioning(分区) | 局部过滤,解决查询扫全表的问题 | 同一节点内的数据再切分 | 单节点内部(本地级别) |
简单关系:一个大表 → 先按 Sharding 分散到不同节点 → 每个节点内部再按 Partitioning 切分成小块。
二、图解:Sharding + Partitioning 组合
┌─────────────────────────────────────────────────────────────────────────┐
│ 分布式表(逻辑视图) │
│ user_id 作为分片键 │
└─────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ 节点1(分片1) │ │ 节点2(分片2) │ │ 节点3(分片3) │
│ user_id: 1,4,7... │ │ user_id: 2,5,8... │ │ user_id: 3,6,9... │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │ │ ┌───────────────────┐ │
│ │ 分区1:2025-01-01 │ │ │ │ 分区1:2025-01-01 │ │ │ │ 分区1:2025-01-01 │ │
│ │ 分区2:2025-01-02 │ │ │ │ 分区2:2025-01-02 │ │ │ │ 分区2:2025-01-02 │ │
│ │ 分区3:2025-01-03 │ │ │ │ 分区3:2025-01-03 │ │ │ │ 分区3:2025-01-03 │ │
│ │ ... │ │ │ │ ... │ │ │ │ ... │ │
│ └───────────────────┘ │ │ └───────────────────┘ │ │ └───────────────────┘ │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘
↑ ↑ ↑
Partitioning(本地切分) Partitioning(本地切分) Partitioning(本地切分)
三、详细对比
| 对比维度 | Sharding(分片) | Partitioning(分区) |
|---|---|---|
| 目标 | 突破单机容量/性能瓶颈 | 减少查询扫描的数据量 |
| 粒度 | 节点级(跨机器) | 文件级(单机内部) |
| 对查询影响 | 决定去哪个节点查 | 决定查哪个文件块 |
| 对写入影响 | 决定数据去哪个节点 | 决定数据进哪个文件块 |
| 典型键 | 用户ID、订单ID(高基数列) | 日期(时间列) |
| ClickHouse 语法 | Distributed 表引擎 + 分片键 |
PARTITION BY |
| 副本关系 | 每个分片可有多个副本 | 分区是副本内部的子集 |
| 设置时机 | 建表时(不可改) | 建表时可改(需迁移) |
| 是否可修改 | ❌ 不可修改,需重建表 | ⚠️ 可改,但需重分区 |
四、分片的完整设置流程
很多人以为分片只是建表时写个 Distributed 引擎就行了。实际上,分片的设置分为两步:集群配置 + 建表指定。
4.1 第一步:集群配置(一次性配置)
在 ClickHouse 配置文件中定义集群,指定节点与分片的归属关系。
配置文件位置 :/etc/clickhouse-server/config.d/cluster.xml(推荐)或 config.xml
xml
<?xml version="1.0"?>
<clickhouse>
<!-- 远程服务器配置:定义集群 -->
<remote_servers>
<!-- 集群名称,建表时会用到 -->
<analytics_cluster>
<!-- 分片1 -->
<shard>
<!-- 可选:是否内部复制(副本间自动同步) -->
<internal_replication>true</internal_replication>
<!-- 副本1 -->
<replica>
<host>clickhouse-node01</host>
<port>9000</port>
<!-- 可选:优先级,用于负载均衡 -->
<priority>1</priority>
</replica>
<!-- 副本2 -->
<replica>
<host>clickhouse-node02</host>
<port>9000</port>
<priority>2</priority>
</replica>
</shard>
<!-- 分片2 -->
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>clickhouse-node03</host>
<port>9000</port>
<priority>1</priority>
</replica>
<replica>
<host>clickhouse-node04</host>
<port>9000</port>
<priority>2</priority>
</replica>
</shard>
</analytics_cluster>
</remote_servers>
<!-- 可选:分片宏配置,用于副本表路径 -->
<macros>
<shard>01</shard>
<replica>node01</replica>
</macros>
</clickhouse>
配置说明:
| 配置项 | 含义 |
|---|---|
remote_servers |
集群配置的根节点 |
analytics_cluster |
自定义集群名称(建表时使用) |
shard |
一个分片节点组 |
internal_replication |
是否由 ClickHouse 自动同步副本(推荐 true) |
replica |
分片内的一个物理节点 |
priority |
优先级,数字越小越优先(配合 load_balancing=in_order 使用) |
macros |
宏变量,用于副本表路径(如 ReplicatedMergeTree) |
验证配置是否生效:
sql
-- 查看集群信息
SELECT * FROM system.clusters WHERE cluster = 'analytics_cluster';
-- 显示所有集群
SHOW CLUSTERS;
4.2 第二步:建表时指定分片键
集群配置好之后,需要在创建分布式表时指定分片键。
sql
-- 1. 在每个节点上创建本地表(存真实数据)
CREATE TABLE user_log_local ON CLUSTER analytics_cluster
(
event_date Date,
user_id UInt64,
event_type LowCardinality(String),
page_url String
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/user_log', '{replica}')
PARTITION BY toYYYYMMDD(event_date) -- 分区:按天
ORDER BY (user_id, event_date);
-- 2. 创建分布式表(指定分片键,作为查询入口)
CREATE TABLE user_log_distributed ON CLUSTER analytics_cluster
AS user_log_local
ENGINE = Distributed(
'analytics_cluster', -- 集群名称(与配置文件一致)
'default', -- 数据库名
'user_log_local', -- 本地表名
user_id -- 分片键:按 user_id 路由到不同分片
);
分片键的选择:
| 分片键类型 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 高基数列 | user_id |
数据分布均匀,查询时易命中单分片 | 无明显缺点 |
| 随机数 | rand() |
绝对均匀 | 无法按业务裁剪分片 |
| 低基数列 | city |
无 | 分布不均,可能倾斜 |
4.3 分片键不可修改
sql
-- ❌ 不支持修改分片键
-- 没有 ALTER 语法可以修改分布式表的分片键
为什么?
- 数据已经按旧规则分布在各个分片上了
- 改分片键意味着要重分布所有数据(成本极高)
如果需要修改:
sql
-- 1. 创建新分布式表(使用新分片键)
CREATE TABLE user_log_distributed_new AS user_log_local
ENGINE = Distributed('analytics_cluster', 'default', 'user_log_local', new_sharding_key);
-- 2. 迁移数据(从旧表插入到新表,会按新规则重新路由)
INSERT INTO user_log_distributed_new SELECT * FROM user_log_distributed;
-- 3. 删除旧表,重命名新表
DROP TABLE user_log_distributed;
RENAME TABLE user_log_distributed_new TO user_log_distributed;
五、在 ClickHouse 中的具体实现
5.1 分区(Partitioning):本地表的物理切分
sql
-- 在每个节点上创建本地表,指定分区键
CREATE TABLE orders_local ON CLUSTER cluster
(
event_date Date,
user_id UInt64,
amount Decimal(10,2)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date) -- 按月分区
ORDER BY (user_id, event_date);
效果 :每个节点内部,数据按月份切分成不同的文件目录。查询 WHERE event_date = '2025-01-15' 时,只扫描对应月份的分区目录。
5.2 分片(Sharding):分布式表的跨节点路由
sql
-- 创建分布式表(逻辑视图,不存数据)
CREATE TABLE orders_distributed AS orders_local
ENGINE = Distributed(
'cluster', -- 集群名(在配置文件中定义)
'default', -- 数据库名
'orders_local', -- 本地表名
user_id -- 分片键:不同 user_id 去不同节点
);
5.3 两者配合的查询执行流程
sql
-- 查询:同时利用分片和分区裁剪
SELECT sum(amount)
FROM orders_distributed
WHERE event_date = '2025-01-15' -- 分区裁剪
AND user_id = 12345; -- 分片裁剪
| 步骤 | 做了什么 | 利用的机制 |
|---|---|---|
| 1 | 根据 user_id = 12345 确定数据在哪个分片 |
Sharding |
| 2 | 只向该分片发送查询 | 避免扫描所有节点 |
| 3 | 在该分片的节点内,根据 event_date 定位到具体分区 |
Partitioning |
| 4 | 只扫描该分区文件 | 避免扫描整节点数据 |
六、生产环境组合案例
6.1 场景:用户行为日志表
需求:
- 日均 10 亿行数据
- 查询通常是:某天 + 某个用户的行为
- 需要保留 90 天
6.2 完整建表流程
1. 集群配置 (/etc/clickhouse-server/config.d/analytics_cluster.xml):
xml
<clickhouse>
<remote_servers>
<analytics_cluster>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>node01</host>
<port>9000</port>
</replica>
<replica>
<host>node02</host>
<port>9000</port>
</replica>
</shard>
<shard>
<internal_replication>true</internal_replication>
<replica>
<host>node03</host>
<port>9000</port>
</replica>
<replica>
<host>node04</host>
<port>9000</port>
</replica>
</shard>
</analytics_cluster>
</remote_servers>
</clickhouse>
2. 本地表 + 分布式表:
sql
-- 本地表:按用户ID分片,按天分区
CREATE TABLE user_log_local ON CLUSTER analytics_cluster
(
event_date Date,
user_id UInt64,
event_type LowCardinality(String),
page_url String
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/user_log', '{replica}')
PARTITION BY toYYYYMMDD(event_date) -- 按天分区
ORDER BY (user_id, event_date);
-- 分布式表
CREATE TABLE user_log_distributed ON CLUSTER analytics_cluster
AS user_log_local
ENGINE = Distributed(analytics_cluster, default, user_log_local, user_id);
6.3 效果
| 查询示例 | 分片裁剪 | 分区裁剪 | 扫描范围 |
|---|---|---|---|
WHERE user_id=123 AND event_date='2025-01-15' |
✅ 命中 1 个分片 | ✅ 命中 1 个分区 | 1 个分片的 1 天数据 |
WHERE user_id=123 |
✅ 命中 1 个分片 | ❌ 无 | 1 个分片的全部 90 天数据 |
WHERE event_date='2025-01-15' |
❌ 全分片 | ✅ 命中 1 个分区 | 全部分片的 1 天数据 |
| 无过滤 | ❌ 全分片 | ❌ 无 | 全部分片的全部数据 |
七、关于轮询写入的问题(你的场景)
你之前的场景提到:轮询写入导致低配节点成为短板。
java
// 轮询写入:绕过分布式表,自己控制去哪个节点
String[] nodes = {"node01", "node02", "node03", "node04"};
for (int i = 0; i < dataList.size(); i++) {
String targetNode = nodes[i % nodes.length];
writeToNode(targetNode, dataList.get(i));
}
问题根源 :这种方式绕过了分片键,数据分布由应用代码决定,而不是 ClickHouse 的分片规则。
| 写入方式 | 分片键是否生效 | 数据分布依据 | 是否推荐 |
|---|---|---|---|
| 通过分布式表写入 | ✅ 生效 | 分片键计算 | ✅ 推荐 |
| 轮询直连节点写入 | ❌ 绕过 | 应用代码随机/轮询 | ❌ 不推荐 |
解决方案 :改为通过分布式表写入,让 user_id 分片键自动路由。
java
// 正确写法:通过分布式表写入
// 连接任意一个节点,写入分布式表即可
String jdbcUrl = "jdbc:clickhouse://任意节点:8123";
try (Connection conn = DriverManager.getConnection(jdbcUrl)) {
PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO user_log_distributed (user_id, event_date, event_type, page_url) VALUES (?, ?, ?, ?)"
);
// 设置参数...
pstmt.executeUpdate();
}
八、常见误区与澄清
| 误区 | 真相 |
|---|---|
| "分区就是分片" | ❌ 分区是本地切文件,分片是跨节点切数据 |
| "分片越多越好" | ❌ 分片过多会导致小文件多、跨节点查询开销大 |
| "分区粒度越细越好" | ❌ 按小时分区会产生大量小分区,元数据膨胀 |
| "有了分片就不需要分区了" | ❌ 分片解决节点分布,分区解决节点内扫描效率,两者互补 |
| "分片键和分区键必须相同" | ❌ 可以不同,但设计时需考虑查询模式 |
| "分片键可以事后修改" | ❌ 不可修改,需重建表并迁移数据 |
| "轮询写入和分片不冲突" | ⚠️ 轮询写入绕过分片键,会导致数据分布不符合预期 |
九、选择建议
| 场景 | 是否需要分片 | 是否需要分区 | 原因 |
|---|---|---|---|
| 数据量 < 2TB | ❌ 不需要 | ✅ 需要 | 单机够用,用分区优化查询即可 |
| 数据量 2TB ~ 10TB | ⚠️ 可选 | ✅ 需要 | 单机可能够,但分区必须 |
| 数据量 > 10TB | ✅ 必须 | ✅ 需要 | 分片扩展容量,分区加速查询 |
| 查询总是带时间范围 | 可选 | ✅ 强烈推荐 | 分区裁剪效果极佳 |
| 查询按用户ID精确分布 | ✅ 推荐 | 可选 | 分片键设计合理可减少跨节点查询 |
| 查询经常跨多天全表聚合 | ✅ 需要 | ⚠️ 帮助有限 | 分片并行计算,分区帮助不大 |
十、总结
| 你关心的问题 | 答案 |
|---|---|
| Sharding 解决什么问题? | 数据量大到单机存不下时,通过分片分散到多节点 |
| Partitioning 解决什么问题? | 减少查询扫描的数据量,快速跳过无关文件块 |
| 两者是什么关系? | 分工不同,可以同时使用。分片决定数据去哪个节点,分区决定节点内查哪个文件 |
| ClickHouse 中如何实现? | 分片用 Distributed 引擎 + 集群配置,分区用 PARTITION BY |
| 分片什么时候设置? | 集群配置时定义节点归属,建表时指定分片键 |
| 分片键能改吗? | 不能,需要重建表并迁移数据 |
| 轮询写入的问题怎么解决? | 改为通过分布式表写入,让分片键自动路由 |
| 生产环境怎么组合? | 高基数列(如 user_id)做分片键,时间列做分区键 |
一句话记忆
分片(Sharding)是把数据"横着切"分到不同机器,解决"一台放不下";分区(Partitioning)是把同一台机器上的数据"竖着切"成小块,解决"一次查太多"。两者可以同时使用,互不冲突。分片在建分布式表时通过
sharding_key设置,一旦定下就不可改。
如需深入了解 ClickHouse 的部署架构选型、分片与副本机制详解、分布式表原理剖析、无中心架构设计哲学、生产环境集群调优、多副本一致性实践、ClickHouse Keeper 核心原理等内容,请持续关注本专栏《ClickHouse 一站式从入门到实战》系列文章。
在 ClickHouse 集群中,"分片"(Sharding)和"分区"(Partitioning)是两个极易混淆的核心概念。很多人误以为分区就是分片,或者认为两者是互斥的------实际上,它们是不同维度、可以协同工作的数据切分策略。本文将系统梳理两者的区别、联系,并通过生产案例展示如何组合使用,帮助你彻底理解并正确应用于实际项目。