数据类型与表引擎:基础数据类型、MergeTree家族引擎、ReplacingMergeTree与SummingMergeTree实战

聊到ClickHouse,绕不开的就是数据类型和表引擎。这两样东西,说白了就是数据库的「地基」和「骨架」。地基没打好,后面盖多高的楼都悬;骨架选错了,跑起来就费劲。今天咱们就掰开揉碎了聊聊这块内容。

一、基础数据类型:别小看这些「砖块」

ClickHouse的数据类型,乍一看跟别的数据库差不多,但用起来细节挺多。我个人习惯先把常用的几类理清楚:

1. 数值类型

  • 整数:UInt8、UInt16、UInt32、UInt64,还有带符号的Int8到Int64。注意,ClickHouse默认用UInt64做自增ID,但如果你数据量没那么大,用UInt32就够了,能省不少空间。
  • 浮点数:Float32、Float64。做量化分析时,价格数据我建议用Decimal而不是Float,避免精度丢失。你想想看,股票价格精确到小数点后两位,用Float32存久了误差会累积。
  • DecimalDecimal(P, S),P是总位数,S是小数位数。比如Decimal(18, 4),存价格、成交量这些精确数据很稳。
我的经验:在量化交易场景中,时间戳用DateTime64(3)存毫秒级精度,别用Int64自己算,查询时直接按时间过滤,效率高很多。

2. 字符串类型

  • String:变长字符串,存股票代码、交易对名称这些。
  • FixedString(N):定长字符串。比如股票代码固定6位,用FixedString(6)比String快,因为不需要记录长度信息。

3. 日期时间类型

  • Date:只存日期,占2字节。
  • DateTime:存日期+时间,精度到秒。
  • DateTime64:支持毫秒、微秒。量化分析中,高频数据必须用这个。

4. 特殊类型

  • Array(T):数组类型。比如存一条K线里的多个价格点,用Array(Float64)很方便。
  • Tuple:元组,可以存不同类型的数据组合。
  • Nullable:允许空值。但注意,Nullable会额外占用空间,能不用尽量不用。
避坑指南:我曾经在项目中大量使用Nullable字段,结果查询性能下降了30%以上。后来全部改成用默认值替代空值,速度立马回来了。记住:ClickHouse里,空值是有代价的。

二、MergeTree家族引擎:核心中的核心

MergeTree是ClickHouse的「王牌引擎」。其他所有引擎,说白了都是它的变种。我刚开始接触时,觉得MergeTree这个名字挺抽象,后来理解了它的工作原理,才发现设计得真巧妙。

它的核心思想是:数据先按分区存储,后台异步合并。写入时很快,查询时按分区裁剪数据,效率极高。

MergeTree 家族引擎核心逻辑 数据写入 INSERT 分区存储 Part 1 | Part 2 | Part 3 后台合并 Merge 线程 查询请求 SELECT 分区裁剪 只扫描相关分区 返回结果 聚合/过滤后数据 核心特点:写入快、按分区查询、后台异步合并 排序键决定数据物理存储顺序,直接影响查询性能

创建MergeTree表的语法很简单:

CREATE TABLE trades (
    symbol String,
    trade_time DateTime64(3),
    price Decimal(18, 4),
    volume UInt64
) ENGINE = MergeTree()
ORDER BY (symbol, trade_time)
PARTITION BY toYYYYMM(trade_time);

这里的关键是ORDER BYPARTITION BY。排序键决定了数据在磁盘上的物理顺序,查询时按这个顺序扫描效率最高。分区键则决定了数据按什么粒度切分。

核心要点:排序键选得好,查询性能翻倍。量化分析中,我通常把「交易对+时间」作为排序键,这样按标的和时间范围查询时,数据是连续存储的,IO开销最小。

三、ReplacingMergeTree:去重利器

做量化分析时,经常遇到重复数据。比如从交易所拉数据,偶尔会收到重复的成交记录。这时候ReplacingMergeTree就派上用场了。

它的原理很简单:在后台合并时,如果发现排序键相同的数据,只保留最后一条(按版本号或时间戳判断)。

CREATE TABLE trades_dedup (
    symbol String,
    trade_time DateTime64(3),
    price Decimal(18, 4),
    volume UInt64,
    version UInt64  -- 版本号,越大越新
) ENGINE = ReplacingMergeTree(version)
ORDER BY (symbol, trade_time)
PARTITION BY toYYYYMM(trade_time);
我的习惯:我会在数据写入时,用now64(3)作为版本号。这样合并时,最新的数据会覆盖旧的。但注意,去重不是实时的,是在后台合并时发生的。如果你需要立即去重,可以手动执行OPTIMIZE TABLE,但生产环境不建议频繁操作。

嗯,这里要注意:ReplacingMergeTree的去重是「最终一致性」的。也就是说,刚写入的数据可能还没合并,查询时仍然会看到重复。我一般在查询时加个FINAL修饰符:

SELECT * FROM trades_dedup FINAL WHERE symbol = 'BTCUSDT';

这样查询时会在内存中做一次去重,但性能会受影响。数据量大的时候,我建议还是等后台合并完成,或者自己写逻辑处理。

四、SummingMergeTree:聚合加速器

这个引擎是我在量化分析中最常用的。它的作用是在后台合并时,自动对数值列进行求和。说白了,就是预聚合。

比如你存的是每秒的成交数据,但查询时经常按分钟、小时聚合。用SummingMergeTree,合并时就把相同排序键的数据求和了,查询时直接读聚合后的结果,快得飞起。

CREATE TABLE trades_agg (
    symbol String,
    bucket_time DateTime,  -- 时间桶,比如每分钟
    total_volume UInt64,
    total_amount Decimal(18, 4),
    trade_count UInt64
) ENGINE = SummingMergeTree()
ORDER BY (symbol, bucket_time)
PARTITION BY toYYYYMM(bucket_time);

写入时,你只需要按时间桶写入原始数据:

INSERT INTO trades_agg VALUES
('BTCUSDT', '2024-01-01 10:00:00', 100, 5000.00, 10),
('BTCUSDT', '2024-01-01 10:00:00', 200, 10000.00, 15);

合并后,这两条数据会变成一条:

('BTCUSDT', '2024-01-01 10:00:00', 300, 15000.00, 25)
避坑指南:我曾经犯过一个错误——把价格字段也放进了SummingMergeTree。结果合并后价格被求和了,变成了一个毫无意义的大数。记住:只有可累加的数值列才适合用这个引擎,像价格、均价这种,应该用ReplacingMergeTree或者自己维护。

五、实战对比:什么时候用哪个?

场景 推荐引擎 原因
原始成交数据,需要精确查询 MergeTree 保留所有原始记录,不做任何变换
有重复数据,需要去重 ReplacingMergeTree 自动去重,节省存储和查询时间
按时间桶聚合,如分钟K线 SummingMergeTree 预聚合,查询速度提升10倍以上
既要去重又要聚合 AggregatingMergeTree 高级用法,支持自定义聚合函数

我个人建议:在量化分析项目中,可以同时建三张表——原始数据用MergeTree,去重后的用ReplacingMergeTree,聚合后的用SummingMergeTree。数据流就是:原始表 → 去重表 → 聚合表。这样各司其职,查询时按需选择。

总结一下:数据类型选对了,存储空间省一半;表引擎选对了,查询速度快10倍。别小看这些基础的东西,我见过太多项目因为一开始没选对,后面改得焦头烂额。嗯,今天就聊到这儿,下次咱们聊聊索引和分区的高级玩法。
专注资料整理