4. ClickHouse数据类型与DDL
数据类型和 DDL,是玩转 ClickHouse 的第一道门槛。很多初学者觉得这有什么好讲的,不就是建表吗?
但我见过太多生产事故,都是因为一开始类型选错了。比如用 String 存 IP,用 Float 存金额,或者把 Nullable 当万能药。嗯,这些坑我都踩过。
这一章,咱们就把 ClickHouse 的数据类型和 DDL 讲透。从基础类型到高级结构,再到表引擎怎么选,一条线串下来。
4.1 基础数据类型:选对类型,事半功倍
ClickHouse 的类型系统,说白了就是为列式存储和向量化执行量身定做的。选对了,查询快;选错了,存储翻倍。
4.1.1 整数与浮点
整数家族很庞大:Int8、Int16、Int32、Int64,还有无符号版本 UInt8 到 UInt64。
我个人习惯:能用 UInt32 存 ID 就别用 Int64。少 4 个字节,一亿行就是 400MB 的差距。
UInt32 或 DateTime,别用字符串。我见过有人用 String 存 "2024-01-01 12:00:00",查询慢了三倍不止。
浮点类型有 Float32 和 Float64。注意,浮点有精度问题。存金额?千万别用 Float。
Decimal 类型。我曾经接手过一个项目,用 Float64 存交易金额,月底对账差了 0.01 元,排查了整整两天。
4.1.2 字符串与日期
字符串有 String 和 FixedString(N)。长度固定的场景,比如 MD5 值,用 FixedString(32) 更省空间。
日期类型:Date、DateTime、DateTime64。存到天用 Date,存到秒用 DateTime,需要毫秒用 DateTime64(3)。
| 类型 | 字节 | 精度 | 适用场景 |
|---|---|---|---|
| Date | 2 | 天 | 日志日期、统计日期 |
| DateTime | 4 | 秒 | 事件时间、操作时间 |
| DateTime64(3) | 8 | 毫秒 | 金融交易、传感器数据 |
4.2 Nullable:用对是宝,用错是坑
Nullable 允许字段为空。但 ClickHouse 的 Nullable 实现方式,和 MySQL 完全不同。
它会在原列后面额外生成一个标记数组,标记哪些行是 NULL。这意味着:
- 存储空间翻倍(至少多一个 bit 位)
- 查询性能下降(每次都要检查标记)
- 索引效率降低(无法充分利用稀疏索引)
我有个真实案例:某张 10 亿行的用户表,50% 的字段用了 Nullable。后来改成默认值,存储从 200GB 降到 120GB,查询快了 40%。
什么时候必须用 Nullable?只有一种情况:业务逻辑明确区分「空值」和「默认值」。比如用户未设置头像 vs 用户设置了默认头像。
4.3 LowCardinality:低基数字段的加速器
LowCardinality 是 ClickHouse 19.x 引入的优化类型。它把字符串映射成整数 ID,存储和比较都按整数来。
说白了,就是内置的字典编码。
适用条件:
- 基数小于 10,000 的字段
- 比如省份、城市、状态码、枚举值
- 查询中大量用于 GROUP BY 或 WHERE
-- 建表示例
CREATE TABLE events (
event_time DateTime,
city LowCardinality(String),
status LowCardinality(String),
user_id UInt64
) ENGINE = MergeTree
ORDER BY event_time;
注意:基数超过 10,000 时,字典维护成本会超过收益。这时候用普通 String 反而更好。
4.4 Array 与 Tuple:复杂结构的基石
4.4.1 Array:数组类型
Array(T) 可以存任意类型的数组。注意,ClickHouse 的数组是定长的,但元素类型必须一致。
-- 建表
CREATE TABLE user_tags (
user_id UInt64,
tags Array(String),
scores Array(Float32)
) ENGINE = MergeTree
ORDER BY user_id;
-- 插入数据
INSERT INTO user_tags VALUES (1, ['vip', 'new_user'], [95.5, 88.0]);
数组常用函数:arrayJoin、arrayLength、has、arraySort。我最常用的是 arrayJoin,能把数组展开成多行,做标签统计特别方便。
4.4.2 Tuple:元组类型
Tuple 可以存不同类型的数据组合。和 Array 不同,Tuple 的元素类型可以不一样。
-- 建表
CREATE TABLE product_info (
product_id UInt64,
info Tuple(name String, price Float32, stock UInt32)
) ENGINE = MergeTree
ORDER BY product_id;
Tuple 在 ClickHouse 里用得不多,但有个场景很实用:作为函数返回值。比如 now() 返回时间,你可以用 Tuple 包装多个计算结果。
4.5 Nested:模拟嵌套结构
Nested 是 ClickHouse 模拟嵌套数据的方式。它本质上是一组长度相同的数组。
CREATE TABLE orders (
order_id UInt64,
items Nested(
product_id UInt32,
quantity UInt16,
price Float32
)
) ENGINE = MergeTree
ORDER BY order_id;
注意,Nested 不是真正的嵌套结构。它只是把多个数组放在一起,通过下标关联。查询时用 arrayJoin 展开。
Nested 适合「写多读少」的嵌套场景。如果需要频繁更新子项,建议拆成两张表。
4.6 表引擎选择策略:选对引擎,少走弯路
ClickHouse 的表引擎有几十种,但 90% 的场景只用三种:MergeTree、ReplacingMergeTree、SummingMergeTree。
4.6.1 MergeTree:万金油引擎
所有引擎的基座。支持主键、分区、排序、TTL、物化视图。90% 的场景选它没错。
CREATE TABLE analytics (
event_date Date,
user_id UInt64,
event_type String,
value Float32
) ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, user_id)
TTL event_date + INTERVAL 90 DAY;
4.6.2 ReplacingMergeTree:去重引擎
适合需要去重的场景。注意,它是异步去重,不是实时去重。
CREATE TABLE user_profile (
user_id UInt64,
name String,
update_time DateTime
) ENGINE = ReplacingMergeTree(update_time)
ORDER BY user_id;
合并时,保留 update_time 最大的那条记录。我一般在做 CDC 同步时用它,避免重复数据。
4.6.3 SummingMergeTree:预聚合引擎
适合统计场景。合并时自动对数值列求和。
CREATE TABLE daily_stats (
stat_date Date,
page_id UInt32,
pv UInt64,
uv UInt64
) ENGINE = SummingMergeTree()
ORDER BY (stat_date, page_id);
注意,SummingMergeTree 只对 ORDER BY 之外的数值列求和。如果你需要更复杂的聚合,还是用物化视图吧。
4.6.4 引擎选择速查表
| 场景 | 推荐引擎 | 说明 |
|---|---|---|
| 通用日志、事件 | MergeTree | 最稳定,功能最全 |
| 需要去重 | ReplacingMergeTree | 异步去重,适合 CDC |
| 预聚合统计 | SummingMergeTree | 自动求和,减少存储 |
| 实时写入 + 去重 | CollapsingMergeTree | 支持实时折叠 |
| 版本控制 | VersionedCollapsingMergeTree | 按版本折叠 |
4.7 本章知识体系
下面这张图,把数据类型和 DDL 的核心脉络串起来了。你可以把它当思维导图用。
这张图把本章的知识点串成了体系。从基础类型到高级结构,再到引擎选择,每一步都有明确的决策依据。
记住:数据类型选对了,后面 90% 的问题都不会发生。建表前多花 5 分钟思考类型,能省下后面 5 小时的排障时间。