数据类型:整型、浮点型、字符串、日期时间、枚举、数组、元组、Nullable、IPv4/IPv6

数据类型这东西,说白了就是给数据找个合适的"容器"。选对了,查询快、省空间;选错了,要么跑不动,要么存不下。我在做ClickHouse项目时,见过太多因为数据类型选错导致的性能事故,所以这一章咱们得好好聊聊。

整型:别总想着用Int64

ClickHouse的整型分有符号和无符号两大类。有符号的从Int8到Int256,无符号的从UInt8到UInt256。你可能会问:"直接用Int64不就行了?"嗯,我以前也这么干过,直到有一次给一个百亿级日志表做优化,光是把Int64改成UInt16,存储就省了75%。

类型 字节数 取值范围 常见用途
Int8 1 -128 ~ 127 状态码、小枚举
Int16 2 -32768 ~ 32767 端口号、年龄
Int32 4 -2^31 ~ 2^31-1 用户ID、时间戳
Int64 8 -2^63 ~ 2^63-1 大数值、订单金额(分)
UInt8 1 0 ~ 255 IP最后一段、小计数
UInt16 2 0 ~ 65535 端口号、HTTP状态码
UInt32 4 0 ~ 2^32-1 IPv4地址(存整数)
UInt64 8 0 ~ 2^64-1 自增ID、时间戳(毫秒)
我的习惯:能用UInt8绝不用Int16,能用Int16绝不用Int32。数据量上亿后,每个字节的节省都会被放大。我曾经把一个表的Int64全改成按需的最小类型,存储从120GB降到了45GB。

浮点型:精度是个坑

ClickHouse提供Float32和Float64,对应单精度和双精度。但我要提醒你——浮点数有精度问题。比如0.1 + 0.2,结果不是0.3,而是0.30000000000000004。

-- 看看这个
SELECT 0.1 + 0.2;
-- 结果: 0.30000000000000004

-- 比较浮点数要小心
SELECT 0.1 + 0.2 = 0.3;
-- 结果: 0 (false!)
我曾经踩过的坑:做金融报表时用了Float64存金额,结果对账差了0.01元。查了两天才发现是浮点精度问题。后来所有金额字段全改成了Decimal类型。

如果你需要精确计算,比如金额、税率,请用Decimal类型。Decimal(P, S)中P是总位数,S是小数位数。

-- Decimal推荐用法
CREATE TABLE orders (
    order_id UInt64,
    amount Decimal(18, 2),  -- 总长18位,小数2位
    tax_rate Decimal(5, 4)   -- 总长5位,小数4位
) ENGINE = MergeTree()
ORDER BY order_id;

字符串:String vs FixedString

String是变长字符串,类似其他数据库的VARCHAR。FixedString(N)是定长,长度固定为N字节。我个人建议:90%的场景用String就够了。

什么时候用FixedString?比如IP地址、固定编码的ID、MD5值。长度固定时,FixedString查询更快,因为不需要记录长度信息。

-- String: 灵活,推荐
CREATE TABLE logs (
    message String,
    url String
) ENGINE = MergeTree()
ORDER BY message;

-- FixedString: 长度固定时用
CREATE TABLE devices (
    mac FixedString(17),  -- "AA:BB:CC:DD:EE:FF"
    md5_hash FixedString(32)
) ENGINE = MergeTree()
ORDER BY mac;
注意:FixedString如果存的内容不足指定长度,会用\0填充。查询时这些\0可能会让你困惑,所以除非你确定长度固定,否则用String更省心。

日期时间:选对粒度很重要

ClickHouse有Date、DateTime、DateTime64三种时间类型。Date只精确到天,占2字节;DateTime精确到秒,占4字节;DateTime64可以精确到纳秒,占8字节。

类型 精度 字节 范围
Date 天 2 1970-01-01 ~ 2149-06-06
DateTime 秒 4 1970-01-01 00:00:00 ~ 2106-02-07 06:28:15
DateTime64 亚秒(可配置) 8 1925-01-01 ~ 2283-11-11
-- 实际使用示例
CREATE TABLE events (
    event_date Date,                    -- 按天分区
    event_time DateTime,                -- 精确到秒
    precise_time DateTime64(3)          -- 精确到毫秒
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)
ORDER BY event_time;
我的建议:如果只需要按天统计,用Date就够了,省一半空间。需要精确到秒用DateTime。只有金融交易、日志毫秒级分析才需要DateTime64。

枚举:用数字代替字符串

Enum8和Enum16让你用数字存字符串。比如状态字段,存'active'、'inactive'、'pending'这些字符串很占空间,用枚举就变成了1、2、3。

-- 定义枚举
CREATE TABLE users (
    user_id UInt32,
    status Enum8('active' = 1, 'inactive' = 2, 'pending' = 3)
) ENGINE = MergeTree()
ORDER BY user_id;

-- 插入时可以直接用字符串
INSERT INTO users VALUES (1, 'active');

-- 查询时看到的是字符串,存的是数字
SELECT status, toInt8(status) AS status_code FROM users;

Enum8最多256个值,Enum16最多65536个值。我个人习惯:状态、类型、级别这种固定集合的字段,都用枚举。查询快,可读性也好。

数组:ClickHouse的"大杀器"

Array(T)可以存任意类型的数组。这是ClickHouse的特色功能,传统关系型数据库很少支持。

-- 创建带数组的表
CREATE TABLE user_tags (
    user_id UInt32,
    tags Array(String),
    scores Array(Float32)
) ENGINE = MergeTree()
ORDER BY user_id;

-- 插入数组数据
INSERT INTO user_tags VALUES 
    (1, ['vip', 'new_user', 'android'], [95.5, 80.0, 88.3]),
    (2, ['vip', 'ios'], [92.1, 85.6]);

-- 数组函数查询
SELECT 
    user_id,
    arrayLength(tags) AS tag_count,
    arraySum(scores) AS total_score
FROM user_tags;
注意:数组里的元素类型必须一致。不能['a', 1, 2.5]这样混着来。ClickHouse会尝试类型转换,但最好保持统一。

元组:把多个字段打包

Tuple(T1, T2, ...)可以把不同类型的数据打包成一个字段。我常用它来存坐标、配置项、或者临时组合数据。

-- 元组示例
CREATE TABLE locations (
    point_id UInt32,
    coordinates Tuple(Float64, Float64),  -- (经度, 纬度)
    metadata Tuple(String, UInt32, Date)  -- (名称, 访问量, 创建日期)
) ENGINE = MergeTree()
ORDER BY point_id;

-- 插入和查询
INSERT INTO locations VALUES 
    (1, (116.397, 39.908), ('天安门', 100000, '2024-01-01'));

SELECT 
    point_id,
    coordinates.1 AS longitude,
    coordinates.2 AS latitude,
    metadata.1 AS name
FROM locations;

Nullable:允许为空,但有代价

Nullable(T)让任何类型都可以存NULL。但我要提醒你——Nullable有性能开销。每个Nullable字段会额外生成一个标记文件来记录哪些行是NULL。

-- 使用Nullable
CREATE TABLE user_profiles (
    user_id UInt32,
    nickname Nullable(String),    -- 昵称可能为空
    age Nullable(UInt8),          -- 年龄可能未知
    email String                  -- 邮箱必填
) ENGINE = MergeTree()
ORDER BY user_id;

-- 插入NULL
INSERT INTO user_profiles VALUES 
    (1, '小明', 25, 'ming@example.com'),
    (2, NULL, NULL, 'hong@example.com');
我曾经踩过的坑:一开始把所有字段都设成Nullable,结果查询慢了一倍。后来发现大部分字段其实不需要NULL,改成非空后性能立马回来了。我的原则是:能不用Nullable就不用,实在需要再考虑。

IPv4/IPv6:专门为网络地址设计

ClickHouse原生支持IPv4和IPv6类型。相比用String存IP地址,这些类型更省空间,查询更快,还支持IP范围计算。

-- IPv4和IPv6示例
CREATE TABLE network_logs (
    client_ip IPv4,
    server_ip IPv6,
    request_time DateTime
) ENGINE = MergeTree()
ORDER BY request_time;

-- 插入和查询
INSERT INTO network_logs VALUES 
    ('192.168.1.1', '::1', now()),
    ('10.0.0.1', '2001:db8::1', now());

-- IP函数
SELECT 
    client_ip,
    IPv4NumToString(client_ip) AS ip_string,
    IPv4StringToNum('192.168.1.1') AS ip_num
FROM network_logs;
我的习惯:存IPv4用IPv4类型,不要用String或UInt32。虽然UInt32也能存,但IPv4类型可读性更好,而且ClickHouse内部做了优化,查询效率不比UInt32差。

知识体系总览

下面这张图帮你理清今天学的所有数据类型,以及它们之间的关系:

ClickHouse 数据类型体系 ClickHouse 数据类型 数值类型 字符串类型 时间类型 复合/特殊类型 整型 浮点型 String FixedString Date DateTime 枚举 数组 元组 Nullable IPv4 / IPv6 选型建议 • 能用小不用大:Int8 > Int16 > Int32 > Int64,每升一级多占一倍空间 • 金额用Decimal,别用Float:精度问题会让你对账对到崩溃 • 字符串优先用String:FixedString只在长度固定时考虑 • Nullable慎用:能不用就不用,性能差距很明显 • IP地址用IPv4/IPv6类型:比String省空间,比UInt32更直观

好了,这一章的内容就到这。数据类型是ClickHouse的基石,选对了事半功倍。记住我的经验:能用小的别用大的,能用非空别用Nullable,能用原生类型别用字符串。你写建表语句时多花一分钟想类型,后面跑查询时就能省一小时。