强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

ClickHouse 教程 / 表引擎:MergeTree 家族

表引擎:MergeTree 家族

MergeTree 是 ClickHouse 最核心的表引擎家族,几乎所有生产场景都使用 MergeTree 或其变体。


1. MergeTree 引擎基础

1.1 基本语法

CREATE TABLE my_table
(
    event_date Date,
    user_id UInt64,
    event_type LowCardinality(String),
    url String,
    duration UInt32,
    extra Map(String, String)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)       -- 分区键
ORDER BY (event_date, user_id, event_type) -- 排序键
PRIMARY KEY (event_date, user_id)       -- 主键(可选,默认等于 ORDER BY)
SAMPLE BY user_id                       -- 采样键(可选)
TTL event_date + INTERVAL 12 MONTH      -- 数据过期时间(可选)
SETTINGS index_granularity = 8192;      -- 索引粒度

1.2 关键子句说明

子句作用必填
ENGINE = MergeTree()指定引擎
PARTITION BY数据分区表达式可选(强烈推荐)
ORDER BY数据排序键
PRIMARY KEY主键索引可选(默认 = ORDER BY)
SAMPLE BY采样表达式可选
TTL数据生存时间可选
SETTINGS引擎参数可选

💡 提示ORDER BYPRIMARY KEY 可以不同。PRIMARY KEY 必须是 ORDER BY 的前缀。PRIMARY KEY 决定索引大小,ORDER BY 决定数据在磁盘上的物理排序。


2. 数据部分(Parts)与合并机制

2.1 数据写入流程

INSERT INTO my_table VALUES (...)
        │
        ▼
  写入内存 buffer(不对外可见)
        │
        ▼ (后台线程,约 10s 或 buffer 达到阈值)
  flush 到磁盘 → 生成 data part(标记为 active)
        │
        ▼ (后台调度器)
  merge: 将多个小 part 合并为更大的 part
  - 排序合并
  - 去重(ReplacingMergeTree)
  - TTL 清理
  - 更新统计信息

2.2 查看数据部分

-- 查看表的所有活跃数据部分
SELECT
    partition,
    name           AS part_name,
    rows,
    formatReadableSize(bytes_on_disk) AS disk_size,
    formatReadableSize(data_compressed_bytes) AS compressed,
    formatReadableSize(data_uncompressed_bytes) AS uncompressed,
    round(data_uncompressed_bytes / data_compressed_bytes, 2) AS compress_ratio,
    modification_time
FROM system.parts
WHERE database = currentDatabase()
  AND table = 'my_table'
  AND active = 1
ORDER BY partition, modification_time;

-- 查看部分合并操作
SELECT
    event_time,
    event_type,
    table,
    partition_id,
    name AS part_name,
    rows,
    formatReadableSize(size_in_bytes) AS size
FROM system.part_log
WHERE database = currentDatabase()
  AND table = 'my_table'
ORDER BY event_time DESC
LIMIT 20;

2.3 合并过程详解

属性说明
触发条件后台调度器根据 part 数量和大小自动触发
合并类型增量合并(incremental)和全量合并(all)
I/O 特征顺序读取 → 排序 → 顺序写入,对 SSD 友好
临时空间合并时需要约 2 倍 part 大小的临时磁盘空间
merge 线程数background_pool_size 控制

⚠️ 注意:频繁的小批量写入会产生大量小 part,增加合并压力。建议批量写入(至少 1000 行以上)。


3. 分区键设计

3.1 常见分区策略

-- ✅ 按月分区(最常用,适合日志和事件数据)
PARTITION BY toYYYYMM(event_date)

-- ✅ 按天分区(数据量极大时)
PARTITION BY event_date

-- ✅ 按类型 + 时间混合分区
PARTITION BY (event_type, toYYYYMM(event_date))

-- ⚠️ 不推荐:分区过多(如按小时),会导致大量 part 文件
PARTITION BY toStartOfHour(event_time)

-- ⚠️ 不推荐:无分区,所有数据在同一个分区
-- (不设置 PARTITION BY)

3.2 分区裁剪(Partition Pruning)

-- 查询时指定分区条件,ClickHouse 自动裁剪不相关分区
-- 只扫描 2026 年 5 月的分区
SELECT count()
FROM my_table
WHERE event_date >= '2026-05-01' AND event_date < '2026-06-01';
-- 执行计划中可以看到 "Selected 1/12 partitions"

-- 使用分区键过滤
SELECT *
FROM system.parts
WHERE table = 'my_table' AND active = 1;

3.3 分区操作

-- 分离分区(数据从表中移除,但文件仍在磁盘上)
ALTER TABLE my_table DETACH PARTITION '202601';

-- 重新附加分区
ALTER TABLE my_table ATTACH PARTITION '202601';

-- 删除分区(不可恢复!)
ALTER TABLE my_table DROP PARTITION '202601';

-- 从另一个表复制分区
ALTER TABLE my_table ATTACH PARTITION '202601' FROM another_table;

-- 卸载分区到指定目录
ALTER TABLE my_table FREEZE PARTITION '202601';

-- 查看分区列表
SELECT DISTINCT partition
FROM system.parts
WHERE database = currentDatabase()
  AND table = 'my_table'
  AND active = 1
ORDER BY partition;

⚠️ 注意DROP PARTITION 会永久删除该分区所有数据!请先确认备份。


4. 排序键与主键关系

4.1 排序键设计原则

-- 排序键列的选择顺序至关重要:
-- 1. 第一列:最常用的等值/范围查询过滤条件
-- 2. 第二列:次常用的过滤条件或 GROUP BY 条件
-- 3. 后续列:按查询模式逐步添加

-- ❌ 错误示例:低基数列放在最前面
CREATE TABLE logs_bad (
    event_date Date,
    status String,    -- 只有几个值,放在第一位过滤效果差
    user_id UInt64,
    url String
) ENGINE = MergeTree()
ORDER BY (status, event_date, user_id);

-- ✅ 正确示例:高选择性列在前
CREATE TABLE logs_good (
    event_date Date,
    status String,
    user_id UInt64,
    url String
) ENGINE = MergeTree()
ORDER BY (event_date, user_id, status);

4.2 排序键对查询的影响

-- 针对 ORDER BY (event_date, user_id, status) 设计的表

-- ✅ 高效查询:匹配排序键前缀
SELECT * FROM logs_good WHERE event_date = '2026-05-11';
SELECT * FROM logs_good WHERE event_date = '2026-05-11' AND user_id = 123;
SELECT * FROM logs_good WHERE event_date = '2026-05-11' AND user_id = 123 AND status = 'error';

-- ⚠️ 一般查询:跳过前缀列
SELECT * FROM logs_good WHERE user_id = 123;
SELECT * FROM logs_good WHERE status = 'error';

-- 💡 可以使用二级索引(跳数索引)优化跳过前缀的查询

4.3 主键与排序键分离

-- 场景:按 (date, user_id, event_id) 排序,但主键只需 (date, user_id)
-- 主键索引更小,内存占用更少
CREATE TABLE events
(
    event_date Date,
    user_id UInt64,
    event_id UInt64,
    payload String
)
ENGINE = MergeTree()
ORDER BY (event_date, user_id, event_id)
PRIMARY KEY (event_date, user_id);
-- 主键索引只需存储 (date, user_id) 的稀疏索引
-- 查询时仍可利用完整的排序键进行过滤

5. TTL 生存时间

-- 列级 TTL:30 天后将 detail 列置为默认值
CREATE TABLE logs_with_ttl
(
    event_date Date,
    user_id UInt64,
    summary String,
    detail String TTL event_date + INTERVAL 30 DAY,
    tags Array(String) TTL event_date + INTERVAL 30 DAY
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id)
TTL event_date + INTERVAL 12 MONTH DELETE;  -- 行级 TTL:12 个月后删除

-- 动态修改 TTL
ALTER TABLE logs_with_ttl MODIFY TTL event_date + INTERVAL 6 MONTH;

-- TTL 表达式可以很灵活
CREATE TABLE tiered_storage
(
    event_date Date,
    data String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY event_date
TTL
    event_date + INTERVAL 30 DAY TO VOLUME 'hot',    -- 30 天后移到热存储
    event_date + INTERVAL 90 DAY TO VOLUME 'cold',   -- 90 天后移到冷存储
    event_date + INTERVAL 365 DAY DELETE;             -- 365 天后删除

-- 查看 TTL 信息
SELECT
    database,
    table,
    partition,
    name,
    rows,
    modification_time,
    ttl_info
FROM system.parts
WHERE table = 'logs_with_ttl' AND active = 1;

6. 存储策略(多磁盘/卷)

<!-- config.xml 中定义存储策略 -->
<storage_configuration>
    <disks>
        <ssd>
            <path>/data/ssd/clickhouse/</path>
        </ssd>
        <hdd>
            <path>/data/hdd/clickhouse/</path>
        </hdd>
        <s3>
            <type>s3</type>
            <endpoint>https://my-bucket.s3.amazonaws.com/data/</endpoint>
            <access_key_id>YOUR_KEY</access_key_id>
            <secret_access_key>YOUR_SECRET</secret_access_key>
        </s3>
    </disks>
    <policies>
        <tiered>
            <volumes>
                <hot>
                    <disk>ssd</disk>
                    <max_data_part_size_bytes>1073741824</max_data_part_size_bytes>
                </hot>
                <cold>
                    <disk>hdd</disk>
                </cold>
                <archive>
                    <disk>s3</disk>
                </archive>
            </volumes>
        </tiered>
    </policies>
</storage_configuration>
-- 创建表时指定存储策略
CREATE TABLE tiered_table
(
    event_date Date,
    data String
)
ENGINE = MergeTree()
ORDER BY event_date
SETTINGS storage_policy = 'tiered';

💡 提示:SSD 存放最近的热数据,HDD 存放较旧的温数据,S3 存放归档冷数据。配合 TTL 可以实现自动化数据生命周期管理。


7. SETTINGS 常用参数

参数默认值说明
index_granularity8192索引粒度(多少行一个索引标记)
index_granularity_bytes10485760按字节自适应粒度
min_bytes_for_wide_part10485760宽格式 part 最小大小
min_rows_for_wide_part0宽格式 part 最小行数
max_parts_to_merge_at_once100单次合并最大 part 数
max_part_loading_threads0加载 part 线程数
merge_with_ttl_timeout86400TTL 合并最小间隔(秒)
use_minimalistic_part_header_in_zookeeper1使用轻量级 ZooKeeper 节点
allow_nullable_key0是否允许 Nullable 排序键
-- 创建表时设置参数
CREATE TABLE optimized_table
(
    event_date Date,
    user_id UInt64,
    data String
)
ENGINE = MergeTree()
ORDER BY (event_date, user_id)
SETTINGS
    index_granularity = 4096,              -- 更细的索引粒度
    min_bytes_for_wide_part = 0,           -- 始终使用宽格式
    max_parts_to_merge_at_once = 50;       -- 减少单次合并的 part 数

8. MergeTree 变体

8.1 ReplacingMergeTree — 去重

-- 自动去重:相同排序键的行只保留最新版本
CREATE TABLE users_latest
(
    user_id UInt64,
    name String,
    email String,
    updated_at DateTime
)
ENGINE = ReplacingMergeTree(updated_at)  -- 按 updated_at 判断最新版本
ORDER BY user_id;

INSERT INTO users_latest VALUES
    (1, 'Alice', '[email protected]', '2026-01-01 10:00:00'),
    (2, 'Bob',   '[email protected]', '2026-01-01 10:00:00');

-- 插入同一 user_id 的新版本
INSERT INTO users_latest VALUES
    (1, 'Alice', '[email protected]', '2026-05-11 10:00:00');

-- ⚠️ 去重发生在 merge 期间,查询时可能返回重复数据
-- 使用 FINAL 强制实时去重(有性能开销)
SELECT * FROM users_latest FINAL;

-- 或者使用 GROUP BY 取最新
SELECT
    user_id,
    argMax(name, updated_at) AS name,
    argMax(email, updated_at) AS email,
    max(updated_at) AS updated_at
FROM users_latest
GROUP BY user_id;

⚠️ 注意ReplacingMergeTree 的去重是最终一致性,不是实时去重。在 merge 完成前,重复数据仍然存在。

8.2 SummingMergeTree — 预聚合求和

-- 自动聚合数值列
CREATE TABLE page_views
(
    event_date Date,
    url String,
    views UInt64,
    unique_users UInt64
)
ENGINE = SummingMergeTree((views, unique_users))  -- 对这些列自动求和
ORDER BY (event_date, url);

INSERT INTO page_views VALUES
    ('2026-05-11', '/home', 100, 50),
    ('2026-05-11', '/home', 200, 80),
    ('2026-05-11', '/about', 50, 30);

-- ⚠️ merge 后数据会被聚合,但 merge 前查询需要手动聚合
SELECT
    event_date,
    url,
    sum(views) AS total_views,
    sum(unique_users) AS total_unique
FROM page_views
GROUP BY event_date, url
ORDER BY total_views DESC;

8.3 AggregatingMergeTree — 预聚合函数

-- 配合物化视图实现复杂预聚合
CREATE TABLE mv_user_stats
(
    event_date Date,
    user_id UInt64,
    event_count AggregateFunction(count, UInt64),
    sum_amount AggregateFunction(sum, Decimal128(2)),
    uniq_urls AggregateFunction(uniq, String)
)
ENGINE = AggregatingMergeTree()
ORDER BY (event_date, user_id);

-- 创建物化视图自动填充
CREATE MATERIALIZED VIEW mv_user_stats_view
TO mv_user_stats
AS
SELECT
    event_date,
    user_id,
    countState()      AS event_count,
    sumState(amount)   AS sum_amount,
    uniqState(url)     AS uniq_urls
FROM raw_events
GROUP BY event_date, user_id;

-- 查询时需要使用合并函数
SELECT
    event_date,
    user_id,
    countMerge(event_count)    AS events,
    sumMerge(sum_amount)       AS total_amount,
    uniqMerge(uniq_urls)       AS unique_urls
FROM mv_user_stats
GROUP BY event_date, user_id;

8.4 CollapsingMergeTree — 逻辑删除

-- 使用 Sign 列标记行的正负状态
CREATE TABLE user_balance
(
    user_id UInt64,
    balance Decimal128(2),
    sign Int8   -- 1 = 有效行, -1 = 抵消行
)
ENGINE = CollapsingMergeTree(sign)
ORDER BY user_id;

INSERT INTO user_balance VALUES
    (1, 100.00, 1),   -- 初始余额 100
    (1, 100.00, -1),  -- 抵消旧数据
    (1, 150.00, 1);   -- 新余额 150

-- 查询需要使用 FINAL 或手动折叠
SELECT
    user_id,
    sum(balance * sign) AS current_balance
FROM user_balance
GROUP BY user_id
HAVING sum(sign) > 0;

8.5 VersionedCollapsingMergeTree — 带版本的折叠

-- 在 CollapsingMergeTree 基础上增加版本号
-- 解决了并发写入导致的折叠顺序问题
CREATE TABLE user_profile
(
    user_id UInt66,
    name String,
    score UInt32,
    sign Int8,
    version UInt32
)
ENGINE = VersionedCollapsingMergeTree(sign, version)
ORDER BY user_id;

INSERT INTO user_profile VALUES
    (1, 'Alice', 100, 1, 1),    -- 版本 1
    (1, 'Alice', 100, -1, 1),   -- 抵消版本 1
    (1, 'Alice', 150, 1, 2);    -- 版本 2

9. 引擎选择指南

场景推荐引擎原因
通用分析表MergeTree最基础,功能最全
需要去重的维度表ReplacingMergeTree自动去重,适合 UPSERT 场景
预聚合统计表SummingMergeTree / AggregatingMergeTree减少查询时计算量
可更新的状态表VersionedCollapsingMergeTree支持行级别的更新语义
时序数据MergeTree + TTL自动过期旧数据
日志存储MergeTree + 按天分区便于管理和清理
-- 快速创建表参考模板

-- 1. 通用事件表
CREATE TABLE events (
    event_date Date,
    event_time DateTime,
    user_id UInt64,
    event_type LowCardinality(String),
    properties Map(String, String)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id, event_type);

-- 2. 维度表(去重)
CREATE TABLE dim_users (
    user_id UInt64,
    name String,
    email String,
    updated_at DateTime
) ENGINE = ReplacingMergeTree(updated_at)
ORDER BY user_id;

-- 3. 统计表(预聚合)
CREATE TABLE stats_daily (
    stat_date Date,
    metric_name LowCardinality(String),
    dimensions Map(String, String),
    value AggregateFunction(sum, Float64)
) ENGINE = AggregatingMergeTree()
ORDER BY (stat_date, metric_name);

扩展阅读

  1. MergeTree 引擎文档
  2. ReplacingMergeTree
  3. CollapsingMergeTree
  4. AggregatingMergeTree
  5. 数据存储策略