文章目录
- 一、开篇:一个颠覆认知的概念
- [二、ClickHouse 主键的本质:排序键 + 稀疏索引](#二、ClickHouse 主键的本质:排序键 + 稀疏索引)
-
- [2.1 主键 = 排序键](#2.1 主键 = 排序键)
- [2.2 主键 = 稀疏索引的依据](#2.2 主键 = 稀疏索引的依据)
- [2.3 主键与 ORDER BY 的关系](#2.3 主键与 ORDER BY 的关系)
- 三、主键的作用
- 四、主键索引的工作方式
-
- [4.1 查询执行流程](#4.1 查询执行流程)
- [4.2 为什么不能精确定位到行?](#4.2 为什么不能精确定位到行?)
- 五、主键设计的最佳实践
-
- [5.1 把高频过滤列放在最前面](#5.1 把高频过滤列放在最前面)
- [5.2 主键不宜过多列](#5.2 主键不宜过多列)
- [5.3 高基数 vs 低基数列的顺序](#5.3 高基数 vs 低基数列的顺序)
- [5.4 主键与分区键的配合](#5.4 主键与分区键的配合)
- 六、常见误区与澄清
- 七、与稀疏索引的关系
- 八、总结
在 MySQL 中,主键是每一行的"身份证",必须唯一且不能为空。但到了 ClickHouse,你发现主键可以重复,甚至可以是 NULL------这完全颠覆了传统认知。那么,ClickHouse 的主键到底是什么?它的作用是什么?本文将彻底讲清 ClickHouse 主键索引的本质、用法和最佳实践。
一、开篇:一个颠覆认知的概念
如果你从 MySQL 转过来,第一次看到 ClickHouse 的主键定义,大概率会困惑:
sql
-- MySQL 的主键:必须唯一,不能为空
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(100)
);
-- ClickHouse 的主键:可以重复,可以为 NULL
CREATE TABLE user_log (
event_time DateTime,
user_id UInt64,
event_type String
) ENGINE = MergeTree()
PRIMARY KEY (event_time, user_id); -- 允许重复值
核心区别:
| 对比项 | MySQL 主键 | ClickHouse 主键 |
|---|---|---|
| 唯一性 | ✅ 强制唯一 | ❌ 允许重复 |
| 作用 | 唯一标识一行 | 决定磁盘排序顺序 + 稀疏索引依据 |
| 能否为 NULL | ❌ 不能 | ✅ 能 |
| 索引类型 | 密集索引(B-Tree) | 稀疏索引 |
| 与 ORDER BY 关系 | 独立 | 紧密关联(可等价) |
二、ClickHouse 主键的本质:排序键 + 稀疏索引
在 ClickHouse 的 MergeTree 引擎中,PRIMARY KEY 和 ORDER BY 关系非常紧密,甚至经常被混用。
2.1 主键 = 排序键
数据在磁盘上按主键列的顺序物理存储。这是 ClickHouse 主键最核心的作用。
sql
-- 数据会先按 event_time 排序,再按 user_id 排序
PRIMARY KEY (event_time, user_id)
效果:
event_time相近的行挨在一起- 同一时间点的
user_id也有序
2.2 主键 = 稀疏索引的依据
ClickHouse 的主键索引是稀疏索引:每隔 8192 行,记录这一行的主键值到索引文件。
┌─────────────────────────────────────────────────────────────┐
│ Granule 0(行1~8192):event_time = 2025-01-01 10:00:00 │
│ Granule 1(行8193~16384):event_time = 2025-01-01 10:01:00│
│ Granule 2(行16385~24576):event_time = 2025-01-01 10:02:00│
└─────────────────────────────────────────────────────────────┘
注意:稀疏索引不记录每一行,只记录每个 granule 的起始行的主键值。
2.3 主键与 ORDER BY 的关系
| 写法 | 效果 | 说明 |
|---|---|---|
只写 ORDER BY |
ORDER BY 即主键 |
ClickHouse 会将其同时作为主键 |
同时写 ORDER BY 和 PRIMARY KEY |
PRIMARY KEY 必须是 ORDER BY 的前缀 |
元数据可冗余,但排序仍按 ORDER BY |
只写 PRIMARY KEY |
ORDER BY 默认等于 PRIMARY KEY |
主键即排序键 |
sql
-- 以下三种写法等价
CREATE TABLE t1 (id UInt64, name String) ENGINE = MergeTree() ORDER BY id;
CREATE TABLE t2 (id UInt64, name String) ENGINE = MergeTree() PRIMARY KEY id;
CREATE TABLE t3 (id UInt64, name String) ENGINE = MergeTree() ORDER BY id PRIMARY KEY id;
三、主键的作用
| 作用 | 说明 | 示例 |
|---|---|---|
| 数据排序 | 相同主键前缀的行物理上相邻 | 提高压缩率,加速范围查询 |
| 稀疏索引 | 快速跳过不相关的 granule | WHERE event_time > '2025-01-01' 快速定位起始 granule |
| 数据去重 | 配合 ReplacingMergeTree 使用(需 ORDER BY) |
去重键必须是 ORDER BY 的前缀 |
| 分区内排序 | 每个分区内独立排序 | 减少跨分区数据混叠 |
注意 :ClickHouse 主键不保证唯一性 ,也不会阻止插入重复值。如果需要去重,需使用 ReplacingMergeTree 表引擎。
四、主键索引的工作方式
4.1 查询执行流程
以 WHERE event_time = '2025-01-01 10:01:30' 为例:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 二分查找索引 | 在索引中找到 event_time >= 目标值 的第一个 granule --- Granule 1 |
| 2 | 定位到 granule | 根据索引条目,定位到 Granule 1 在磁盘上的位置 |
| 3 | 块内扫描 | 在 Granule 1(8192行)内逐行扫描,找到匹配的行 |
结论:主键索引帮你快速跳过不相关的 granule,但 granule 内仍需线性扫描。
4.2 为什么不能精确定位到行?
因为 ClickHouse 是列式存储 + 压缩。如果索引精确定位到行,需要解压并读取该行所有列,反而更慢。块内扫描可以批量解压、向量化执行,效率更高。
五、主键设计的最佳实践
5.1 把高频过滤列放在最前面
sql
-- ✅ 好的设计:查询总是带 event_date
PRIMARY KEY (event_date, user_id)
-- ❌ 差的设计:user_id 在第一位,但查询很少用它过滤
PRIMARY KEY (user_id, event_date)
5.2 主键不宜过多列
sql
-- ❌ 过多列
PRIMARY KEY (col1, col2, col3, col4, col5)
-- ✅ 1-3 列最佳
PRIMARY KEY (event_date, user_id)
原因:主键列越多,排序开销越大,索引粒度越粗,块内扫描范围越大。
5.3 高基数 vs 低基数列的顺序
| 查询模式 | 推荐顺序 | 原因 |
|---|---|---|
等值查询为主(WHERE user_id = 123) |
高基数在前 | 快速缩小范围 |
范围查询为主(WHERE event_date > '2025-01-01') |
低基数在前 | 范围字段放前面,利用有序性 |
5.4 主键与分区键的配合
sql
-- 按天分区,按时间+用户排序
PARTITION BY toYYYYMMDD(event_date)
PRIMARY KEY (event_date, user_id)
效果:
- 分区裁剪 → 只扫描相关分区
- 主键索引 → 快速定位分区内的 granule
六、常见误区与澄清
| 误区 | 真相 |
|---|---|
| "主键必须唯一" | ❌ ClickHouse 主键允许重复,甚至全是 NULL |
| "主键是行级标识" | ❌ ClickHouse 没有行级唯一标识的概念 |
| "有了主键,点查就快" | ⚠️ 主键帮助跳过 granule,但 granule 内仍需扫描 8192 行 |
| "主键越多越好" | ❌ 主键列过多会导致排序开销大,索引粒度变粗 |
| "PRIMARY KEY 和 ORDER BY 可以完全不同" | ⚠️ 可以不同,但 PRIMARY KEY 必须是 ORDER BY 的前缀 |
七、与稀疏索引的关系
| 概念 | 关系 | 说明 |
|---|---|---|
| 主键 | 决定了按什么列排序 | 是数据物理顺序的定义 |
| 稀疏索引 | 基于主键建立的索引 | 每隔 8192 行记录主键值到索引文件 |
| 主键索引 | 两者合称 | 主键 + 稀疏索引共同构成 |
一句话:主键定义规则,稀疏索引基于规则建立路标。
八、总结
| 问题 | 答案 |
|---|---|
| ClickHouse 主键是什么? | 排序键 + 稀疏索引依据,决定数据的物理顺序 |
| 与 MySQL 主键的区别? | 不保证唯一、可为 NULL、不是行级标识 |
| 主键的作用? | 排序、压缩、稀疏索引、数据去重(配合特定引擎) |
| 主键能加速点查吗? | 有限,只能跳过 granule,块内仍需扫描 |
| 主键设计原则? | 高频过滤列在前,1-3 列最佳 |
| PRIMARY KEY 和 ORDER BY 的关系? | 紧密相关,PRIMARY KEY 必须是 ORDER BY 的前缀 |
一句话记住
ClickHouse 的主键不是"唯一身份证",而是"排序规则 + 稀疏索引依据"。它决定了数据在磁盘上的物理顺序,帮助快速跳过不相关的数据块,但不保证唯一性,也不精确定位到行。
如需深入了解 ClickHouse 的稀疏索引原理、ORDER BY 设计、分区策略等内容,请持续关注本专栏《ClickHouse 一站式从入门到实战》系列文章。
在 MySQL 中,主键是每一行的"身份证",必须唯一且不能为空。但到了 ClickHouse,你发现主键可以重复,甚至可以是 NULL------这完全颠覆了传统认知。那么,ClickHouse 的主键到底是什么?它的作用是什么?本文将彻底讲清 ClickHouse 主键索引的本质、用法和最佳实践。