2. ClickHouse 基础

行情数据具有写入吞吐高、查询维度多、时间范围敏感、历史数据体量大等特征。ClickHouse 之所以能在行情场景中承担实时入库与聚合分析,核心并不只是“快”,而是其列式存储、MergeTree 引擎族、分区、排序键、主键与稀疏索引共同构成的一套面向批量扫描与范围过滤的存储结构。理解这套结构,是后续建模与性能调优的前提。

一、列式存储:为什么适合行情数据

行式存储按整行连续存放,适合点查与事务更新;列式存储按列独立组织,同一列的数据类型一致、重复度高、压缩比好。行情表通常包含 symbol、exchange、ts、last_price、volume、bid1、ask1 等字段,查询往往只关心少数列,例如按时间范围统计某只股票的成交均价。列式存储只需读取 ts、symbol、last_price、volume 等列,避免把无关列全部读入。

更重要的是,列式存储让 ClickHouse 可以按列做向量化执行、按块压缩、按粒度跳过。行情数据中 symbol 基数有限、exchange 更少、ts 单调递增,这些特性使压缩与索引效率都很高。因此,列式存储不是单纯“少读列”,而是与排序、分区、索引协同后,形成“先定位数据块,再只读必要列”的扫描路径。

二、MergeTree 引擎族:行情表的主力引擎

ClickHouse 最常用的表引擎是 MergeTree 家族。它采用“写入即生成 part,后台异步合并”的 LSM 式思路:每次插入形成一个新的数据片段,后台按排序键合并成更大的 part。MergeTree 家族常见成员及适用场景如下:

引擎 核心能力 行情场景
MergeTree 基础排序、分区、主键索引 逐笔行情、快照行情原始表
ReplacingMergeTree 按排序键去重,保留版本最新 行情快照去重、状态修正
SummingMergeTree 合并时对数值列求和 成交量、成交额预聚合
AggregatingMergeTree 合并时按聚合函数状态合并 分钟 K 线、指标中间态
CollapsingMergeTree 按标记列折叠取消 需要更新语义的行情修正

选择引擎时,先问三个问题:是否需要去重?是否需要预聚合?是否需要支持更新/删除语义?如果只是原始行情落地,MergeTree 足够;如果快照可能重复,ReplacingMergeTree 更合适;如果要做分钟级聚合,AggregatingMergeTree 或 SummingMergeTree 能显著降低查询成本。

三、分区、排序键与主键:数据如何被定位

分区键决定数据在磁盘上的物理目录划分,常见做法是按天或按月分区,例如 toYYYYMMDD(ts)。分区裁剪能在查询条件命中分区键时直接跳过无关分区,这是行情查询中“按时间范围查”的第一层过滤。

排序键决定 part 内部数据的物理顺序,也决定主键索引的稀疏粒度。ClickHouse 的主键不是唯一约束,而是“索引标记”的依据。数据按排序键有序写入,每隔 index_granularity 行记录一个主键标记,查询时通过标记快速定位到可能包含目标数据的 granule,再读取对应列。排序键设计应遵循“高过滤性在前、范围查询在后”的原则。例如行情表常用:

ORDER BY (exchange, symbol, ts)

这样按交易所、代码、时间查询时,能快速缩小扫描范围。若把 ts 放在最前,则单代码时间范围查询仍可受益,但多代码等值过滤效率会下降。分区与排序键的关系可以概括为:分区负责“大块裁剪”,排序键负责“块内定位”,主键索引负责“快速跳读”。

行情表数据定位:分区 → 排序键 → 主键索引 分区 Partition 2025-01-02 2025-01-03 2025-01-04 按天分区,查询命中分区键可跳过无关目录 排序键 ORDER BY exchange = SSE symbol = 600519 ts 升序 part 内有序,决定 granule 排列 主键索引 Primary Key index_granularity = 8192 每 8192 行一个标记 稀疏索引,快速跳读 不唯一,只用于定位数据块

四、案例与误区复盘

假设要建一张逐笔行情表,常见建表语句如下:

CREATE TABLE market_tick
(
    exchange LowCardinality(String),
    symbol   LowCardinality(String),
    ts       DateTime64(3),
    last_price Decimal64(4),
    volume   UInt64
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (exchange, symbol, ts)
SETTINGS index_granularity = 8192;

这条语句中,PARTITION BY toYYYYMMDD(ts) 让按天查询可以裁剪分区;ORDER BY (exchange, symbol, ts) 让同一交易所、同一代码的数据在 part 内按时间有序;index_granularity = 8192 控制主键索引的稀疏程度。查询 WHERE exchange='SSE' AND symbol='600519' AND ts BETWEEN ... 时,ClickHouse 先裁分区,再用主键索引定位 granule,最后只读取所需列。

常见误区一:把主键当唯一约束。ClickHouse 主键不保证唯一,重复数据仍会写入。需要去重应使用 ReplacingMergeTree 或在查询层聚合。

常见误区二:分区键粒度过细。按秒或按代码分区会产生大量小 part,增加合并压力。行情表通常按天或按月分区即可。

常见误区三:排序键顺序随意。排序键顺序直接影响过滤效率。应把等值过滤、高基数列放在前面,范围查询列放在后面。

复盘方法:用 EXPLAIN indexes = 1 查看分区裁剪与主键索引命中情况;用 system.parts 观察 part 数量与分区分布;用 system.query_log 对比调整排序键前后的扫描行数。行情场景中,先保证分区与排序键匹配主要查询模式,再考虑预聚合引擎与物化视图。

综上,列式存储解决“读得少”,MergeTree 解决“写得快、合并得稳”,分区解决“大范围裁剪”,排序键与主键索引解决“块内快速定位”。这四者共同构成 ClickHouse 行情建模的底层骨架,也是后续入库与实时聚合实战的性能基础。