4. Tick数据存储:数据库选型、存储结构设计与压缩策略

Tick数据有多「毒」?我打个比方你就懂了。

一只股票一天产生几十万笔逐笔成交,加上Level2的快照数据,一天下来轻松上千万条记录。你要是把沪深300全市场Tick都收下来,一天就是几十亿条。嗯,这数据量,普通的关系型数据库直接跪。

所以,选对存储方案,是Tick数据工程的第一步。这一步走错了,后面所有分析都是空中楼阁。

4.1 数据库选型:InfluxDB vs ClickHouse

我个人习惯把时序数据库分成两类:一类是「专为时序而生」的InfluxDB,另一类是「全能型分析引擎」ClickHouse。两者都能处理Tick数据,但侧重点完全不同。

特性 InfluxDB ClickHouse
写入性能 单机百万点/秒 单机50-200万行/秒
压缩比 约10:1 约5-15:1(列式存储优势)
查询灵活性 强在时间窗口聚合 强在复杂分析、JOIN
SQL兼容性 类SQL(InfluxQL/Flux) 标准SQL
运维复杂度 低,开箱即用 中,需要调优
典型场景 实时监控、简单聚合 历史分析、因子计算

我在项目中遇到过这样的坑:一开始用InfluxDB存Tick数据,查询单只股票某一天的逐笔成交很快,但一旦要做跨品种的因子计算,比如计算全市场资金流向,InfluxDB就力不从心了。后来切到ClickHouse,同样的查询快了10倍不止。

我的建议:

  • 如果你主要做实时监控简单报警,InfluxDB够用
  • 如果你要做历史回测复杂因子计算,ClickHouse是更好的选择
  • 两者可以共存:InfluxDB做热数据,ClickHouse做冷数据

4.2 存储结构设计

存储结构设计,说白了就是「怎么把Tick数据摆整齐」。摆得好,查询快;摆得乱,查询慢到怀疑人生。

4.2.1 逐笔成交表设计

CREATE TABLE tick_trades (
    symbol      String,       -- 股票代码
    trade_time  DateTime64(3),-- 成交时间(毫秒级)
    price       Float64,      -- 成交价
    volume      Int64,        -- 成交量
    amount      Float64,      -- 成交额
    trade_type  String,       -- 买卖方向(B/S/N)
    trade_index Int64,        -- 成交序号
    channel     String        -- 交易所通道
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(trade_time)
ORDER BY (symbol, trade_time);

这里有个关键点:排序键。我习惯把symbol放在第一位,trade_time放在第二位。为什么?因为绝大多数查询都是「查某只股票某段时间的数据」。这样设计,ClickHouse可以快速定位到对应的分区和索引块。

避坑指南:我曾经把trade_time放在排序键第一位,结果查询单只股票全天的数据时,ClickHouse需要扫描所有股票的时间范围,效率极低。改成(symbol, trade_time)后,查询速度提升了20倍。

4.2.2 Level2快照表设计

CREATE TABLE tick_snapshot (
    symbol      String,
    snap_time   DateTime64(3),
    bid_prices  Array(Float64),  -- 买一到买十价格
    bid_volumes Array(Int64),    -- 买一到买十数量
    ask_prices  Array(Float64),  -- 卖一到卖十价格
    ask_volumes Array(Int64),    -- 卖一到卖十数量
    total_bid   Float64,         -- 委买总量
    total_ask   Float64,         -- 委卖总量
    turnover    Float64,         -- 成交额
    volume      Int64            -- 成交量
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(snap_time)
ORDER BY (symbol, snap_time);

快照数据用Array类型存储买卖盘口,比拆成10个字段更优雅。你想想看,如果拆成bid_price1到bid_price10,查询时写SQL得多痛苦?

4.3 数据压缩策略

Tick数据压缩,我踩过的坑比走过的路还多。这里直接给你三个最实用的策略。

4.3.1 列式压缩

ClickHouse默认使用LZ4压缩,压缩比约5:1。如果你对存储空间敏感,可以换成ZSTD:

ALTER TABLE tick_trades MODIFY SETTING compression = 'zstd';

ZSTD压缩比能到8:1以上,但写入速度会慢20%左右。嗯,这是典型的空间换时间。

4.3.2 时间戳降精度

Tick数据的时间戳通常是毫秒级。如果你不需要微秒精度,可以存成秒级或毫秒级整数:

-- 毫秒级时间戳存储
trade_time_ms UInt64  -- 从1970-01-01开始的毫秒数

用整数存时间戳,比DateTime64节省4-8个字节。别小看这点空间,一天几十亿条数据,省下来的空间相当可观。

4.3.3 编码优化

对于价格、成交量这类数值字段,ClickHouse提供了多种编码方式:

  • Delta编码:适合连续递增的字段(如成交序号)
  • DoubleDelta编码:适合变化缓慢的字段(如价格)
  • Gorilla编码:适合浮点数(如成交额)
CREATE TABLE tick_trades (
    ...
    price Float64 CODEC(DoubleDelta, LZ4),
    volume Int64 CODEC(Delta, LZ4),
    ...
) ENGINE = MergeTree();

注意:编码不是万能的。如果数据本身随机性很强(比如高频的买卖方向),编码反而可能增加存储空间。建议先用小批量数据测试,再决定用哪种编码。

4.4 知识体系总览

下面这张图,是我自己梳理的Tick数据存储知识体系。你可以把它当作一个「决策地图」:

Tick数据存储知识体系 数据库选型 InfluxDB 实时监控 · 简单聚合 写入快 · 运维简单 ClickHouse 历史分析 · 因子计算 SQL灵活 · 压缩率高 存储结构设计 逐笔成交表 排序键: (symbol, trade_time) Level2快照表 Array类型存储盘口 数据压缩策略:列式压缩 · 时间戳降精度 · 编码优化

4.5 实战建议

最后,给你三个可以直接用的建议:

  1. 先小后大:别一上来就存全市场Tick。先存几只股票,跑通流程,再逐步扩展。我见过太多人一上来就搞全量,结果存储爆了,查询也慢,最后推倒重来。
  2. 分区要合理:按天分区是最常见的做法。如果你数据量特别大(比如全市场),可以考虑按小时分区。但分区太多也会影响查询性能,这个平衡点需要自己试。
  3. 定期清理:Tick数据是有时效性的。超过3个月的历史数据,如果不需要做回测,建议归档到冷存储。ClickHouse支持TTL自动删除,非常方便。

一个小技巧:用ClickHouse的system.parts表可以查看每个分区的数据量和压缩比。我每周都会跑一次这个查询,看看哪些分区需要优化。

好了,Tick数据存储这块,核心就是「选对库、摆好结构、压紧数据」。下一节我们聊聊数据清洗——嗯,那才是真正让人头秃的部分。

专注资料整理