4. 行情建模原则

行情数据建模不是“把字段塞进一张表”这么简单。它同时受三股力量拉扯:写入端每秒可能几十万行,查询端又要求毫秒级聚合;业务上既要按标的看最新盘口,又要按时间窗口做K线、VWAP、波动率。建模的目标,是在ClickHouse的列式存储与MergeTree引擎约束下,找到写入吞吐、查询延迟、存储成本三者的平衡点。

一、核心原理:宽表与窄表的取舍

宽表把一次行情快照的所有字段放在同一行,例如 symbol, ts, bid1, ask1, bid1_vol, ask1_vol, last, volume, turnover。窄表则把每个指标拆成独立行,用 metric_name, metric_value 表达。两者在ClickHouse中的表现差异极大。

  • 宽表优势:一次写入即完整快照,查询最新盘口无需聚合,列裁剪效率高,适合 ReplacingMergeTree 或 ReplacingMergeTree 去重。
  • 宽表代价:字段多、稀疏字段多时压缩率下降;新增指标需改表结构。
  • 窄表优势:扩展灵活,新增指标无需DDL;适合指标种类多、写入频率不一致的场景。
  • 窄表代价:查询需按 metric_name 过滤并做行转列,聚合开销大,高频写入时行数膨胀严重。
行情场景中,盘口快照、逐笔成交、K线三类数据应分别建模,不要强行统一为一张窄表。盘口和逐笔天然是宽表结构,K线是时间序列聚合结果。

二、维度建模与时间序列建模的融合

维度建模强调事实表与维度表分离。行情中的事实是“某时刻某标的的行情快照”,维度包括标的、交易所、品种、交易时段。ClickHouse没有传统外键,但可以用低基数维度内联的方式:把 exchange、product 等枚举字段直接放入事实表,用 LowCardinality(String) 编码,既保留维度语义,又避免Join开销。

时间序列建模的关键是时间分区与排序键。行情表通常按天分区,排序键为 (symbol, ts) 或 (exchange, symbol, ts)。这样按标的查最新值、按时间范围扫描都能命中主键索引。对于高频写入,分区粒度不宜过细,否则小分区过多会拖慢合并。

CREATE TABLE market.quote_snapshot
(
    exchange   LowCardinality(String),
    symbol     LowCardinality(String),
    ts         DateTime64(3, 'Asia/Shanghai'),
    bid1       Decimal(18, 4),
    ask1       Decimal(18, 4),
    bid1_vol   UInt64,
    ask1_vol   UInt64,
    last       Decimal(18, 4),
    volume     UInt64,
    turnover   Decimal(24, 4)
)
ENGINE = ReplacingMergeTree
PARTITION BY toDate(ts)
ORDER BY (exchange, symbol, ts)
TTL ts + INTERVAL 30 DAY;

三、高频写入与查询平衡的工程手段

写入侧要控制单批次行数与分区数。ClickHouse最佳实践是每批写入10万~100万行,批次过小会产生大量小part,触发频繁合并。查询侧要利用物化视图预聚合,把K线、VWAP等重查询从原始表剥离。

维度宽表方案窄表方案推荐场景
写入吞吐高,单行完整低,行数膨胀盘口/逐笔用宽表
查询最新值直接取行,快需过滤聚合,慢宽表
指标扩展需DDL灵活窄表
压缩率列式压缩好重复维度多,压缩一般宽表
典型引擎ReplacingMergeTreeMergeTree + 物化视图按数据形态选择
常见误区:用窄表存盘口,再用 GROUP BY symbol, ts 行转列。这在每秒百万行写入下会导致查询延迟飙升,且无法利用主键索引。盘口必须宽表化。

四、案例复盘:从窄表迁移到宽表+物化视图

某行情平台最初用窄表 quote_metric(ts, symbol, metric, value) 存储所有指标,写入峰值80万行/秒,查询最新盘口需 WHERE metric IN (...) 再聚合,P99延迟超过3秒。迁移方案如下:

  1. 盘口与逐笔改为宽表,按 (exchange, symbol, ts) 排序,写入批次控制在50万行。
  2. K线、VWAP、成交量等聚合指标用物化视图从宽表实时计算,写入 AggregatingMergeTree 目标表。
  3. 查询最新盘口直接 SELECT ... ORDER BY ts DESC LIMIT 1,命中主键。
  4. 历史指标查询走物化视图结果表,避免扫描原始明细。
CREATE MATERIALIZED VIEW market.mv_kline_1m
ENGINE = AggregatingMergeTree
PARTITION BY toDate(ts)
ORDER BY (symbol, ts)
AS SELECT
    symbol,
    toStartOfMinute(ts) AS ts,
    argMinState(last, ts) AS open,
    maxState(last) AS high,
    minState(last) AS low,
    argMaxState(last, ts) AS close,
    sumState(volume) AS volume
FROM market.quote_snapshot
GROUP BY symbol, ts;

迁移后写入吞吐提升约40%,最新盘口查询P99降至20毫秒以内,K线查询从秒级降到百毫秒级。核心经验是:原始明细用宽表保真,聚合结果用物化视图预计算,维度用LowCardinality内联,时间用分区+排序键裁剪。建模不是一次性的,需随写入量和查询模式持续调优。

行情建模:写入端与查询端的平衡结构 写入端 盘口/逐笔 宽表 批次 10万~100万行 ReplacingMergeTree 存储层 按天分区 ORDER BY (exchange,symbol,ts) LowCardinality维度 查询端 最新盘口 主键直取 K线/VWAP 物化视图 AggregatingMergeTree 平衡原则 宽表保真原始明细 · 物化视图预聚合 · 分区裁剪时间 · 排序键命中查询 · 批次控制合并压力 误区:窄表存盘口 行转列聚合,延迟飙升 误区:小批次写入 小part过多,合并风暴 正解:分层建模 明细宽表+聚合视图