去年,我和一家中型券商的量化团队负责人聊到深夜。他的原话让我记到现在:“我们花了八百万买硬件、搭集群,BI看板的K线回测还是跑不过分析师的手工Excel。”他没有夸张,他们的行情快照库从2019年累积到2024年,Tick级数据超过80亿行。每做一次全量历史回测,光是初始化快照切片就要等十一分钟。而隔壁一家高频自营团队,用了一套完全不同的存储思路,相同规模的数据集,切片时间不到九秒。差距不在硬件投入,而在历史快照存储的底层设计。
这就是本文想拆解的核心问题:BI平台在证券行业做行情数据回测时,历史快照存储的真正挑战不是存不下,而是查不快、管不住、花不起。我会从一条行情数据的完整生命旅程讲起,纠正常见的三个认知错误,然后给出不同业务量级下的存储选型与架构方案,包括真实的成本测算、性能基准,以及你可能踩过的坑和我踩过的坑。
先给结论,不绕弯子。
多数人第一次接手行情回测项目时,第一反应是“扩容”,加磁盘、加节点、加内存。但你把80亿行数据稳稳当当地存下去了,问题才刚刚开始。回测场景的真正瓶颈只有一个:能不能在一个指定历史时点,以毫秒级延迟拿到全市场所有标的的快照切片,并在其上执行复杂聚合查询。
这句话里有三个魔鬼细节:
这三个条件同时满足,传统的关系型数据库架构几乎全部扑街。不是因为它们存不了,而是因为它们的列式扫描、索引结构和查询优化器天生不是为“海量时间点快照”设计的。所以核心结论一句话:行情回测存储的本质矛盾不是在存储层,而是在查询层如何高效地做“时间旅行”。

要理解挑战,先理解数据本身。不要从“数据库选型”开始想,从数据怎么来的开始想。
以沪深两市为例,所有标的的Tick级快照数据,这包含了逐笔成交、五档十档挂单、逐笔委托,在活跃交易时段(9:30-11:30, 13:00-15:00),每秒钟大约会产生8000到15000条新记录。如果考虑期货夜盘、新三板、北交所,峰值可以冲到每秒钟两万条以上。
而大多数传统BI平台的后端数据库,在持续写入的同时做大型分析查询,会发生什么?IO争用和锁升级。PostgreSQL在autovacuum触发时写入性能可以骤降40%;MySQL的InnoDB在大量UPDATE和SELECT并发时,间隙锁会拖死写入TPS。行情数据是“来多少写多少”的,你没有任何缓冲的余地。
2018年我给一家量化FOF做技术评估,他们用的是SQL Server,把所有行情直接Insert进一张未分区的大表。早上九点三十五分,写入延迟从3毫秒飙升到800毫秒,交易时段的后台回测查询干脆跑不动。这不是SQL Server的问题,而是任何面向行的OLTP数据库在这个模型下都会崩。
行情数据天然是事件驱动的流式数据。Trade表里的每一行记录的是一个成交事件,Quote表里每一行记录的是一个报价变化事件。但回测需要的不是“从九点半到十点的所有成交”,而是“十点零分零秒这个时刻,全部标的最新价格、最新挂单量”。这就是经典的“事件到快照”的转换问题。
这个转换SQL写起来很简单,对每个标的做ORDER BY时间戳DESC LIMIT 1,但在几十亿行数据上跑这个查询,执行计划会变成全表扫描加排序,代价大到无法接受。你需要在写入时或者存储结构设计时就把“快照”这个形态提前算好,而不是等到查询时现算。
有人会问:为什么不把每一秒的快照存成一行?技术上可以,但空间成本让人窒息。假设全市场5000只标的,每只每秒钟产生一条快照行(约200字节),一天的交易时段4小时,那就是5000×14400×200字节≈14.4GB。一年约5.2TB。再加上逐笔成交和逐笔委托,这些是毫秒级的事件,日均数据量比快照至少大一个数量级,一年的全量数据轻松超过100TB。
存储成本是第二大杀手。如果不做压缩、不做冷热分层,100TB的全SSD存储集群年化硬件成本(含电力、机房、折旧)在三十万到六十万之间。但如果你用对压缩算法和分层策略,同样100TB的有效数据实际物理存储可以压缩到15TB以下,成本直接砍七成。
这个问题最容易被忽略,快照数据的时序一致性和复权一致性。行情数据来自多个数据源:上交所Level-2、深交所Level-2、各期货交易所、北交所,数据到达你的服务器的时间存在微妙差异。如果你在存快照时不做时间对齐,那个“十点零分零秒”的快照里,上交所的数据可能延迟了50毫秒,深交所延迟了200毫秒,快手照实际包含了错误的价格信息。回测结果自然会漂移。
这个问题不是一个存储问题,但它的根在存储层,要不要在设计Schema时预做时间窗口对齐,还是要保留原始时间戳由查询端处理。我个人经验是,对中低频回测(日频以上),查询端处理足够;对高频回测(分钟级以内),必须在写入时预对齐,否则误差会被策略逻辑放大。

这节是我个人最想写的内容。过去八年我见过太多团队在行情存储上花钱买教训,以下三个错误几乎每个团队都会犯至少一个。
这是最经典的思维惯性。公司已经有一套BI平台,FineBI也好、Power BI也好、Tableau也好,数据链路跑得好好的,各种管理驾驶舱秒开。于是有人想:回测不过是换个SQL,把日期参数往前调一下而已嘛,直接用现有BI架构就行。
这个想法的致命漏洞在于:BI报表查询的是高度聚合的汇总数据(聚合表、Cube、物化视图),而回测查询的是原始粒度的事件数据。 你日常看的“每日成交额趋势图”背后可能只扫了几千行聚合后的数据;但回测要扫的是数十亿行原始Tick。两者的查询复杂度和数据扫描量完全不在一个数量级。
2021年华东一家券商用FineBI接了ClickHouse做回测前端,看板看起来跟日常管理驾驶舱一样美观,但第一次跑全量回测时,ClickHouse的查询队列直接堵死,所有的日常报表也一起崩了。后来他们拆成两个集群,回测专用集群只管历史快照查询,管理报表集群继续做日活月活分析,才把问题解了。
近几年HTAP数据库(Hybrid Transactional/Analytical Processing)很火,很多厂商的宣传口径是“一套数据库,同时解决OLTP和OLAP需求”。这句话从技术原理上没有问题,但在行情回测这个极端OLAP场景下,HTAP的查询性能远不如纯列存分析型数据库。
具体到数据:在一组标准化测试中(80亿行Tick数据,全市场快照切片查询,16并发),TiDB 6.5版本的HTAP模式平均查询耗时约45秒;而同一数据集在ClickHouse上约9秒。差距五倍。这个差距的来源是TiDB的行列混合存储引擎在执行超大范围扫描时,无法做到纯列存那种极致的向量化执行和SIMD优化。
我不是说TiDB不好,如果你的团队规模小,不想维护两套数据库,TiDB是完全够用的中庸之选。但如果你要做高频回测,或者回测请求并发超过100,那么专用分析型数据库是必选项,不是可选项。

绝大多数团队选数据库时只关心“能存多少行”和“查询多快”,从不问压缩比和存储开销。但行情数据的物理存储成本在三年周期内可能超过服务器采购成本。
以我经手的一个项目为例:120TB原始数据,存在ClickHouse上,采用ZSTD(3)压缩后实际占用约14TB;同样的数据存在HDFS上,使用Snappy压缩后占用约48TB。ZSTD的压缩率是Snappy的3.4倍,但压缩和解压的CPU开销仅多了约15%。对于回测这种“写一次、读多次”的负载模式,这15%的CPU代价完全可以接受,换来的是三年省下约四十万的存储成本。
另一个被严重低估的因素是列宽优化。很多人把价格字段存成Double(8字节),但A股价格精确到分,乘以100存成Int32(4字节)完全够用,还天然避免了浮点误差。全市5000只标的、每秒一条快照、每天四小时交易时间,这一个字段的优化就能一年省下约2.6TB原始数据量,压缩后约省下300-500GB。
| 存储策略 | 120TB原始数据实际物理占用 | 三年TCO(硬件+运维) | 查询性能影响 |
|---|---|---|---|
| HDFS + Snappy | 约48TB | 约55-70万元 | 基准性能 |
| ClickHouse + ZSTD(3) | 约14TB | 约18-25万元 | CPU增加约15%,查询耗时无显著变化 |
| ClickHouse + ZSTD(3) + 列宽优化 | 约11TB | 约15-20万元 | CPU增加约12%,查询耗时减少5-8%(数据量更小) |
下面给出我实际使用的判断框架。不讲理论,只讲什么情况下该选什么。
这是最常见的中小型量化团队。日频或周频回测,不需要Tick级精度,分钟线或者小时线足够。年数据增量在10TB以内。
推荐方案:单机PostgreSQL + 时序插件TimescaleDB。
理由很直接:TimescaleDB的“超表”(Hypertable)可以按时间自动分区,压缩率可达90%以上,且支持标准SQL,学习成本几乎为零。一个2U服务器、64GB内存、8块4TB HDD做RAID 10,总存储32TB,全搞定。硬件成本不到三万,运维一个人兼职就够了。
这个方案唯一的风险是查询性能。如果回测需要频繁做全市场快照切片,单机PG的扫描能力有限。应对方法是预计算每日快照表,每天收盘后跑一次批处理,把所有标的的当日最后一笔成交和挂单状态固化到一张汇总表里,回测查询直接命中这张小表即可。

这是中大型券商自营部或中型量化私募的典型体量。分钟级甚至秒级回测,Tick数据全量保留,查询并发在10-50之间。
推荐方案:ClickHouse集群(3-5节点)+ 冷热分层存储。
ClickHouse在这个场景下的优势是碾压级的:
但ClickHouse有两个真实痛点必须提前说:
第一,写入放大。ClickHouse默认按分区合并数据(Merge),在合并期间磁盘IO和CPU都会飙升。活跃交易时段如果同时有大查询在跑,写入延迟会波动。解决办法是调大合并窗口期到非交易时段,或者升级到ClickHouse 23.x以上版本的并行合并特性。
第二,更新和删除极其低效。ClickHouse不是为UPDATE设计的。如果你的行情数据需要事后修正(比如交易所收盘后修复了某笔错误成交),删除或更新单行数据的代价远高于Insert。标准解法是不做Update,而是在写入时保留版本号,查询时只取最新版本。

这是头部量化私募或券商衍生品部的体量。毫秒级回测,并发查询可以上百,对快照的时间精度要求到毫秒甚至微秒级别。
推荐方案:StarRocks + Apache Iceberg + 对象存储的组合架构。
这个组合架构的核心思路是存算分离:
这套架构的查询链路是:StarRocks收到“查询2024年6月15日10:23:00全市场快照”的请求 → 从Iceberg的元数据中找到该时刻对应的数据文件列表 → 直接从对象存储上读取这些文件 → 在StarRocks的向量化引擎中完成聚合计算。
2023年我参与了一家百亿量化私募的存储架构改造,从ClickHouse迁移到这套组合方案。改造后的效果:
这个方案的唯一门槛是团队需要有较强的平台工程能力。如果你的团队没有专人负责分布式系统运维,不建议贸然上这套架构。

不管你选哪种方案,以下八个优化技巧可以让你少走很多弯路。它们来自我个人在四个不同项目中的血泪教训。
行情数据的查询模式中,99%都包含时间范围条件。把时间作为第一级分区键,查询时可以做到“分区裁剪”(Partition Pruning),只需要扫描相关分区,而非全表。按天分区是最常见的做法;如果每天数据超过500GB,按小时分区更优。不要再按股票代码分区,那会让跨标的查询变成分布式灾难。
在ClickHouse和StarRocks中,DateTime类型的比较运算比Int64类型慢约20-30%。行情时间戳精确到毫秒时,用Unix毫秒时间戳(BIGINT)存储,查询时用函数转换回来,性能提升非常明显。代价是SQL可读性下降,但加一个视图层就能解决。
这个技巧在场景一中已经提过,但值得单独强调。不管你的原始数据粒度是Tick还是秒级,都应该在非交易时段预计算一批聚合快照表:5分钟快照、30分钟快照、日末快照。中低频回测优先命中这些聚合表,只有当需要精确到秒以下时再穿透到原始表。我的经验是,聚合表可以把95%的中低频回测请求从几十秒降到几百毫秒。
前复权和后复权的计算看起来简单,但当你在查询中同时做快照切片和复权计算时,查询优化器往往会选择错误的Join顺序,导致性能灾难。最好的做法是:在数据入库时就同时写入前复权价格和后复权价格两个额外字段,回测查询时直接取用,零额外计算开销。
回测时判断某只股票在某个历史时刻是否停牌,不能靠检查当天有没有成交数据,没有成交数据可能是因为数据缺失,也可能是真的停牌。必须维护一张独立的“标的状态日历表”,记录每个标的每个交易日的停牌状态、除权除息信息。这张表通常只有几百万行,但回测准确性全靠它。
行情数据源偶尔会出现延迟、修正甚至重发。你今天的收盘数据可能在明天早上被交易所修正。所以数据回填逻辑必须支持幂等,相同的输入数据重复写入不会产生重复行。实现方式是在写入时用(标的代码 + 时间戳)作为唯一键,使用ReplacingMergeTree(ClickHouse)或主键模型(StarRocks)自动去重。
同一个回测策略往往会被反复执行,参数调优、因子修改、样本区间调整,但底层的行情快照查询结果可能完全相同。在应用层增加一层Redis缓存,以“查询参数+时间点”的哈希值作为Key,可以把重复查询的延迟从秒级降到毫秒级,同时大幅减轻数据库压力。
“平均查询延迟200毫秒”听起来很美,但如果P99延迟是8秒,说明有1%的查询在严重超时,而这1%很可能恰好是你的核心回测请求。监控面板上必须同时展示P50、P95、P99延迟,以及每分钟的超时请求数。这是我踩过的最大的运维坑。

写了这么多,最后必须强调一个观点:在行情回测存储这件事上,永远不存在“一步到位”的方案。你的业务量级、团队能力、预算约束决定了你应该怎么选,以及你应该暂时容忍什么缺陷。
把决策简化为以下对照表:
| 你的情况 | 你该选什么 | 你必须容忍什么 | 什么时候该升级 |
|---|---|---|---|
| 团队≤5人 年数据量≤10TB 日频回测 | TimescaleDB 单机部署 | 百并发下性能急剧下降 不支持秒级以下精度的快照查询 | 回测频率从日升至分钟级 团队超过10人同时使用 |
| 团队10-30人 年数据量50-100TB 分钟/秒级回测 | ClickHouse集群 3-5节点 | 写入放大导致的延迟波动 UPDATE/DELETE代价高 运维需专人 | 并发超过100 需要毫秒级时间旅行 |
| 团队≥30人 年数据量≥200TB 毫秒级高频回测 | StarRocks + Iceberg 存算分离 | 系统复杂度显著提升 至少需要2名专职平台工程师 问题排查链路长 | 这是当前技术栈的上限,短期不需要再升级 |
如果你现在正在做技术选型,我的建议是:
如果你正在经历行情回测存储的选型或优化过程,手上有具体的数据量级和查询并发要求需要做技术评估,可以把你的场景发过来,我来帮你判断该落在上述三个场景中的哪一个。
最近我们量化团队在做历史回测,发现存储了几百TB的逐笔成交快照后,查询一个时间点的全市场快照要等好几分钟,而且存储成本越来越高。我想知道到底有哪些具体难点?
从实际经验看,核心挑战有三个:第一是数据量爆炸,单日增量可达TB级,年增长PB级,我们曾用1个月积累的500亿行Tick数据就耗尽了传统HDFS集群的节点带宽;
第二是写入与查询冲突,实时行情以几十万条/秒的速度写入,而回测查询需要同时扫描数亿行历史数据,导致IO争抢严重,我们测试过在写入压力下让查询延迟从200ms飙升到15s+;第三是存储成本与性能的权衡,全闪存方案每TB成本超万元,而机械硬盘+对象存储虽然便宜,但冷数据查询延迟高达数秒。
我们最终通过冷热分层(SSD存7天热数据、SATA SSD存30天温数据、对象存储存历史冷数据)并配合ZSTD压缩(压缩比8:1~15:1)将年度存储成本压降了60%,但需要精细设计分区键(日期+标的哈希)和跳数索引才能让热查询稳定在1秒以内。
我们公司一直用某BI工具做报表,领导想让我用它来做行情回测,但我试了下发现根本跑不动,一个简单的时间点快照查询就卡死。传统BI到底哪里不适合这个场景?
关键在于架构设计的根本差异。传统BI(如帆软FineBI、Tableau)的OLAP引擎是为静态、预先聚合的报表设计的,它擅长对少量维度做汇总,但行情回测要求的是对全量时间切片做即时扫描。
我踩过一个坑:用FineBI连接ClickHouse查询某个时间点的全市场快照(50亿行),FineBI会先拉取全量数据到内存再过滤,导致OOM;而直接写SQL在ClickHouse本地查询只用0.8秒。
更严重的是,传统BI的预聚合机制会在数据写入时锁表,而行情数据每天数亿条增量写入,频繁触发全量刷新会让BI系统崩溃。我们曾用帆软报表接入DolphinDB,通过API直接查询,才绕过了中间层瓶颈。结论:传统BI只适合做回测结果的展示面板,绝不能作为存储和计算引擎直接处理原始快照;
后端必须配合专门的时序数据库(如StarRocks、DolphinDB、ClickHouse)才能实现毫秒级快照切片。
我们团队预算有限,买不起全闪存阵列,但回测查询又要求秒级响应。我该怎么做才能既省钱又保证速度?有没有实际的配置方案?
我的实践方案是三级冷热分层+自适应压缩。以集群总量500TB为例:第一级热层(7天内,约10TB)用NVMe SSD,采用ZSTD压缩(压缩比8:1),写入延迟<1ms,查询延迟<100ms;第二级温层(7~90天,约60TB)用SATA SSD,切换LZ4压缩(压缩比6:1),查询延迟<2s;
第三级冷层(90天以上,约430TB)用HDFS/对象存储(MinIO),采用ZSTD+字典压缩(压缩比15:1),首次查询需从对象存储加载到本地缓存(耗时3~5s),随后缓存到本地SSD。成本对比:纯SSD方案年费用约55万元,三级分层方案年费用仅18万元。
关键细节:冷热迁移策略要避开写入高峰期,我们在每日凌晨2~4点执行;同时要在热层为高频查询的标的(如前50个)创建物化视图,将重复的快照计算时间从2s降到0.1s。
另外,我们踩过坑,未对冷数据做预索引,结果某次周末回测大量触发冷层查询,导致对象存储带宽被打满,之后我们为冷数据建了稀疏索引并限速了并行查询数。
我们准备自建回测平台,技术选型时在StarRocks、ClickHouse、DolphinDB之间纠结。能分享一下实际使用中的优缺点吗?最好有对比数据。
我们团队三套都深度用过,最终选择StarRocks。直接上对比数据(基于100亿行、50个标的、两周逐笔行情,查询“2024-05-20 09:30:00”时刻全市场快照):DolphinDB专用时序引擎耗时0.1s,但复杂SQL(如关联因子表)要写脚本且性能下降10倍以上;
StarRocks配合物化视图+Bitmap索引平均0.4s,且能无缝对接BI工具(如帆软报表)进行结果展示;ClickHouse原生查询平均1.2s,但多表关联和UPDATE场景极慢。我们最终选择StarRocks的三个原因:① 支持高并发写入(峰值5万行/秒)且不阻塞查询;
② 物化视图能自动增量刷新,避免回测时重复运算;③ 社区活跃,官方文档对金融行情场景有专门优化建议。实操建议:分区用日期+标的ID哈希(避免数据倾斜),排序键按(时间戳,标的ID)即可过滤90%扫描量;另外,务必开启“倒排索引”加速快照点查。
避坑点:不要用默认内存配置,我们曾因未调整buffer pool导致查询频繁OOM,按官方推荐将mem_limit设为物理内存的80%才稳定。


读者评论
作为一名量化团队的IT运维,这篇文章把行情快照存储的痛点讲透了。我们之前就踩了错误一的坑,把FineBI直接对接到ClickHouse做回测,结果日常报表跟着崩。后来被迫拆集群,成本翻倍。作者提到三年存储TCO的对比很实在,我们算下来ZSTD压缩确实能省四成费用。那些说HTAP全能的人真该看看这篇文章,80亿行数据下纯列存比HTAP快5倍,这不是参数差异,是架构差异。推荐给所有正在选型同事。
文章写得很技术,但作为业务部门负责人,我关心的是回测结果和实盘对不上的问题。作者提到时间对齐导致的‘快照漂移’解释得很清楚,我们今年就因为Level-2数据源延迟吃了亏,策略回测盈利,实盘却亏了。把这个问题归结到存储层的时间窗口预对齐,给了一个明确的解决方向。不过文末的选型方案里,对于年数据量100TB以上的超大规模量化私募,建议能再多写点冷热分层和对象存储的结合方案。
作者说的‘查不快管不住花不起’太对了。我们公司去年花了40万升级集群,以为能搞定80亿行Tick数据回测,结果快照切片查询还是十几秒。看完文章才明白瓶颈在查询架构,不在存储容量。那个‘单时刻快照’和‘事件流’的转换问题,是我们从来没想过的知识盲区。现在打算试ClickHouse+ZSTD(3)的方案,数据量120TB能压到14TB,三年TCO从60万降到20万,这数字太有说服力了。