3. 行情数据建模
行情数据建模是分布式行情集群能否稳定运行的地基。ClickHouse 擅长高吞吐写入与大规模聚合查询,但前提是表结构、分区键、排序键与数据生命周期策略与行情数据的真实访问模式匹配。本节围绕四类核心行情数据展开:K线、逐笔成交、快照与盘口,先分析它们的时序特征,再落到 ClickHouse 的建表参数与生命周期管理。
一、四类行情数据的结构与特征
行情数据本质上都是带时间戳的时序数据,但不同数据的写入频率、查询模式和保留价值差异极大,不能套用同一张表。
- K线(Bar):按固定周期(1s、1m、1h、1d)聚合,字段包括开盘价、最高价、最低价、收盘价、成交量、成交额。写入频率低,查询以区间扫描和降采样为主,历史价值高,适合长期保留。
- 逐笔成交(Trade/Tick):每笔成交一条记录,包含成交价、成交量、买卖方向、成交编号。写入量最大,是回测与微观结构分析的核心,通常只保留数周至数月。
- 快照(Snapshot):按固定间隔(如 3s、500ms)采集的全市场或单标的切片,包含最新价、累计成交量、涨跌幅等。写入频率中等,用于行情回放与状态重建。
- 盘口(OrderBook):快照中买卖各 N 档的价量数组,结构为嵌套数组或扁平化档位字段。查询常需展开档位,对列式存储的嵌套类型支持要求高。
从时序特征看,逐笔与快照是高频追加写,K线是低频聚合写,盘口是定长数组写。这决定了它们在分区粒度、排序键和 TTL 上的不同选择。
二、分区键与排序键设计方法
分区键决定数据在磁盘上的物理切分,排序键决定 MergeTree 内部的有序性和主键索引。两者共同影响写入放大、查询裁剪和合并压力。
- 分区粒度:逐笔数据量大,建议按天分区
toYYYYMMDD(ts);若单日写入超过数亿行,可细化为小时toStartOfHour(ts)。K线数据量小,按月分区即可。 - 排序键顺序:把等值过滤字段放前面,时间字段放后面。例如
ORDER BY (symbol, ts)适合按标的查询;若查询以时间范围为主,则用ORDER BY (ts, symbol)。 - 主键与排序键分离:ClickHouse 中
PRIMARY KEY必须是ORDER BY的前缀。若排序键为(symbol, ts),主键可设为(symbol, ts)或仅symbol,视索引粒度而定。 - 避免高基数分区键:不要用
symbol做分区键,否则会产生海量小分区,拖垮合并线程。
经验法则:分区键控制数据生命周期(TTL 按分区删除),排序键控制查询裁剪效率。两者职责不同,不要混用。
三、典型建表示例
以下为逐笔成交与盘口快照的建表语句,体现分区、排序与 TTL 的组合。
-- 逐笔成交表
CREATE TABLE market.trade
(
ts DateTime64(3),
symbol LowCardinality(String),
price Decimal64(4),
volume UInt64,
side Enum8('buy' = 1, 'sell' = 2),
trade_id UInt64
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (symbol, ts)
TTL ts + INTERVAL 90 DAY DELETE
SETTINGS index_granularity = 8192;
-- 盘口快照表(Nested 存储买卖档位)
CREATE TABLE market.orderbook
(
ts DateTime64(3),
symbol LowCardinality(String),
bids Nested(price Decimal64(4), volume UInt64),
asks Nested(price Decimal64(4), volume UInt64)
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (symbol, ts)
TTL ts + INTERVAL 30 DAY DELETE;
盘口使用 Nested 类型后,查询时可用 bids.price[1] 取买一价,也可用 ARRAY JOIN 展开全部档位。若档位固定为 5 档或 10 档,也可用扁平列 bid_price_1 ... bid_price_10,查询更直观但扩展性差。
四、数据生命周期管理
行情数据价值随时间衰减,生命周期管理要兼顾存储成本与合规要求。
| 数据类型 | 热存储 | 冷存储 | 删除策略 |
|---|---|---|---|
| 逐笔成交 | 7 天 | 90 天 | TTL 到期 DELETE |
| 快照 | 3 天 | 30 天 | TTL 到期 DELETE |
| 盘口 | 1 天 | 14 天 | TTL 到期 DELETE |
| K线 | 30 天 | 永久 | 不删除或归档 |
ClickHouse 的 TTL 支持 TO DISK、TO VOLUME 和 DELETE 三种动作,可组合使用:先移动到冷盘,再到期删除。
ALTER TABLE market.trade
MODIFY TTL ts + INTERVAL 7 DAY TO VOLUME 'cold',
ts + INTERVAL 90 DAY DELETE;
冷热分层依赖存储策略配置。在 storage_configuration 中定义 hot 与 cold 两个 volume,分别指向 SSD 和 HDD,再在 TTL 中引用。
五、常见误区与复盘
- 误区一:所有行情表用同一套分区排序。逐笔与K线的写入量和查询模式完全不同,统一建模会导致小分区过多或查询扫描全表。
- 误区二:用 symbol 做分区键。标的数量可达数千,分区数爆炸,合并线程被拖垮,写入延迟飙升。
- 误区三:TTL 只设 DELETE 不设冷热分层。热盘被历史数据占满,新数据写入触发频繁合并,查询性能下降。
- 误区四:盘口用 String 存 JSON。失去列式存储优势,档位查询需解析字符串,CPU 开销大且无法用数组函数。
复盘建议:上线前用真实行情回放压测,观察 system.parts 的分区数量与 system.merges 的合并队列。若单表分区数持续超过 1000,说明分区粒度过细,应调整为更粗粒度或改用排序键裁剪。
行情数据建模没有万能模板,核心是让分区键对齐生命周期、排序键对齐查询模式、TTL 对齐数据价值。把这三点落实到每张表的 DDL 中,分布式行情集群才能在写入吞吐与查询延迟之间取得平衡。