4. ClickHouse 数据类型与 DDL:基础数据类型、Nullable、LowCardinality、Array、Tuple、Nested、表引擎选择策略

数据类型和 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 位)
  • 查询性能下降(每次都要检查标记)
  • 索引效率降低(无法充分利用稀疏索引)
核心原则: 能用默认值代替 NULL,就尽量别用 Nullable。比如缺失的数值用 0,缺失的字符串用 ''。

我有个真实案例:某张 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;
我的建议: 状态码、地区、渠道来源这类字段,无脑用 LowCardinality。我优化过一个广告系统,把 20 个 String 字段改成 LowCardinality,查询性能提升了 3 倍。

注意:基数超过 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 存订单明细,结果发现无法单独更新某个子项。因为 ClickHouse 不支持部分数组更新。后来改成了明细表 + 宽表的设计。

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 按版本折叠
我的选择原则: 不确定时,无脑选 MergeTree。等业务稳定了,再根据查询模式优化引擎。别一开始就上复杂引擎,容易踩坑。

4.7 本章知识体系

下面这张图,把数据类型和 DDL 的核心脉络串起来了。你可以把它当思维导图用。

ClickHouse 数据类型与 DDL 知识体系 DDL + 数据类型 基础数据类型 Nullable / LowCardinality Array / Tuple / Nested 表引擎选择策略 Int / Float String / FixedString Date / DateTime Nullable 标记数组 LowCardinality 字典编码 Array(T) 定长数组 Tuple 异构元组 Nested 模拟嵌套 MergeTree 通用 ReplacingMergeTree 去重 SummingMergeTree 预聚合 核心原则 1. 能用默认值代替 NULL,就别用 Nullable 2. 低基数字段优先用 LowCardinality 3. 不确定引擎时,无脑选 MergeTree

这张图把本章的知识点串成了体系。从基础类型到高级结构,再到引擎选择,每一步都有明确的决策依据。

记住:数据类型选对了,后面 90% 的问题都不会发生。建表前多花 5 分钟思考类型,能省下后面 5 小时的排障时间。