表引擎选择:查询性能的第一道分水岭

说实话,很多刚接触 ClickHouse 的朋友,上来就默认用 MergeTree。我一开始也这样。直到有一次线上查询慢得离谱,排查了半天,才发现问题出在引擎选型上。

表引擎,说白了就是数据的存储和计算策略。选对了,查询快得像本地数组遍历;选错了,再牛的优化技巧也救不了。今天咱们就把 MergeTree 家族这几个主力引擎掰开揉碎,看看它们到底适合什么场景。

核心观点:没有万能的引擎,只有最匹配业务的引擎。选型前,先想清楚你的数据是「只追加」还是「会更新」,查询是「求明细」还是「要聚合」。

MergeTree 家族引擎选型决策树 数据写入模式? 只追加,不更新 有更新或去重需求 查询需求:明细还是聚合? 去重引擎:ReplacingMergeTree 要明细:MergeTree 要聚合:Summing/Aggregating 💡 选型口诀: 明细查询用 MergeTree,去重更新用 Replacing,数值累加用 Summing,复杂聚合用 Aggregating。 如果拿不准,先用 MergeTree 跑起来,后续通过物化视图或 ALTER 转换。

1. MergeTree:最通用的基础引擎

MergeTree 是 ClickHouse 的基石。它支持按主键排序、分区、数据分片和副本复制。我个人的习惯是,只要业务没有特殊要求,先用 MergeTree 兜底。

适用场景:

  • 日志数据、事件流数据,只追加不修改
  • 需要按时间范围做快速过滤
  • 查询以明细数据为主,偶尔做简单聚合

性能特点:

  • 写入吞吐极高,单机每秒可写入数十万行
  • 主键索引能大幅减少数据扫描量
  • 不支持数据去重,同一主键会保留多行

我的经验:有一次做用户行为日志存储,每天几十亿条记录。用 MergeTree 配合 ORDER BY (event_time, user_id),按天分区。查询某用户某天的行为,秒级返回。如果换成 MySQL,同样的数据量得跑几分钟。

2. ReplacingMergeTree:去重利器

这个引擎在 MergeTree 基础上加了去重逻辑。合并分区时,它会保留每个主键的最后一条记录。嗯,这里要注意:去重发生在后台合并时,不是写入时。

适用场景:

  • 需要更新状态的业务表,比如订单状态、用户信息
  • 数据源会重复发送相同主键的记录
  • 能接受短暂的数据重复(合并完成前)

我曾经踩过的坑:有次做实时订单宽表,用 ReplacingMergeTree 存订单状态。业务方要求查询必须精确去重。结果发现刚写入的数据还没合并,查出来有重复行。后来加了 FINAL 修饰符,但性能下降明显。最终方案是:写入时先做一次客户端去重,引擎只做兜底。

性能对比:

特性 MergeTree ReplacingMergeTree
写入速度 极快 略慢(多版本标记)
查询去重 不支持 支持(需合并或 FINAL)
存储空间 最小 略大(保留历史版本)
适用场景 日志、事件 状态表、维度表

3. SummingMergeTree:预聚合的加速器

这个引擎会在合并时,把相同主键的数值列自动求和。你想想看,如果每天有上亿条交易记录,但查询只需要按用户汇总金额,用 SummingMergeTree 能省掉大量实时计算。

适用场景:

  • 需要按维度做累加统计,比如 PV/UV、交易金额
  • 查询模式固定,不需要明细数据
  • 对数据精度要求不极端(合并时可能丢失部分细节)
-- 创建 SummingMergeTree 表
CREATE TABLE daily_sales (
    sale_date Date,
    product_id UInt32,
    amount Decimal(18,2),
    quantity UInt32
) ENGINE = SummingMergeTree()
ORDER BY (sale_date, product_id);

-- 写入多条相同主键的数据
INSERT INTO daily_sales VALUES ('2024-01-01', 1001, 50.00, 2);
INSERT INTO daily_sales VALUES ('2024-01-01', 1001, 30.00, 1);

-- 合并后查询,amount 自动变成 80.00,quantity 变成 3
SELECT * FROM daily_sales FINAL;

性能提升:我在一个广告计费项目中,用 SummingMergeTree 替代了原来的 MergeTree + 实时聚合查询。查询耗时从 3 秒降到了 50 毫秒,存储空间也减少了 60%。代价是丢失了单次点击的明细数据——但业务方只需要日报,完全能接受。

4. AggregatingMergeTree:复杂聚合的终极方案

这个引擎更强大。它允许你自定义聚合函数,比如 uniqState(去重计数)、avgState(平均值)、quantileState(分位数)。合并时,它会保留聚合函数的中间状态,而不是最终结果。

适用场景:

  • 需要精确去重计数(UV)
  • 需要计算分位数、标准差等复杂统计
  • 数据量大,但查询维度固定
-- 创建 AggregatingMergeTree 表
CREATE TABLE page_views_agg (
    page_date Date,
    page_url String,
    uv AggregateFunction(uniq, String),
    pv SimpleAggregateFunction(sum, UInt64)
) ENGINE = AggregatingMergeTree()
ORDER BY (page_date, page_url);

-- 写入时使用 -State 函数
INSERT INTO page_views_agg
SELECT
    today(),
    '/index.html',
    uniqState(user_id),
    sumState(1)
FROM page_views_raw
WHERE page_date = today();

我的建议:AggregatingMergeTree 适合做多层聚合的底层存储。比如先按小时聚合,再按天聚合。但要注意,查询时必须用对应的 -Merge 函数,否则拿不到结果。这个学习曲线稍微陡一点,但性能回报非常可观。

5. 引擎选型决策表

业务需求 推荐引擎 理由
存储原始日志,需要明细查询 MergeTree 写入快,查询灵活
订单表,需要按订单号去重 ReplacingMergeTree 支持主键去重
按天统计销售额,不需要明细 SummingMergeTree 自动预聚合,查询极快
需要精确 UV 和分位数 AggregatingMergeTree 支持复杂聚合函数
既有明细需求,又有聚合需求 MergeTree + 物化视图 兼顾灵活性和性能

6. 性能差异的底层原因

为什么这些引擎性能差异这么大?说白了,就是「计算时机」不同。

  • MergeTree:查询时计算,最灵活但最慢
  • SummingMergeTree:合并时预计算,查询时直接读结果
  • AggregatingMergeTree:合并时保留中间状态,查询时做最终聚合
  • ReplacingMergeTree:合并时去重,但查询时可能还需要 FINAL

我习惯把 MergeTree 比作「生鲜超市」——什么都有,但每次都要现做。SummingMergeTree 就像「预制菜」——提前做好,热一下就能吃。AggregatingMergeTree 则是「半成品」——需要最后一步加工,但比从头做快得多。

重要提醒:不要为了追求性能盲目使用预聚合引擎。如果业务查询模式经常变化,预聚合的字段可能用不上,反而浪费存储和合并开销。我见过一个项目,用了 SummingMergeTree 但查询条件经常变,结果预聚合的字段根本用不上,查询还是全表扫描。

选引擎没有银弹。我的做法是:先用 MergeTree 跑原型,观察查询模式。等业务稳定了,再根据热点查询创建物化视图或迁移到专用引擎。这样既保证了灵活性,又能在需要时获得极致性能。