码到成功
ClickHouse——Data Types
在 ClickHouse 里,类型系统不是附属品,它几乎直接影响:
- 存储体积
- 查询速度
- 压缩效果
- 索引与跳数效果
- Python 侧写入和读取时的心智负担
官方文档把类型系统拆得非常细,从整数、浮点、时间、字符串,到 Map、JSON、Dynamic、LowCardinality、Nullable,每一类都有自己的脾气。Data Types in ClickHouse
这篇就围绕 data-types 这条线来收一遍,不追求把所有类型背成目录,而是尽量讲清楚:常见类型怎么选、哪些包装类型特别值钱、哪些地方最容易踩坑。
先别急着建表,先知道 ClickHouse 到底有哪些类型
官方文档的类型页列出了当前支持的一大串类型,除了常见的:
Int / UIntFloat32 / Float64 / BFloat16DecimalString / FixedStringDate / Date32 / Time / Time64 / DateTime / DateTime64BooleanArray / Tuple / Map / Nested
还有一批更偏分析场景的类型:
UUIDIPv4 / IPv6EnumVariantLowCardinalityNullableJSONDynamic
官方文档还特别提到一个很实用的系统表:system.data_type_families。它能帮助你查看当前有哪些类型、哪些名字其实是别名、以及名称是否区分大小写。Data Types in ClickHouse
这张表很适合在你不确定某个类型名到底怎么写的时候先查一眼:
SELECT *
FROM system.data_type_families
ORDER BY name;
这个动作看起来不起眼,但在你开始接触 Bool、UUID、DateTime64、LowCardinality 这些类型时,会非常省脑子。
数值类型:先把“能装下”和“算得准”分开想
Int / UInt:最基础,也最常用
这类类型最容易理解,也最容易被滥用。
例如:
- 设备计数、用户 ID、状态码,常见用
UInt32 / UInt64 - 会出现负值的指标,比如增量差值、偏移量,才更适合
Int32 / Int64
一个很实用的习惯是:别默认上 Int64,先看业务值域到底多大。
在列式数据库里,类型越贴近真实值域,通常越容易拿到更好的压缩和存储效率。
Float:算得快,但别拿来装钱
Float32 / Float64 适合:
- 指标计算
- 比例、分数
- 带小数的近似值
但如果你在乎精确小数,比如:
- 金额
- 结算单价
- 折扣值
那就别偷懒用 Float64,直接考虑 Decimal。
Decimal:给精确小数一个正式工位
Decimal 的存在,就是为了避免浮点在精度上的那些小脾气。
一个常见例子:
CREATE TABLE order_amount
(
order_id UInt64,
amount Decimal(18, 2)
)
ENGINE = MergeTree
ORDER BY order_id;
在分析型数据库里,钱这类字段如果一开始就用错,后面补救成本很高。这里不需要赌运气,直接用 Decimal。
Boolean:有,但别把所有状态都塞成 Bool
官方文档里也列出了 Boolean。Data Types in ClickHouse
但一个工程上的小建议是:如果状态天然就不止两类,比如:
pending / running / success / faileddraft / published / archived
那就不要硬拆成多个布尔列,直接上枚举或字符串会更自然。
字符串和枚举:别一股脑全都 String
String:最通用,但不是最省
String 非常灵活,所以很多人建表时会本能地往它身上靠。
问题在于,它过于宽泛了。
如果你的列有明显的模式,比如:
- 状态值很少
- 国家码重复很多
- 渠道名固定集合
那 String 虽然能装,但通常不是最优解。
FixedString(N):定长字段才值得用
FixedString(N) 不是拿来代替所有 String 的。它更适合那些长度天然固定的字段,比如固定编码或二进制片段。
如果字符串长度变化很大,硬上 FixedString 反而会别扭。
Enum:适合小而稳定的离散集合
如果一列的取值非常少,而且变化频率低,Enum 是个很利落的选择。
比如:
status Enum8(
'new' = 1,
'paid' = 2,
'done' = 3,
'cancelled' = 4
)
但它有一个现实问题:后续要改枚举成员时,没有 String 或 LowCardinality(String) 那么松快。
LowCardinality:ClickHouse 里特别值得认识的包装类型
官方文档对 LowCardinality(T) 的描述很直接:它把其他类型的内部表示改成字典编码(dictionary-encoded)。LowCardinality(T)
这东西特别适合:
- 城市名
- 国家码
- 渠道名
- 类别标签
- 重复度很高的字符串列
官方文档还给了一个很有参考价值的经验区间:
- 如果字典里的不同值少于
10,000,通常收益更明显 - 如果不同值超过
100,000,可能反而不如普通类型
LowCardinality(T)
这条经验很实用,别背成教条,但值得记在脑子里。
另外,官方文档还建议:处理字符串时,可以优先考虑 LowCardinality 而不是 Enum,因为它更灵活,而且经常有相近甚至更好的效率。LowCardinality(T)
这句话在工程里非常有分量。
时间类型:别让时间精度和时区混成一团
ClickHouse 的时间类型并不少:
DateDate32TimeTime64DateTimeDateTime64
实际建表时,可以先按这个思路判断:
Date / Date32:只关心日期,不关心具体时刻
如果你的分析只是按天聚合,比如订单日、注册日、账单日,优先考虑 Date 或 Date32。
你不需要“时分秒”的时候,就别多背一个更重的时间类型。
DateTime:到秒就够
很多埋点、日志、订单创建时间,用 DateTime 就足够了。
DateTime64:需要更高精度时再上
如果你要记录:
- 毫秒级日志
- 链路事件
- 更细粒度的操作时间
那 DateTime64 会更自然。
一个很常见的做法是:
event_time DateTime64(3, 'Asia/Shanghai')
它表达得就非常清楚:毫秒精度,按指定时区解释。
这里最怕的不是类型太少,而是“明明只按天分析,却全表都上了高精度时间列”,最后存了很多自己根本不会用到的信息。
UUID、IP、网络类字段:别把它们都塞成字符串
官方文档列出了 UUID、IPv4、IPv6 这些专门类型。Data Types in ClickHouse
如果你本来就知道一列语义明确,比如它天然就是 UUID 或 IP 地址,那直接用对应类型通常比 String 更稳。
原因很直白:
- 语义更清楚
- 查询和函数适配更自然
- 后续转换和校验也更方便
这类列如果一开始被你偷懒塞成了字符串,之后经常会在过滤、转换、函数使用上多出很多补丁。
容器类型:Array、Tuple、Map、Nested 怎么分工
Array:同类型列表
Array(T) 很适合表示一列里的一组同类型值,比如:
- tag 列表
- 维度 ID 列表
- 时间序列片段
示例:
tags Array(String)
Tuple:一行里绑在一起的定长结构
Tuple 更像是“几种类型的固定打包”,适合轻量结构,但不一定适合作为特别复杂的业务对象主载体。
Map:看起来像字典,但别把它想得太像传统哈希表
Map(K, V) 很容易让人产生一个误解:它是不是和很多数据库里的 map 一样,按 key 查非常自然?
官方文档专门提醒了两个关键点:
- ClickHouse 的 map 允许重复 key
m[k]的读取是线性扫描,运行时复杂度跟 map 大小成线性关系
Map(K, V)
这两个点非常值得记住。
也就是说:
Map更像“成对存储的一组 key-value”- 它内部实现接近
Array(Tuple(K, V)) - 它不是为“超高频、随机 key 查找”量身做的
官方文档还说了一个细节:
Map的 key 不能是Nullable- 读取不存在的 key 时,返回的是值类型的默认值,而不是报错
Map(K, V)
如果你真要判断 key 存不存在,记得用 mapContains,不要只看 m[k] 的结果。
Nested:适合成组出现的列
Nested 适合那种“一组字段一起重复”的结构,比如事件属性列表、商品明细片段。它和 JSON 的感觉有点像,但更偏结构化、预定义。
Nullable:能不用就别乱用
Nullable(T) 很常见,但官方文档对它的提醒也非常直接。
首先,Nullable 会额外存一份 NULL mask 文件;其次,官方文档明确写了:使用 Nullable 几乎总会对性能产生负面影响。Nullable(T)
这个提醒非常重要。
另外,官方文档还指出:
Nullable列不能包含在表索引里Array / Map不能直接作为Nullable(T)的T- 但你可以写成
Array(Nullable(Int8))这种形式
Nullable(T)
实际建模时,一个很实用的原则是:
- 如果业务上真有“缺失值”,再用
Nullable - 如果只是“空字符串”“0”“unknown”就能表达,那可以先别上
Nullable
尤其在高频查询列上,不要图省事一把梭全设成 Nullable。
JSON、Dynamic、Variant:半结构化数据别再只会塞 String
ClickHouse 这块现在已经不是“只能把 JSON 当字符串存着”的阶段了。
JSON:把 JSON 文档作为单列存储
官方文档说明,JSON 类型可以把 JSON 文档存进单列里,而且能按路径读子列。JSON Data Type
你可以像这样访问子路径:
SELECT json.a.b, json.c FROM events;
官方文档还展示了通过子列读取的方式,这也是它比“单纯 String 存 JSON”更有意思的地方。JSON Data Type
如果你的数据天然就是:
- 属性很多
- 字段会缓慢演化
- 并不想为每个字段立刻建显式列
那 JSON 会比裸 String 更像一等公民。
Dynamic:我现在还不知道它到底会来什么类型
官方文档对 Dynamic 的描述很直接:它允许在事先不知道所有类型的情况下,存储任意类型的值。Dynamic
这类类型适合那些模式不完全稳定、又不想一开始就过度建模的场景。
Variant:有限集合里的多种可能类型
如果一列只会在几种已知类型之间切换,Variant(T1, T2, ...) 比完全放飞的 Dynamic 更可控。
简单说:
Variant:类型集合是已知的Dynamic:类型集合不一定预先知道JSON:重点在文档结构和路径读取
一个很实用的选型顺序
如果你现在要设计一张 ClickHouse 表,类型选择可以按这个顺序想:
- 它是不是纯数值?如果要精确小数,优先
Decimal - 它是不是日期或时间?如果只按天分析,就别上高精度时间
- 它是不是重复值很多的字符串?优先想想
LowCardinality(String) - 它是不是真的需要
NULL?不要默认Nullable - 它是不是 key-value 结构?先确认
Map的读取代价能不能接受 - 它是不是半结构化文档?优先评估
JSON / Dynamic / Variant
这套判断比死记类型目录更有用。
用 Python 看类型系统,会更有手感
官方 Python 集成页给的是 clickhouse-connect,安装方式也很直接:pip install clickhouse-connect。Integrating ClickHouse with Python
先来一个最基础的例子,直接查系统表:
import clickhouse_connect
client = clickhouse_connect.get_client(
host='localhost',
port=8123,
username='default',
password=''
)
result = client.query("""
SELECT name, case_insensitive, alias_to
FROM system.data_type_families
ORDER BY name
LIMIT 20
""")
for row in result.result_rows:
print(row)
这个小查询很适合作为“类型系统体检入口”。
用 Python 建一张涵盖多类类型的表
client.command("""
CREATE TABLE IF NOT EXISTS demo_data_types
(
id UInt64,
city LowCardinality(String),
price Decimal(18, 2),
created_at DateTime64(3, 'Asia/Shanghai'),
is_active Boolean,
tags Array(String),
attrs Map(String, String),
ext_json JSON,
note Nullable(String)
)
ENGINE = MergeTree
ORDER BY (city, id)
""")
这个结构里其实就把几类常用选择放在一起了:
- 主键 ID 用
UInt64 - 城市名用
LowCardinality(String) - 金额用
Decimal - 时间用
DateTime64 - tag 用
Array - 属性包用
Map - 扩展字段用
JSON - 可缺失文案才用
Nullable
再插一批数据感受一下
from decimal import Decimal
from datetime import datetime
rows = [
[
1,
'Shanghai',
Decimal('19.90'),
datetime.now(),
True,
['new', 'vip'],
{'channel': 'app', 'campaign': 'spring'},
{'device': {'os': 'android'}, 'score': 98},
None,
],
[
2,
'Shanghai',
Decimal('29.50'),
datetime.now(),
False,
['promo'],
{'channel': 'web'},
{'device': {'os': 'ios'}, 'score': 88},
'manual review',
],
]
client.insert('demo_data_types', rows)
查一下几类类型的效果
result = client.query("""
SELECT
city,
sum(price) AS total_price,
count() AS cnt,
groupArray(tags) AS tag_groups,
ext_json.device.os AS os
FROM demo_data_types
GROUP BY city, os
ORDER BY city, os
""")
print(result.result_rows)
这里有几个特别直观的点:
LowCardinality(String)在分组场景里通常很香Decimal聚合时比浮点更让人安心JSON子路径能直接读Nullable仍然要有意识地控制使用范围