> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chaintable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 工作原理

> 理解 BlockDB 的表层次、子表和区块进度，以及四类表如何组织当前记录与历史变化。

Chaintable 的在线表由 BlockDB 提供读写能力。表类型决定 `id` 表示什么、一次写入如何影响当前记录，以及历史查询如何找到需要的版本。

| 类型              | 适合的数据          | 如何表示变化               |
| --------------- | -------------- | -------------------- |
| **Normal**      | 资产说明、业务配置、查找字典 | 同一 `id` 对应一条当前记录     |
| **Block Event** | 转账、交易、合约事件     | 每次事件有独立 `id`，记录所属区块  |
| **Block State** | 池储备、合约状态       | 同一业务 `id` 在不同区块有不同版本 |
| **Time**        | 价格、按时间采样的数值    | 同一 `id` 按物理时间保存数值变化  |

## L1、L2 与子表

BlockDB 将表分为两个层次。这里的 **L1、L2 是存储抽象层次**，与区块链的扩容分层无关。

* **L1 表**提供字段、主键、索引和行读写。普通表直接使用这一层。
* **L2 表**在 L1 之上增加区块或时间语义。区块事件表、区块状态表和时间表都属于 L2；它们将一次逻辑写入转换成对一组 L1 表的维护。

这一组由系统管理的表称为 **subtables（子表）**。它们分别保存业务数据、历史版本或处理进度，使系统能够同时回答“现在是什么值”“某个区块时是什么值”和“哪些区块已经处理”。

以区块状态表 `demo.pool_reserves.eth` 为例：

| 子表标识                              | 保存的内容           | 作用                        |
| --------------------------------- | --------------- | ------------------------- |
| `demo.pool_reserves.eth`          | 每个业务 `id` 的最新状态 | 在页面中标为 **latest**，用于当前值读取 |
| `demo.pool_reserves.eth._archive` | 对象在不同区块的历史版本    | 用于按区块回看状态                 |
| `demo.pool_reserves.eth._height`  | 已处理的区块高度区间      | 判断覆盖范围和缺口                 |
| `demo.pool_reserves.eth._bundle`  | 每个区块包的数据摘要和覆盖计数 | 支持按范围比较数据、同步变化            |
| `demo.pool_reserves.eth._write`   | 异步写任务及其状态       | 跟踪批量写入是否完成                |

\*\*区块包（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 依次完成：

1. 根据区块 ID 确定高度和时间，并校验写入范围。
2. 写入事件记录，或维护状态表的 latest 和 archive。
3. 更新区块包摘要，并将该高度合并进已处理区间。

业务数据与进度更新在同一次提交中完成。即使记录列表为空，也会推进覆盖信息；订阅通知在提交后发出。

**起始高度**是这张表开始覆盖的第一个区块。低于它的数据不属于该表的处理范围，写入会被拒绝。

**共识高度**表示从表的起始高度开始，已经连续处理到的最高区块。它反映这张表的数据覆盖情况，不代表区块链的最终确认状态，也不等于记录中最大的 `block_height`。

假设起始高度为 `25939930`，已经处理 `25939930` 和 `25939932`，中间的 `25939931` 尚未处理。此时最新记录可能来自 `25939932`，共识高度仍停在 `25939930`；补齐缺口后才能前进。

未指定区块的状态读取不会自动退回共识高度；事件表按 ID 默认读取该 ID 在最高区块的记录。需要在同一区块比较多个对象或多张表时，应显式传入区块 ID，并确认各表从各自起始高度起已覆盖该区块。指定高于共识高度的区块会被拒绝，而直接读取 latest 并不能证明中间区块已经齐全。

设置起始高度、查看共识高度和异步写任务的方法见[进阶功能](/zh/guides/tables/advanced)。

## 时间表：按时间保留数值变化

时间表按物理时间组织状态，固定使用 `id`、`time_at` 和 `value`。它适合价格等数值序列，不绑定某一条链的区块。

时间表也分别维护 latest 和 archive。`TimeTable(table_id).get(id)` 读取最新值；指定 `time_at` 时，服务端先将时间向上对齐到相应桶边界，再寻找不晚于该边界的最近一条历史记录，不进行数值插值。

时间桶将相近的采样归入同一时间段，每个对象在同一桶内只保留一个值，重复写入该桶会更新这个值。分桶粒度会随数据年龄从分钟变为小时、天，以降低长期存储量。因此，旧数据适合观察趋势，但不能据此还原桶内每次价格跳动；指定秒级时间也不代表可以得到秒级历史快照。

各类表的读写示例见[读写数据](/zh/guides/tables/read-and-write)。
