2. 逐笔行情数据解剖
要设计一张能承载全市场逐笔行情的分区表,第一步不是选分区键,而是把“一行逐笔数据到底长什么样”彻底拆开。逐笔行情与快照行情的本质差异在于:快照是某一时刻的截面状态,逐笔是事件流中的一条增量记录。它记录的是“发生了什么”,而不是“现在是什么”。因此字段结构、时间精度、序列特征和更新模式这四件事,直接决定了后续哪些字段能作为分区键候选、哪些字段必须进入排序键、哪些字段只能作为普通列。
一、逐笔成交与逐笔委托的字段结构差异
逐笔成交(Trade)与逐笔委托(Order)是两条独立的事件流,字段结构并不对称。逐笔成交的核心字段通常包括:证券代码、成交时间、成交编号、买方委托编号、卖方委托编号、成交价格、成交数量、成交金额、买卖方向标识(部分市场提供)、成交类型(如撤单成交、盘后成交)。逐笔委托的核心字段则包括:证券代码、委托时间、委托编号、委托价格、委托数量、买卖方向、委托类型(限价、市价、本方最优等)、订单状态(新增、撤单)。
这里有一个对分区设计极其关键的事实:逐笔成交没有“价格档位”概念,逐笔委托没有“成交金额”概念。两者共享的字段只有证券代码、时间、编号体系和方向。这意味着如果要把两类事件放进同一张分区表,必须做字段对齐——用事件类型字段区分,把各自独有字段置空,或者拆成两张结构不同的表再统一查询视图。前者写入简单但列稀疏,后者查询清晰但需要联合。这个取舍会直接影响分区键的选择范围。
二、时间精度:分区键的“分辨率”问题
逐笔行情的时间字段精度远高于快照。交易所对外发布的逐笔数据通常带毫秒级时间戳,部分市场提供微秒级,且同一毫秒内可能有多条事件。这里必须区分两个概念:事件时间(交易所撮合或委托进入系统的时间)与接收时间(本地行情网关落库的时间)。两者在正常网络下相差几毫秒到几十毫秒,但在行情高峰或网络抖动时会显著拉大。
对分区设计而言,时间精度决定了分区粒度。如果按“交易日”做一级分区,时间精度只影响分区内的排序键;如果按“小时”甚至“分钟”做分区,就必须保证事件时间与接收时间不会跨分区边界漂移,否则同一笔成交可能因接收延迟被写入错误的时间分区,造成查询遗漏。工程上的常见做法是:一级分区用交易日,二级分区用证券代码哈希或范围,事件时间只作为分区内排序键,从而规避时间漂移问题。这是一个典型的“精度越高、越不适合直接做分区键”的反直觉结论。
三、序列特征:编号体系与连续性
逐笔数据最容易被忽视却最重要的特征是序列性。逐笔成交有成交编号,逐笔委托有委托编号,两者在各自通道内通常单调递增,但跨通道不保证全局有序。上交所与深交所的编号体系不同,同一证券的成交编号与委托编号也不在同一序列空间。这意味着:
- 编号可以作为分区内的排序键,用于还原事件顺序,但不能作为分区键——它基数极高且分布随交易日重置。
- 撤单事件复用原委托编号,因此委托编号在逐笔委托流中不唯一,必须配合订单状态字段才能定位唯一事件。
- 成交编号在部分市场存在跳号(如集合竞价期间),不能假设连续,做数据完整性校验时要用“区间覆盖”而非“逐号比对”。
序列特征还影响更新模式。逐笔流是纯追加(append-only)的,理论上不存在更新;但撤单事件在语义上是对原委托的“状态变更”。如果表设计成“一行一委托、状态原地更新”,就会破坏追加写的性能优势;如果设计成“一行一事件、状态作为事件属性”,则查询当前订单簿时需要按委托编号做最新状态聚合。分区表设计应默认采用后者,把更新模式转化为追加模式,这也是逐笔数据与快照数据在存储模型上的根本分野。
四、更新模式对分区键的约束
把上述分析收敛成一条设计约束:分区键必须来自“写入时即确定、且不随事件后续变化”的字段。交易日、证券代码、事件类型都满足这一条件;订单状态、成交金额则不满足(前者会变,后者是派生值)。事件时间虽然写入时确定,但因接收延迟存在漂移风险,更适合降级为排序键。
以某交易日、某证券的逐笔数据为例(以下为教学假设参数):单市场单日逐笔成交约 8000 万条、逐笔委托约 1.2 亿条,单条记录约 120 字节,日增原始数据约 24 GB。若按交易日一级分区、证券代码二级分区,单分区平均约 3 万条、几 MB 量级,既避免了小文件问题,又保证按证券查询时能命中少量分区。若错误地按事件时间的小时做一级分区,则单日会产生数十个分区,且跨分区边界的事件漂移会导致约千分之几的记录错位——这个量级在回测场景中足以造成成交序列断裂。
综上,字段结构决定了哪些字段可用,时间精度决定了分区粒度上限,序列特征决定了排序键与完整性校验方式,更新模式则划定了分区键的“不可变”红线。四者共同把分区键候选收敛到交易日、证券代码、事件类型这三个字段上,而事件时间与各类编号则各归其位,进入排序键与校验逻辑。