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数据存储知识体系。你可以把它当作一个「决策地图」:
4.5 实战建议
最后,给你三个可以直接用的建议:
- 先小后大:别一上来就存全市场Tick。先存几只股票,跑通流程,再逐步扩展。我见过太多人一上来就搞全量,结果存储爆了,查询也慢,最后推倒重来。
- 分区要合理:按天分区是最常见的做法。如果你数据量特别大(比如全市场),可以考虑按小时分区。但分区太多也会影响查询性能,这个平衡点需要自己试。
- 定期清理:Tick数据是有时效性的。超过3个月的历史数据,如果不需要做回测,建议归档到冷存储。ClickHouse支持TTL自动删除,非常方便。
一个小技巧:用ClickHouse的system.parts表可以查看每个分区的数据量和压缩比。我每周都会跑一次这个查询,看看哪些分区需要优化。
好了,Tick数据存储这块,核心就是「选对库、摆好结构、压紧数据」。下一节我们聊聊数据清洗——嗯,那才是真正让人头秃的部分。