码到成功
ClickHouse——Data Types
在 `ClickHouse` 里,类型系统不是附属品,它几乎直接影响:
- 存储体积
- 查询速度
- 压缩效果
- 索引与跳数效果
- Python 侧写入和读取时的心智负担
官方文档把类型系统拆得非常细,从整数、浮点、时间、字符串,到 `Map`、`JSON`、`Dynamic`、`LowCardinality`、`Nullable`,每一类都有自己的脾气。[Data Types in ClickHouse](https://clickhouse.com/docs/sql-reference/data-types)
这篇就围绕 `data-types` 这条线来收一遍,不追求把所有类型背成目录,而是尽量讲清楚:**常见类型怎么选、哪些包装类型特别值钱、哪些地方最容易踩坑。**

## 先别急着建表,先知道 ClickHouse 到底有哪些类型
官方文档的类型页列出了当前支持的一大串类型,除了常见的:
- `Int / UInt`
- `Float32 / Float64 / BFloat16`
- `Decimal`
- `String / FixedString`
- `Date / Date32 / Time / Time64 / DateTime / DateTime64`
- `Boolean`
- `Array / Tuple / Map / Nested`
还有一批更偏分析场景的类型:
- `UUID`
- `IPv4 / IPv6`
- `Enum`
- `Variant`
- `LowCardinality`
- `Nullable`
- `JSON`
- `Dynamic`
官方文档还特别提到一个很实用的系统表:`system.data_type_families`。它能帮助你查看当前有哪些类型、哪些名字其实是别名、以及名称是否区分大小写。[Data Types in ClickHouse](https://clickhouse.com/docs/sql-reference/data-types)
这张表很适合在你不确定某个类型名到底怎么写的时候先查一眼:
```sql
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` 的存在,就是为了避免浮点在精度上的那些小脾气。
一个常见例子:
```sql
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](https://clickhouse.com/docs/sql-reference/data-types)
但一个工程上的小建议是:如果状态天然就不止两类,比如:
- `pending / running / success / failed`
- `draft / published / archived`
那就不要硬拆成多个布尔列,直接上枚举或字符串会更自然。
## 字符串和枚举:别一股脑全都 String
### String:最通用,但不是最省
`String` 非常灵活,所以很多人建表时会本能地往它身上靠。
问题在于,它过于宽泛了。
如果你的列有明显的模式,比如:
- 状态值很少
- 国家码重复很多
- 渠道名固定集合
那 `String` 虽然能装,但通常不是最优解。
### FixedString(N):定长字段才值得用
`FixedString(N)` 不是拿来代替所有 `String` 的。它更适合那些长度天然固定的字段,比如固定编码或二进制片段。
如果字符串长度变化很大,硬上 `FixedString` 反而会别扭。
### Enum:适合小而稳定的离散集合
如果一列的取值非常少,而且变化频率低,`Enum` 是个很利落的选择。
比如:
```sql
status Enum8(
'new' = 1,
'paid' = 2,
'done' = 3,
'cancelled' = 4
)
```
但它有一个现实问题:后续要改枚举成员时,没有 `String` 或 `LowCardinality(String)` 那么松快。
### LowCardinality:ClickHouse 里特别值得认识的包装类型
官方文档对 `LowCardinality(T)` 的描述很直接:它把其他类型的内部表示改成字典编码(dictionary-encoded)。[LowCardinality(T)](https://clickhouse.com/docs/sql-reference/data-types/lowcardinality)
这东西特别适合:
- 城市名
- 国家码
- 渠道名
- 类别标签
- 重复度很高的字符串列
官方文档还给了一个很有参考价值的经验区间:
- 如果字典里的不同值少于 `10,000`,通常收益更明显
- 如果不同值超过 `100,000`,可能反而不如普通类型
[LowCardinality(T)](https://clickhouse.com/docs/sql-reference/data-types/lowcardinality)
这条经验很实用,别背成教条,但值得记在脑子里。
另外,官方文档还建议:处理字符串时,可以优先考虑 `LowCardinality` 而不是 `Enum`,因为它更灵活,而且经常有相近甚至更好的效率。[LowCardinality(T)](https://clickhouse.com/docs/sql-reference/data-types/lowcardinality)
这句话在工程里非常有分量。
## 时间类型:别让时间精度和时区混成一团
ClickHouse 的时间类型并不少:
- `Date`
- `Date32`
- `Time`
- `Time64`
- `DateTime`
- `DateTime64`
实际建表时,可以先按这个思路判断:
### Date / Date32:只关心日期,不关心具体时刻
如果你的分析只是按天聚合,比如订单日、注册日、账单日,优先考虑 `Date` 或 `Date32`。
你不需要“时分秒”的时候,就别多背一个更重的时间类型。
### DateTime:到秒就够
很多埋点、日志、订单创建时间,用 `DateTime` 就足够了。
### DateTime64:需要更高精度时再上
如果你要记录:
- 毫秒级日志
- 链路事件
- 更细粒度的操作时间
那 `DateTime64` 会更自然。
一个很常见的做法是:
```sql
event_time DateTime64(3, 'Asia/Shanghai')
```
它表达得就非常清楚:毫秒精度,按指定时区解释。
这里最怕的不是类型太少,而是“明明只按天分析,却全表都上了高精度时间列”,最后存了很多自己根本不会用到的信息。
## UUID、IP、网络类字段:别把它们都塞成字符串
官方文档列出了 `UUID`、`IPv4`、`IPv6` 这些专门类型。[Data Types in ClickHouse](https://clickhouse.com/docs/sql-reference/data-types)
如果你本来就知道一列语义明确,比如它天然就是 UUID 或 IP 地址,那直接用对应类型通常比 `String` 更稳。
原因很直白:
- 语义更清楚
- 查询和函数适配更自然
- 后续转换和校验也更方便
这类列如果一开始被你偷懒塞成了字符串,之后经常会在过滤、转换、函数使用上多出很多补丁。
## 容器类型:Array、Tuple、Map、Nested 怎么分工
### Array:同类型列表
`Array(T)` 很适合表示一列里的一组同类型值,比如:
- tag 列表
- 维度 ID 列表
- 时间序列片段
示例:
```sql
tags Array(String)
```
### Tuple:一行里绑在一起的定长结构
`Tuple` 更像是“几种类型的固定打包”,适合轻量结构,但不一定适合作为特别复杂的业务对象主载体。
### Map:看起来像字典,但别把它想得太像传统哈希表
`Map(K, V)` 很容易让人产生一个误解:它是不是和很多数据库里的 map 一样,按 key 查非常自然?
官方文档专门提醒了两个关键点:
- ClickHouse 的 map **允许重复 key**
- `m[k]` 的读取是**线性扫描**,运行时复杂度跟 map 大小成线性关系
[Map(K, V)](https://clickhouse.com/docs/sql-reference/data-types/map)
这两个点非常值得记住。
也就是说:
- `Map` 更像“成对存储的一组 key-value”
- 它内部实现接近 `Array(Tuple(K, V))`
- 它不是为“超高频、随机 key 查找”量身做的
官方文档还说了一个细节:
- `Map` 的 key 不能是 `Nullable`
- 读取不存在的 key 时,返回的是值类型的默认值,而不是报错
[Map(K, V)](https://clickhouse.com/docs/sql-reference/data-types/map)
如果你真要判断 key 存不存在,记得用 `mapContains`,不要只看 `m[k]` 的结果。
### Nested:适合成组出现的列
`Nested` 适合那种“一组字段一起重复”的结构,比如事件属性列表、商品明细片段。它和 JSON 的感觉有点像,但更偏结构化、预定义。
## Nullable:能不用就别乱用
`Nullable(T)` 很常见,但官方文档对它的提醒也非常直接。
首先,`Nullable` 会额外存一份 `NULL` mask 文件;其次,官方文档明确写了:**使用 `Nullable` 几乎总会对性能产生负面影响**。[Nullable(T)](https://clickhouse.com/docs/sql-reference/data-types/nullable)
这个提醒非常重要。
另外,官方文档还指出:
- `Nullable` 列不能包含在表索引里
- `Array / Map` 不能直接作为 `Nullable(T)` 的 `T`
- 但你可以写成 `Array(Nullable(Int8))` 这种形式
[Nullable(T)](https://clickhouse.com/docs/sql-reference/data-types/nullable)
实际建模时,一个很实用的原则是:
- 如果业务上真有“缺失值”,再用 `Nullable`
- 如果只是“空字符串”“0”“unknown”就能表达,那可以先别上 `Nullable`
尤其在高频查询列上,不要图省事一把梭全设成 `Nullable`。
## JSON、Dynamic、Variant:半结构化数据别再只会塞 String
ClickHouse 这块现在已经不是“只能把 JSON 当字符串存着”的阶段了。
### JSON:把 JSON 文档作为单列存储
官方文档说明,`JSON` 类型可以把 JSON 文档存进单列里,而且能按路径读子列。[JSON Data Type](https://clickhouse.com/docs/sql-reference/data-types/json)
你可以像这样访问子路径:
```sql
SELECT json.a.b, json.c FROM events;
```
官方文档还展示了通过子列读取的方式,这也是它比“单纯 String 存 JSON”更有意思的地方。[JSON Data Type](https://clickhouse.com/docs/sql-reference/data-types/json)
如果你的数据天然就是:
- 属性很多
- 字段会缓慢演化
- 并不想为每个字段立刻建显式列
那 `JSON` 会比裸 `String` 更像一等公民。
### Dynamic:我现在还不知道它到底会来什么类型
官方文档对 `Dynamic` 的描述很直接:它允许在事先不知道所有类型的情况下,存储任意类型的值。[Dynamic](https://clickhouse.com/docs/sql-reference/data-types/dynamic)
这类类型适合那些模式不完全稳定、又不想一开始就过度建模的场景。
### Variant:有限集合里的多种可能类型
如果一列只会在几种已知类型之间切换,`Variant(T1, T2, ...)` 比完全放飞的 `Dynamic` 更可控。
简单说:
- `Variant`:类型集合是已知的
- `Dynamic`:类型集合不一定预先知道
- `JSON`:重点在文档结构和路径读取
## 一个很实用的选型顺序
如果你现在要设计一张 ClickHouse 表,类型选择可以按这个顺序想:
1. 它是不是纯数值?如果要精确小数,优先 `Decimal`
2. 它是不是日期或时间?如果只按天分析,就别上高精度时间
3. 它是不是重复值很多的字符串?优先想想 `LowCardinality(String)`
4. 它是不是真的需要 `NULL`?不要默认 `Nullable`
5. 它是不是 key-value 结构?先确认 `Map` 的读取代价能不能接受
6. 它是不是半结构化文档?优先评估 `JSON / Dynamic / Variant`
这套判断比死记类型目录更有用。

## 用 Python 看类型系统,会更有手感
官方 Python 集成页给的是 `clickhouse-connect`,安装方式也很直接:`pip install clickhouse-connect`。[Integrating ClickHouse with Python](https://clickhouse.com/jp/integrations/python)
先来一个最基础的例子,直接查系统表:
```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 建一张涵盖多类类型的表
```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`
### 再插一批数据感受一下
```python
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)
```
### 查一下几类类型的效果
```python
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` 仍然要有意识地控制使用范围
