数据类型:整型、浮点型、字符串、日期时间、枚举、数组、元组、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、时间戳(毫秒) |
浮点型:精度是个坑
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!)
如果你需要精确计算,比如金额、税率,请用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;
日期时间:选对粒度很重要
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;
枚举:用数字代替字符串
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;
元组:把多个字段打包
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');
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;
知识体系总览
下面这张图帮你理清今天学的所有数据类型,以及它们之间的关系:
好了,这一章的内容就到这。数据类型是ClickHouse的基石,选对了事半功倍。记住我的经验:能用小的别用大的,能用非空别用Nullable,能用原生类型别用字符串。你写建表语句时多花一分钟想类型,后面跑查询时就能省一小时。