2. 金融数据特征与列式存储基础

在金融数据工程中,理解数据的“形状”比理解数据的“数量”更重要。行情快照、订单簿、逐笔成交、基本面因子,这些数据在时间维度、字段维度、更新频率和查询模式上差异巨大。如果沿用传统行式存储的思路,很容易在回测、风控和实时监控场景中遇到性能瓶颈。本节从金融数据的类型特征出发,逐步引出列式存储的核心原理与优势。

一、金融数据的四种典型形态

金融数据并非单一结构,按业务语义大致可分为四类:

  • 时间序列数据:如股票日线、分钟线、利率曲线、宏观指标。特点是按时间戳严格递增,字段相对固定,查询常按时间区间过滤并做聚合。
  • 行情快照数据:如Level-1/Level-2快照,包含最新价、买卖一档、成交量、成交额等。特点是高频、宽表、字段多但每行更新频繁。
  • 订单簿数据:如逐笔委托、逐笔成交、十档买卖盘。特点是事件驱动、非均匀时间间隔、买卖方向与价格档位形成嵌套结构。
  • 基本面数据:如财报科目、估值指标、行业分类。特点是低频、稀疏、维度多,常需要按报告期对齐和跨表关联。

这四类数据在写入模式上也有明显差异:时间序列和基本面多为批量追加或按报告期覆盖;行情快照和订单簿则是持续流式写入,且对乱序容忍度低。理解这些差异,是选择存储格式和分区策略的前提。

二、列式存储的核心原理

行式存储将一行记录的所有字段连续存放,适合“按行读取整条记录”的OLTP场景。列式存储则把同一列的值连续存放,形成列块(Column Chunk)。这种布局带来三个关键机制:

  1. 按列裁剪:查询只涉及少数列时,无需读取其他列的数据页,I/O量大幅下降。
  2. 同质编码:同一列数据类型一致,便于使用字典编码、游程编码、Delta编码等压缩手段,压缩比通常可达5~20倍。
  3. 向量化执行:列块可批量加载到CPU缓存,配合SIMD指令做过滤和聚合,减少函数调用开销。

在Parquet中,一个列块由页(Page)组成,页内保存编码后的值和统计信息(min/max、null count)。查询引擎利用这些统计信息跳过不满足谓词的页,这就是谓词下推(Predicate Pushdown)的物理基础。

Arrow则更进一步,它定义的是内存中的列式布局,强调零拷贝和跨语言互操作。Parquet负责磁盘上的高效压缩与编码,Arrow负责内存中的高效计算与传输,两者常配合使用:Parquet读入后转为Arrow RecordBatch,再交给计算引擎处理。

金融数据列式存储:列裁剪与谓词下推 行式存储(逐行读取) 2024-01-02 | 000001 | 10.2 | 10.5 2024-01-03 | 000001 | 10.3 | 10.6 2024-01-04 | 000001 | 10.1 | 10.4 2024-01-05 | 000001 | 10.6 | 10.9 2024-01-08 | 000001 | 10.8 | 11.0 查询收盘价仍需读取全部字段 I/O 放大明显 列式存储(按列读取) 日期列 代码列 收盘价列 只读收盘价列,I/O 降低约 2/3 同列同质,压缩比更高 查询执行路径 SQL / DataFrame 查询 列裁剪 + 谓词下推 Parquet 页级过滤 Arrow 向量化计算

三、方法流程:从原始行情到列式数据集

以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进行高效数据工程的基础。列式存储不是银弹,但在“宽表、少列查询、高压缩、批量聚合”的金融分析场景中,它几乎是默认答案。