4. ClickHouse 数据类型:整型、浮点型、字符串、日期时间、数组、Map、Nullable
数据类型这东西,说白了就是给数据定个「身份证」。你用对了类型,查询快、存储省;用错了,嗯,等着被运维同事请喝茶吧。
ClickHouse 的数据类型比 MySQL 丰富不少,但也没 PostgreSQL 那么「学术」。我个人习惯把它分成三组:基础型(整型、浮点、字符串)、时间型、复合型(数组、Map、Nullable)。咱们一个一个过。
4.1 整型:Int8 到 Int256,还有无符号版
ClickHouse 的整型家族很庞大。从 Int8 到 Int256,外加对应的 UInt8 到 UInt256。你可能会问:「256 位?谁用得上?」我在做用户画像标签系统时,确实用过 UInt128 存 IP 段的位图掩码——普通数据库根本扛不住这种玩法。
| 类型 | 字节数 | 范围 | 常见用途 |
|---|---|---|---|
| Int8 | 1 | -128 ~ 127 | 状态码、枚举值 |
| Int16 | 2 | -32768 ~ 32767 | 端口号、小范围计数 |
| Int32 | 4 | -2^31 ~ 2^31-1 | 用户ID、时间戳(秒级) |
| Int64 | 8 | -2^63 ~ 2^63-1 | 订单金额(分)、事件ID |
| UInt8 | 1 | 0 ~ 255 | IP 最后一段、小范围正数 |
| UInt16 | 2 | 0 ~ 65535 | 端口、年份 |
| UInt32 | 4 | 0 ~ 2^32-1 | IPv4 地址(整数形式) |
| UInt64 | 8 | 0 ~ 2^64-1 | 高精度计数器 |
4.2 浮点型:Float32 和 Float64
浮点型就两个:Float32(对应 C++ 的 float)和 Float64(对应 double)。
但我要泼盆冷水——别拿 ClickHouse 的浮点型做金融计算。浮点数的精度问题在聚合时会被放大。我记得有一次做广告计费报表,用 Float64 累加千分之几的误差,月底对账差了十几万。后来全改成 Int64 存「分」单位,才消停。
-- 浮点型建表示例
CREATE TABLE sensor_data (
sensor_id UInt32,
temperature Float32, -- 温度,允许小误差
humidity Float32,
event_time DateTime
) ENGINE = MergeTree()
ORDER BY event_time;
4.3 字符串:String 和 FixedString
字符串类型就两个:String 和 FixedString(N)。
String 是变长字符串,底层用 LZ4 压缩,存储效率很高。FixedString(N) 是定长,N 字节,不够的补 \0。你可能会觉得 FixedString 更省空间——其实不一定。String 配合列式压缩,很多时候比 FixedString 还小。
-- 字符串类型对比
CREATE TABLE user_profiles (
user_id UInt64,
name String, -- 变长,推荐
country_code FixedString(2), -- 定长,适合 ISO 国家码
email String
) ENGINE = MergeTree()
ORDER BY user_id;
4.4 日期时间:Date、DateTime、DateTime64
时间类型是 OLAP 的灵魂。ClickHouse 提供了三种:
- Date:只存日期,2 字节,范围 1970-01-01 ~ 2149-06-06
- DateTime:日期+时间,4 字节,秒级精度
- DateTime64:日期+时间,可指定毫秒/微秒/纳秒精度
我刚开始用 ClickHouse 时犯过一个错:用 DateTime 存日志时间戳,结果发现同一秒内的事件顺序乱了。后来换成 DateTime64(3) 存毫秒,才把时序理清楚。
-- 时间类型建表
CREATE TABLE events (
event_id UInt64,
event_date Date, -- 分区常用
event_time DateTime, -- 秒级
precise_time DateTime64(3), -- 毫秒级
timezone String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, event_time);
4.5 数组:Array(T)
Array 是 ClickHouse 的「杀手锏」之一。它允许在一列里存多个值,而且支持嵌套。
你可能会问:「这不违反第一范式吗?」嗯,在 OLAP 场景下,范式是给 OLTP 准备的。Array 配合 arrayJoin 函数,可以轻松实现「一行变多行」的展开操作。
-- 数组类型示例
CREATE TABLE product_tags (
product_id UInt32,
tags Array(String), -- 标签数组
prices Array(Float32), -- 历史价格数组
spec Array(Array(String)) -- 嵌套数组:规格列表
) ENGINE = MergeTree()
ORDER BY product_id;
-- 插入数据
INSERT INTO product_tags VALUES
(1, ['手机', '数码', '5G'], [4999.00, 4799.00], [['黑色','6GB'], ['白色','8GB']]);
-- 展开查询
SELECT product_id, arrayJoin(tags) AS tag
FROM product_tags
WHERE product_id = 1;
Array(Nullable(String)) 而不是空数组,存储效率更高。
4.6 Map:Map(K, V)
Map 类型是 ClickHouse 21.6 之后才稳定下来的。它本质上是一个键值对数组,但查询语法更友好。
我拿它做过「动态属性」场景——用户自定义字段。传统做法要建 EAV 表,查询时一堆 JOIN。用 Map 一列搞定,配合 mapKeys、mapValues 函数,灵活度拉满。
-- Map 类型示例
CREATE TABLE user_attributes (
user_id UInt64,
attributes Map(String, String) -- 动态属性
) ENGINE = MergeTree()
ORDER BY user_id;
-- 插入数据
INSERT INTO user_attributes VALUES
(1, {'level': 'VIP3', 'source': '广告', 'register_channel': 'iOS'});
-- 查询某个 key
SELECT user_id, attributes['level'] AS level
FROM user_attributes
WHERE user_id = 1;
4.7 Nullable:让列可以「空」
ClickHouse 默认所有列都是 NOT NULL。想允许空值,得显式声明 Nullable(类型)。
但我要说——Nullable 有代价。它会在每行额外存一个标记位,压缩率下降,查询性能也受影响。我见过有人把所有列都包上 Nullable,结果表体积膨胀了 30%。
-- Nullable 使用示例
CREATE TABLE user_actions (
user_id UInt64,
action String,
duration Nullable(UInt32), -- 可能为空
error_code Nullable(String) -- 成功时为 NULL
) ENGINE = MergeTree()
ORDER BY (user_id, action);
4.8 本章知识体系总览
下面这张图把 ClickHouse 的数据类型分成了四大族群,以及它们之间的关联关系。你可以把它当作「类型选择速查表」。
好了,ClickHouse 的数据类型就这些。记住一个核心原则:类型越精确,性能越好。别偷懒全用 String 一把梭——那是 MySQL 时代的坏习惯。在 ClickHouse 里,每一字节都值得认真对待。