2. 数据类型与表引擎

说实话,ClickHouse 的学习曲线不算陡,但有个坎儿必须过——数据类型和表引擎。我见过不少新手,上来就建表、写 SQL,结果跑起来发现性能不对,或者数据对不上。回头一看,全是类型选错、引擎用错的问题。

这一章,咱们就把地基打牢。你想想看,如果连 Int 和 UInt 都分不清,后面谈什么性能优化?

基础数据类型:选对类型,省一半存储

ClickHouse 的数据类型设计,核心思路就一条:能用小的,别用大的。列式存储下,每个字节都乘以行数,省下来的就是真金白银。

整数类型:Int 与 UInt

我最常用的就是 Int8、Int16、Int32、Int64,以及对应的无符号版本 UInt8、UInt16 等。区别很简单:有符号能表示负数,无符号能多一倍正数范围。

类型 字节 范围 我常用的场景
UInt8 1 0 ~ 255 状态码、性别、小枚举
Int16 2 -32768 ~ 32767 端口号、年龄
UInt32 4 0 ~ 42亿 用户ID、订单ID
Int64 8 -2^63 ~ 2^63-1 时间戳(毫秒级)
我的习惯:能用 UInt32 就别用 Int64。我在一个日志项目里,把用户 ID 从 Int64 改成 UInt32,单表 10 亿行,存储直接降了 40GB。别小看这 4 个字节。

浮点数:Float32 与 Float64

Float32 是单精度,Float64 是双精度。日常统计用 Float32 就够了。但注意——浮点数有精度问题。如果你做金额计算,千万别用 Float,用 Decimal。

我曾经踩过的坑:有个金融客户,用 Float64 存交易金额,跑聚合时发现总和差了 0.01 元。排查半天,是浮点精度丢失。后来全改成 Decimal(18,2),问题解决。记住:钱的事,别用 Float。

字符串:String 与 FixedString

String 是变长,存啥都行。FixedString(N) 是定长,适合长度固定的场景,比如 IP 地址、MD5 值。

我个人建议:除非你确定长度不变,否则优先用 String。FixedString 如果存的数据长度不够,会用 \0 补齐,查询时容易出幺蛾子。

日期与时间:Date、DateTime、DateTime64

这三个类型,我几乎每天都要用:

  • Date:精确到天,2 字节,存 '2024-01-01' 这种。
  • DateTime:精确到秒,4 字节,存 '2024-01-01 12:00:00'。
  • DateTime64:精确到毫秒甚至纳秒,适合高频交易、监控数据。

选型很简单:按需取最小。如果只按天统计,用 Date;需要小时级,用 DateTime;要毫秒精度,上 DateTime64。

Nullable 与默认值:小心 Null 的代价

ClickHouse 的 Nullable 和 MySQL 不一样。它本质上是用一个额外的标记列来存“是否为空”。这意味着:Nullable 列会多占存储,而且查询性能会下降。

核心原则:能用默认值代替 Null,就别用 Nullable。

比如用户年龄,用 0 表示未知,比用 Null 好得多。我在一个用户画像项目里,把 5 个 Nullable 列改成默认值,查询速度提升了 30%。

如果你确实需要 Null,语法很简单:

CREATE TABLE example (
    id UInt32,
    name String,
    age Nullable(UInt8) DEFAULT 0
) ENGINE = MergeTree()
ORDER BY id;

注意:Nullable 不能用在索引列上。这是硬限制,别问为什么,记住就行。

MergeTree 家族:ClickHouse 的灵魂

如果说数据类型是砖,那表引擎就是房子的结构。MergeTree 家族是 ClickHouse 最核心、最强大的引擎,没有之一。

我画了一张图,帮你快速理解它们的关系:

MergeTree 家族引擎关系图 MergeTree ReplacingMergeTree SummingMergeTree AggregatingMergeTree 去重场景 按排序键去重,保留最后一条 适合:用户状态、订单快照 预聚合场景 合并时自动 SUM 数值列 适合:PV/UV、流量统计 高级聚合场景 存储聚合中间状态 适合:物化视图、复杂指标 核心思想:写入时快,合并时算 所有变体都基于 MergeTree,只是合并策略不同

MergeTree:一切的基础

MergeTree 的核心机制就两句话:数据按排序键有序存储,后台定期合并小数据块。写入时只管追加,查询时利用排序键做二分查找,快得离谱。

建表语法:

CREATE TABLE events (
    event_date Date,
    event_time DateTime,
    user_id UInt32,
    event_type String,
    cost Float32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_time);

这里 ORDER BY 就是排序键,也是索引。查询时如果条件匹配排序键前缀,性能最好。

我的建议:排序键选查询最频繁的字段,而且把基数低的放前面。比如先按天、再按用户,比反过来好得多。

ReplacingMergeTree:去重利器

这个引擎在 MergeTree 基础上加了去重逻辑。合并时,如果排序键相同,只保留最后一条(按版本或时间戳)。

注意:去重只在合并时发生,不是实时的。你刚写入的数据,可能还在小分区里,查出来会有重复。

CREATE TABLE user_status (
    user_id UInt32,
    status String,
    update_time DateTime
) ENGINE = ReplacingMergeTree(update_time)
ORDER BY user_id;

括号里的 update_time 是版本列,合并时保留最大的那条。

我曾经踩过的坑:有个实时报表,用了 ReplacingMergeTree,结果刚写入的数据查出来有重复。后来加了 FINAL 关键字才解决——但 FINAL 查询很慢,慎用。更好的做法是:在查询时用 argMax 自己取最后一条。

SummingMergeTree:自动求和

这个引擎更狠——合并时,把排序键相同的行,数值列自动求和。适合做 PV、UV、流量统计这类场景。

CREATE TABLE page_views (
    page String,
    date Date,
    views UInt64,
    duration Float32
) ENGINE = SummingMergeTree()
ORDER BY (date, page);

写入多条 ('home', '2024-01-01', 1, 2.5),合并后自动变成 ('home', '2024-01-01', N, sum)。

但注意:不是实时的。如果你要精确的实时数据,还是得用普通 MergeTree 自己算。

AggregatingMergeTree:高级聚合

这个引擎不直接存数值,而是存聚合函数的中间状态。配合物化视图使用,可以实现秒级聚合查询。

CREATE TABLE agg_metrics (
    date Date,
    metric String,
    value AggregateFunction(uniq, UInt32)
) ENGINE = AggregatingMergeTree()
ORDER BY (date, metric);

写入时要用 uniqState 函数,查询时用 uniqMerge。说实话,这个引擎有点复杂,我一般只在物化视图里用。

一句话总结四种引擎:

  • MergeTree:基础款,啥都能干
  • ReplacingMergeTree:去重,适合存最新状态
  • SummingMergeTree:自动求和,适合统计
  • AggregatingMergeTree:存中间状态,适合高级聚合

嗯,这一章内容不少。数据类型选对了,表引擎用对了,你的 ClickHouse 之路就成功了一半。剩下的,就是多写多练,踩坑多了自然就熟了。