id 表示什么、一次写入如何影响当前记录,以及历史查询如何找到需要的版本。
L1、L2 与子表
BlockDB 将表分为两个层次。这里的 L1、L2 是存储抽象层次,与区块链的扩容分层无关。- L1 表提供字段、主键、索引和行读写。普通表直接使用这一层。
- L2 表在 L1 之上增加区块或时间语义。区块事件表、区块状态表和时间表都属于 L2;它们将一次逻辑写入转换成对一组 L1 表的维护。
demo.pool_reserves.eth 为例:
**区块包(bundle)**是固定 1000 个区块的区间,按高度顺序编号:区块包
N 覆盖高度 (N-1) × 1000 + 1 到 N × 1000。批量写入和分析同步都以它为单位。
不同表类型使用的子表不同:事件表的同名数据表标为 event;时间表有 latest 和 archive,不需要区块进度表。latest、event 是数据表的角色名称,不是在表 ID 后追加的后缀。
日常读写应通过对应表类型的 SDK 接口完成,由 BlockDB 同时维护相关子表。直接改写某个子表,可能让业务数据与历史、进度信息不一致。子表的主要用途是查询数据和检查处理状态。
普通表:每个 ID 一条当前记录
普通表以id 为主键。写入一个新 id 会新增记录,对已有 id 写入会更新对应记录;它不自动保留旧值。
例如,demo.asset_notes 中 id = 'usdc' 的记录可以保存代币符号和业务说明。更新说明后,按该 ID 读取的就是当前内容。如果需要追踪变化,应选择带历史语义的表类型,或自行设计历史记录。
区块事件表:记录发生过的事
区块事件表将每条事件关联到block_height、block_id 和 block_timestamp。底层事件记录由区块高度与事件 id 共同区分,因此同一区块内的多条事件必须使用不同 ID。
例如,一个地址一天内收到多次 USDC 转账,每次转账都应有自己的事件 ID。通常可用交易与日志位置标识一次转账;只用接收地址,会使同一区块内发给该地址的多次转账相互冲突。
事件表适合保留每次发生的事实。对象的“当前余额”则需要根据事件计算,或另用状态表保存,不能从某条转账记录直接得到。
区块状态表:当前状态与历史版本
区块状态表的id 表示持续跟踪的对象。例如,流动性池储备使用池地址作为 ID;同一个池在不同区块发生变化时,写入该 ID 的新状态。
写入一个区块时,BlockDB 保存该对象的 archive 版本,并维护 latest。历史记录中的 original_id 指向业务 ID,历史记录自身有独立的 id。只有区块高度不早于已有最新版本的写入,才会更新 latest,因此回填较早区块不会用旧状态覆盖较新的状态。
两种读取方式使用不同的数据路径:
StateTable(table_id).get(id)直接读取 latest,得到该对象已写入的最新状态。StateTable(table_id, block_id).get(id)在检查区块覆盖后,从 archive 找到不晚于目标区块的最新版本。
25939930 更新后,若接下来几个区块没有变化,读取后续已处理区块时仍会得到这次储备值。历史读取不要求对象恰好在目标区块发生变化,也不需要为每个未变化的区块重复保存一份状态。
区块数据与处理进度
业务记录只能说明“这个区块产生了哪些数据”。一个区块没有 USDC 转账,既可能是已经检查且没有匹配事件,也可能是还没有处理。因此,BlockDB 单独记录区块覆盖。 以单个区块的写入为例,BlockDB 依次完成:- 根据区块 ID 确定高度和时间,并校验写入范围。
- 写入事件记录,或维护状态表的 latest 和 archive。
- 更新区块包摘要,并将该高度合并进已处理区间。
block_height。
假设起始高度为 25939930,已经处理 25939930 和 25939932,中间的 25939931 尚未处理。此时最新记录可能来自 25939932,共识高度仍停在 25939930;补齐缺口后才能前进。
未指定区块的状态读取不会自动退回共识高度;事件表按 ID 默认读取该 ID 在最高区块的记录。需要在同一区块比较多个对象或多张表时,应显式传入区块 ID,并确认各表从各自起始高度起已覆盖该区块。指定高于共识高度的区块会被拒绝,而直接读取 latest 并不能证明中间区块已经齐全。
设置起始高度、查看共识高度和异步写任务的方法见进阶功能。
时间表:按时间保留数值变化
时间表按物理时间组织状态,固定使用id、time_at 和 value。它适合价格等数值序列,不绑定某一条链的区块。
时间表也分别维护 latest 和 archive。TimeTable(table_id).get(id) 读取最新值;指定 time_at 时,服务端先将时间向上对齐到相应桶边界,再寻找不晚于该边界的最近一条历史记录,不进行数值插值。
时间桶将相近的采样归入同一时间段,每个对象在同一桶内只保留一个值,重复写入该桶会更新这个值。分桶粒度会随数据年龄从分钟变为小时、天,以降低长期存储量。因此,旧数据适合观察趋势,但不能据此还原桶内每次价格跳动;指定秒级时间也不代表可以得到秒级历史快照。
各类表的读写示例见读写数据。