2. 金融数据特征与列式存储基础
在金融数据工程中,理解数据的“形状”比理解数据的“数量”更重要。行情快照、订单簿、逐笔成交、基本面因子,这些数据在时间维度、字段维度、更新频率和查询模式上差异巨大。如果沿用传统行式存储的思路,很容易在回测、风控和实时监控场景中遇到性能瓶颈。本节从金融数据的类型特征出发,逐步引出列式存储的核心原理与优势。
一、金融数据的四种典型形态
金融数据并非单一结构,按业务语义大致可分为四类:
- 时间序列数据:如股票日线、分钟线、利率曲线、宏观指标。特点是按时间戳严格递增,字段相对固定,查询常按时间区间过滤并做聚合。
- 行情快照数据:如Level-1/Level-2快照,包含最新价、买卖一档、成交量、成交额等。特点是高频、宽表、字段多但每行更新频繁。
- 订单簿数据:如逐笔委托、逐笔成交、十档买卖盘。特点是事件驱动、非均匀时间间隔、买卖方向与价格档位形成嵌套结构。
- 基本面数据:如财报科目、估值指标、行业分类。特点是低频、稀疏、维度多,常需要按报告期对齐和跨表关联。
这四类数据在写入模式上也有明显差异:时间序列和基本面多为批量追加或按报告期覆盖;行情快照和订单簿则是持续流式写入,且对乱序容忍度低。理解这些差异,是选择存储格式和分区策略的前提。
二、列式存储的核心原理
行式存储将一行记录的所有字段连续存放,适合“按行读取整条记录”的OLTP场景。列式存储则把同一列的值连续存放,形成列块(Column Chunk)。这种布局带来三个关键机制:
- 按列裁剪:查询只涉及少数列时,无需读取其他列的数据页,I/O量大幅下降。
- 同质编码:同一列数据类型一致,便于使用字典编码、游程编码、Delta编码等压缩手段,压缩比通常可达5~20倍。
- 向量化执行:列块可批量加载到CPU缓存,配合SIMD指令做过滤和聚合,减少函数调用开销。
在Parquet中,一个列块由页(Page)组成,页内保存编码后的值和统计信息(min/max、null count)。查询引擎利用这些统计信息跳过不满足谓词的页,这就是谓词下推(Predicate Pushdown)的物理基础。
Arrow则更进一步,它定义的是内存中的列式布局,强调零拷贝和跨语言互操作。Parquet负责磁盘上的高效压缩与编码,Arrow负责内存中的高效计算与传输,两者常配合使用:Parquet读入后转为Arrow RecordBatch,再交给计算引擎处理。
三、方法流程:从原始行情到列式数据集
以A股分钟行情为例,一个典型的列式数据工程流程如下:
原始行情源(CSV/API/消息队列)
↓ 解析与字段标准化
统一Schema:symbol, trade_date, trade_time, open, high, low, close, volume, amount
↓ 按日期分区 + 按symbol排序
写入Parquet:partition_by=["trade_date"],row_group_size=128MB
↓ 读取时列裁剪
df = pd.read_parquet("minute/", columns=["symbol","close","volume"])
↓ 转为Arrow Table
table = pa.Table.from_pandas(df)
↓ 向量化聚合
result = table.group_by("symbol").aggregate([("close","mean"),("volume","sum")])
这个流程中有两个关键决策点:分区键和行组大小。分区键决定目录裁剪效率,行组大小影响页级过滤和内存占用。对于分钟行情,按交易日分区通常比按月份分区更均衡;行组过小会导致元数据膨胀,过大则降低跳过效率。
四、案例与场景:订单簿快照的列式建模
订单簿快照包含买一至买十、卖一至卖十的价格和数量,共40个字段。若按行存储,每次查询“某只股票某时刻的买一价”都要读取整行。列式存储下,买一价、买一量各自成列,查询只触碰目标列。
| 数据形态 | 典型字段 | 更新频率 | 推荐存储策略 |
|---|---|---|---|
| 时间序列 | open/high/low/close/volume | 低频批量 | 按日期分区,Parquet+Snappy |
| 行情快照 | bid1~bid10, ask1~ask10 | 高频流式 | 按日期+标的二级分区,行组128MB |
| 订单簿事件 | order_id, price, qty, side | 事件驱动 | 按日期分区,Arrow内存表实时聚合 |
| 基本面数据 | report_date, roe, pe, pb | 低频稀疏 | 按报告期分区,字典编码压缩 |
对于订单簿这类嵌套结构,Parquet支持嵌套列(List/Struct),可将十档买卖盘建模为list<struct<price, qty>>,既保留列式裁剪能力,又避免字段爆炸。
五、常见误区与复盘
- 误区一:列式存储一定比行式快。如果查询需要返回整行且列数很少,行式反而更优。列式的优势建立在“只读部分列”和“同质压缩”之上。
- 误区二:分区越多越好。过多小文件会拖慢元数据读取和查询规划。建议单分区文件数控制在合理范围,必要时合并小文件。
- 误区三:忽略时间戳精度。金融数据常需毫秒甚至微秒精度,Parquet的TIMESTAMP逻辑类型需明确指定单位,否则跨系统读取时可能丢失精度。
- 误区四:直接对原始Parquet做高频点查。Parquet适合批量扫描,不适合单行随机读。高频点查应配合Arrow内存表或KV索引。
复盘建议:在项目初期就用真实查询模式验证列裁剪效果。记录每次查询实际读取的列数、行组数和字节数,用数据驱动分区与行组参数调优,而不是凭感觉设置。
掌握金融数据的形态差异与列式存储的物理原理,是后续使用Parquet和Arrow进行高效数据工程的基础。列式存储不是银弹,但在“宽表、少列查询、高压缩、批量聚合”的金融分析场景中,它几乎是默认答案。