3. 行情数据建模

行情数据建模是分布式行情集群能否稳定运行的地基。ClickHouse 擅长高吞吐写入与大规模聚合查询,但前提是表结构、分区键、排序键与数据生命周期策略与行情数据的真实访问模式匹配。本节围绕四类核心行情数据展开:K线、逐笔成交、快照与盘口,先分析它们的时序特征,再落到 ClickHouse 的建表参数与生命周期管理。

一、四类行情数据的结构与特征

行情数据本质上都是带时间戳的时序数据,但不同数据的写入频率、查询模式和保留价值差异极大,不能套用同一张表。

  • K线(Bar):按固定周期(1s、1m、1h、1d)聚合,字段包括开盘价、最高价、最低价、收盘价、成交量、成交额。写入频率低,查询以区间扫描和降采样为主,历史价值高,适合长期保留。
  • 逐笔成交(Trade/Tick):每笔成交一条记录,包含成交价、成交量、买卖方向、成交编号。写入量最大,是回测与微观结构分析的核心,通常只保留数周至数月。
  • 快照(Snapshot):按固定间隔(如 3s、500ms)采集的全市场或单标的切片,包含最新价、累计成交量、涨跌幅等。写入频率中等,用于行情回放与状态重建。
  • 盘口(OrderBook):快照中买卖各 N 档的价量数组,结构为嵌套数组或扁平化档位字段。查询常需展开档位,对列式存储的嵌套类型支持要求高。

从时序特征看,逐笔与快照是高频追加写,K线是低频聚合写,盘口是定长数组写。这决定了它们在分区粒度、排序键和 TTL 上的不同选择。

行情数据建模:写入频率 × 保留价值 × 查询模式 K线 Bar 低频聚合写 区间扫描 / 降采样 保留:长期 逐笔成交 Trade 高频追加写 回测 / 微观分析 保留:数周~数月 快照 Snapshot 中频定长写 回放 / 状态重建 保留:数周 盘口 OrderBook 定长数组写 档位展开查询 保留:数天~数周 ClickHouse 建模决策 分区键:toYYYYMMDD(ts) 或 toStartOfHour(ts),按写入量选择粒度 排序键:高频查询维度前置,如 (symbol, ts) 或 (ts, symbol) TTL:按数据价值分层,逐笔短、K线长,配合 TO DISK/VOLUME 冷热分层 引擎:MergeTree 家族,盘口用 Nested/Array 列,K线用 AggregatingMergeTree 预聚合

二、分区键与排序键设计方法

分区键决定数据在磁盘上的物理切分,排序键决定 MergeTree 内部的有序性和主键索引。两者共同影响写入放大、查询裁剪和合并压力。

  1. 分区粒度:逐笔数据量大,建议按天分区 toYYYYMMDD(ts);若单日写入超过数亿行,可细化为小时 toStartOfHour(ts)。K线数据量小,按月分区即可。
  2. 排序键顺序:把等值过滤字段放前面,时间字段放后面。例如 ORDER BY (symbol, ts) 适合按标的查询;若查询以时间范围为主,则用 ORDER BY (ts, symbol)。
  3. 主键与排序键分离:ClickHouse 中 PRIMARY KEY 必须是 ORDER BY 的前缀。若排序键为 (symbol, ts),主键可设为 (symbol, ts) 或仅 symbol,视索引粒度而定。
  4. 避免高基数分区键:不要用 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 中,分布式行情集群才能在写入吞吐与查询延迟之间取得平衡。