Crypto OS
Technical Crypto OS第五阶段 · Infrastructure & Security

T25 · Blockchain Data Infrastructure

怎样把全链数据变成可查询的仓库?

练习的能力
BuilderOnchain Literacy
动手
把一个协议的历史事件全量落地成列存文件,做一次跨月份的聚合查询。
AI Lab
让 AI 写一条分析查询,自己核对它的口径与链上原始事件是否一致。

一个现实问题

你的索引器已经按 T17 跑得很稳:不漏块、能回滚、能重跑。业务库里躺着几千万条转账事件。

运营发来第一个问题:「过去 12 个月,我们协议每个月的活跃地址数和手续费收入是多少?」

你写了一条 SQL,点回车。四十分钟后结果出来了,与此同时线上告警响了——那条查询把数据库的 IO 吃满,前端接口开始超时。

你加了索引,第二次跑快了一些。然后第二个问题来了:「把新用户和老用户拆开看。」 又是半小时。

第三个问题:「能不能看 24 个月?」

你打开库一看,只有最近 6 个月的数据。因为磁盘涨得太快,三个月前你写了个定时任务删旧数据。

这三个问题一个比一个普通,普通到运营觉得应该是「点两下就有」。但你的系统一个都答不好。

不是你的索引器写得差——它在自己的岗位上做得很对。问题是:为「查一个地址此刻的余额」优化出来的系统,和为「把十亿行扫一遍求个和」优化出来的系统,从存储格式到重跑策略都不是一个东西。

这一章要建的是后面那个。

思想实验

先看一张纸上的账本。

一本流水账,每一页记一笔:日期、对方、金额、用途、经手人、发票号、部门、项目编号——三十个字段。

问题一:「张三昨天那笔钱是多少?」 你翻到那一页,看一眼,答完。翻一页的代价很小。

问题二:「去年全年按月的总支出。」 你必须把一整年的每一页都翻过去。翻的时候你只关心「日期」和「金额」两列,但纸是按页装订的,你没办法只翻那两列——每一页的三十个字段都得从眼前过一遍。

现在换一种装订方式:把所有页的「日期」抄成一卷,所有页的「金额」抄成另一卷,三十个字段三十卷。 问题二变成只取两卷,工作量掉到十五分之一。而且同一卷里全是同类型的数字,压缩率高得离谱,连搬运的重量都轻了。

代价是问题一变难了:想看张三那一笔的完整信息,你得从三十卷里各抽一条再拼起来。

这就是行存和列存的全部区别。没有谁更好,只有各自适合什么问题。

但真正难的不是这个,是第二个思想实验。

假设这本账不是一次抄完的,而是每天新增一批,并且偶尔要把上个月某一天的内容整天换掉:链发生了重组、你发现某个字段一直解析错了、合约升级后事件的含义变了。

这时候问题就不是「怎么翻得快」,而是:

怎么把「某一天」重做一遍,并且做完之后,和从第一天开始完整重做的结果一模一样?

能做到这件事,你的仓库才敢被人拿去做对账、做报表、做结算。做不到,它就只是一个跑得快一点的缓存。

你来决定

运营那三个问题落到你头上,你要选一条路。

观察结果

把四条路放在一起:

交付速度聚合性能历史深度口径可控长期成本
业务库加索引与视图最快受磁盘限制完全可控中,且会拖累线上
只读副本受磁盘限制完全可控中,多一份全量
列存文件 + 分析引擎几乎不受限完全可控低,但要养流水线
第三方数据平台最快不可控按量付费,依赖外部

注意第四列。前三行都是「完全可控」,第四行是「不可控」——而这一列在出事时的权重,比其他四列加起来都大。

因为对一份分析数据,人们真正会问的问题只有两个:

这个数字是怎么算出来的?

我重新算一遍,还是这个数字吗?

第一个问题叫口径,第二个问题叫可重放。它们都不是查询问题,是 ETL 问题。

这就是这一章最反直觉的一点:搭一个能跑的分析仓库,难点不在 SQL,SQL 部分一个下午就能学完。难点在于让每一条数据都能说清出处、每一个分区都能被独立重做、每一次重做都得到同样的结果。

而你已经有一半的答案了:T17 那套 checkpoint、幂等、区间表、重组回滚,在仓库里会以另一种形态再出现一次。

建立模型

三层:Raw、Decoded、Curated

不要把「从链上拉数据」和「算出业务指标」写在同一段代码里。中间至少分三层:

  1. RPC / 节点
  2. Raw 原始层
  3. Decoded 解码层
  4. Curated 业务层
  5. 报表与接口
每一层只做一件事,且每一层都能独立重做
存什么重做它要付什么代价
Raw区块头、交易、回执、日志的原始字节,不解码重新打 RPC,最贵,可能要归档节点
Decoded按 ABI 解出来的结构化事件:谁、给谁、多少重新跑一遍解码,只读本地 Raw,便宜
Curated业务语义:日活、手续费、持仓快照、LP 收益重新跑一遍聚合,最便宜

分层的全部价值就在最后一列。代价最高的那一步,要做到只做一次。

现实里最常见的返工是「某个字段解析错了」或者「合约升级后事件含义变了」。如果你的流水线是 RPC 直接算到指标,这类返工意味着把全链历史重拉一遍——几天起步,还要看 RPC 服务商的脸色。如果 Raw 层留着原始字节,同样的返工只是重跑一次解码,几十分钟。

Raw 层是你为「将来一定会发现自己解错了」买的保险。 它的存储成本远低于一次全量重拉的代价。

分区:按什么切,决定了一切

分区就是把数据物理切成一块块文件,查询时只读相关的块。这个动作叫分区裁剪。

链上数据天然有两个候选键:区块高度时间。选哪个?

两个都要,但角色不同:

物理分区键  →  日期(从区块时间推导,UTC)
分区内排序  →  区块高度,其次 log_index

理由很直接。人和业务提的问题永远是按时间的(「上个月」「Q3」),所以分区按日期切,裁剪才有效。而写入、去重、重做的单位是区块高度,所以分区内部按高度排好序,既能让引擎跳过整段数据,也让「这个分区覆盖哪个高度区间」成为一个精确可查的事实。

目录长这样:

warehouse/
  raw_logs/
    dt=2024-03-01/part-0000.parquet
    dt=2024-03-02/part-0000.parquet
  decoded_transfers/
    dt=2024-03-01/part-0000.parquet
  curated_daily_metrics/
    dt=2024-03-01/part-0000.parquet

两个很容易写错的细节:

第一,分区键必须来自区块时间,绝对不能来自 now() 用写入时间打分区,同一批数据今天跑和明天重跑会落到不同分区,重放立刻失效。这是这条流水线上最隐蔽的一个坑,T16 讲事实表时已经强调过一次:时间戳来自区块头,不是来自你的服务器。

第二,分区粒度不要太细。 一天一个文件、每个文件几十上百兆是舒服的。按小时切会得到几万个小文件,元数据开销会吃掉列存的全部好处——这个现象有个很形象的名字叫小文件问题。

幂等:分区是写入的原子单位

T17 在数据库里靠事务和主键做幂等。文件系统没有事务,但它有一个足够用的替代品:原子重命名

1. 把这个分区的数据完整写进一个临时目录
   warehouse/decoded_transfers/_tmp_2024-03-01_run42/
2. 写完、校验通过之后,原子地换名到正式路径
   → warehouse/decoded_transfers/dt=2024-03-01/
3. 更新 manifest:这个分区的高度区间、行数、校验和、解码器版本

规则和 T17 是同一条,只是换了个载体:

一个分区要么完整存在,要么完全不存在。绝不允许出现「写了一半」的分区。

任务在第 1 步挂掉,临时目录被下次运行清理掉,正式路径纹丝不动。任务在第 2 步之后挂掉,manifest 没更新,下次重跑会重做这个分区——重做的结果和上次逐字节相同,所以无害。

注意顺序:先换名,后更新 manifest。 反过来会得到「manifest 说有,文件却没有」——这正是 T17 里 checkpoint 排在数据前面的那个错误,换了一身衣服又来一次。

Manifest:这条流水线唯一的真相

没有 manifest,你的仓库就是一堆文件,没人能回答「这个月的数据全不全」。

create table warehouse_manifest (
  dataset        text    not null,      -- raw_logs / decoded_transfers / ...
  chain_id       integer not null,
  dt             date    not null,      -- 分区键
  from_block     bigint  not null,      -- 这个分区实际覆盖的高度区间
  to_block       bigint  not null,
  row_count      bigint  not null,
  checksum       text    not null,      -- 内容指纹,用于比对重跑结果
  decoder_version text   not null,      -- 解码器的代码版本,血缘的关键
  status         text    not null,      -- pending / running / done / failed
  attempts       integer not null default 0,
  built_at       timestamptz not null default now(),
  primary key (dataset, chain_id, dt)
);

它直接回答四个平时没人问、出事时人人都问的问题:

-- 1. 有没有洞?
select dt from generate_series($from::date, $to::date, '1 day') g(dt)
 where not exists (
   select 1 from warehouse_manifest m
    where m.dataset = 'decoded_transfers' and m.chain_id = $chain
      and m.dt = g.dt and m.status = 'done'
 );

-- 2. 高度区间连不连续?相邻两天之间不能断档
select a.dt, a.to_block, b.dt, b.from_block
  from warehouse_manifest a
  join warehouse_manifest b
    on b.dataset = a.dataset and b.chain_id = a.chain_id
   and b.dt = a.dt + 1
 where b.from_block <> a.to_block + 1;

-- 3. 哪些分区是用旧版解码器生成的?改了解码逻辑之后,这就是待重做清单
select dt from warehouse_manifest
 where dataset = 'decoded_transfers' and decoder_version <> $current;

-- 4. 重跑之后内容变了吗?校验和逐分区比对

第 1 条和 T17 的区间表是同一个思想:「完成」必须是一个能被一条 SQL 回答的问题,而不是「进度看起来到了」。 第 3 条则是分层设计真正兑现价值的地方。

重组:仓库的正确姿势是落后

列存文件不可变,怎么处理 T17 那种链头重组?

最简单也最正确的办法:根本不让链头进仓库。

业务库(OLTP)   ← 贴着链头,负责实时,承担重组回滚

        │  只搬运 block_number 不超过 safeHeight 的数据

数据仓库(OLAP)  ← 落后若干区块,落地即不可变

safeHeight 就是 T17 里 outbox 用的那个安全高度。仓库天然落后几分钟到几小时,这是设计,不是缺陷。

于是仓库里的重组处理简化成了一句话:仓库里不存在需要回滚的数据。代价是它不适合回答「刚刚那一秒发生了什么」——那个问题本来就该由业务库和 T18 的实时通道回答。

两个系统,两种新鲜度,两种一致性保证。不要试图用一个系统同时满足。

万一真要改写一个已经落地的分区(比如发现三个月前某天解码错了),流程也是现成的:重新生成那个分区、原子换名、更新 manifest、把依赖它的下游分区标成 pending 重跑。这就是为什么 manifest 里要记 decoder_version

Curated 层:口径写在代码里,不写在脑子里

到这一层,技术问题基本结束了,剩下的全是定义问题。

「月活跃地址」这一个词,至少有六种算法:

- 只算发起交易的地址,还是转账双方都算?
- 失败的交易算不算?
- 合约地址算不算?
- 通过聚合器路由过来的,算聚合器还是算最终用户?
- 一个人用五个地址,算一个还是算五个?
- 时区按 UTC 还是按本地?

六个问题各有两个答案,组合起来是几十种口径,每一种都能理直气壮地叫「月活」。

所以 curated 层的每一张表,都要在代码里带一段口径注释,而且这段注释要和 SQL 放在同一个文件里,不要放在另一个文档里——放另一个文档里的定义,三个月后一定和代码对不上。

-- daily_active_addresses
-- 口径(2024-03 定稿):
--   1. 只统计成功交易产生的日志
--   2. 转账的 from 与 to 都计入,各算一次去重
--   3. 零地址(铸造与销毁的对手方)排除
--   4. 已知的合约地址不排除,但单独标记
--   5. 按 UTC 自然日
create table curated_daily_active (
  chain_id   integer not null,
  dt         date    not null,
  contract   bytea   not null,
  addresses  bigint  not null,
  tx_count   bigint  not null,
  primary key (chain_id, dt, contract)
);

一条经验:当两个人对同一个指标报出不同的数字时,九成不是谁算错了,而是口径不同。 把口径写进代码,这类争论会从一小时缩短到一分钟。

一个完整的例子

从解码层算出月度指标,跨月份聚合:

-- 跨 12 个月的月度活跃地址与手续费
-- 分区裁剪靠 dt,实际扫描的只有这 12 个月的文件
with e as (
  select dt, from_addr, to_addr, amount, gas_used, gas_price
    from decoded_transfers
   where chain_id = 1
     and contract = $contract
     and dt >= date '2024-01-01'
     and dt <  date '2025-01-01'
),
addrs as (
  select date_trunc('month', dt) as m, from_addr as addr from e
  union all
  select date_trunc('month', dt) as m, to_addr   as addr from e
)
select m,
       count(distinct addr)                      as active_addresses,
       sum(gas_used * gas_price) / 1e18          as fees_native
  from addrs
  join e on date_trunc('month', e.dt) = addrs.m
 group by m
 order by m;

这条查询里唯一值得盯的一行是 where dt >= ... and dt < ...它决定了引擎读 12 个文件还是读全部文件,而这就是那一到两个数量级的来源。

一个必须知道的反例:把条件写成 where date_trunc('month', dt) = ... 或者对 dt 套任何函数,分区裁剪会失效,引擎老老实实全扫。查询快不快,往往取决于你有没有让分区键保持「裸露」。

它叫什么

ETL抽取转换加载

把数据从来源取出、变形、写入目标的流水线。链上场景里对应「拉区块、解码事件、算业务指标」三段。

它的反义词不是别的技术,而是「写一条 SQL 直接从生产库算指标」。一旦这条 SQL 要跑超过一分钟,你已经在做 ETL 了,只是没有把它当成工程来做。

数据仓库Warehouse

为分析而不是为事务优化的数据存储。它接受更高的写入延迟、更弱的实时性,换来聚合性能和历史深度。

判断标准很简单:如果一个系统同时要服务「查这一个地址的余额」和「扫十亿行求和」,它一定两件事都做不好。

列存Columnar Storage

按列而不是按行组织数据。只读需要的列,同列同类型带来高压缩率。

分析场景里它是默认选择。本章概念列表里的两个名字分别代表两类实现:一类是嵌入式单机引擎,跑在你的进程里,直接查文件,适合单机能装下的数据量;另一类是分布式列存数据库,适合大规模和高并发。选哪个取决于数据量和团队,而这一章的模型对两者完全一样。

分区Partitioning

按某个键把数据物理切块,查询时只读相关的块。这个动作叫分区裁剪。

链上数据的实践是:物理分区按日期,分区内按区块高度排序。前者服务查询,后者服务写入与重做。分区同时也是幂等写入的原子单位。

幂等加载Idempotent Load

同一批数据重复加载任意多次,结果不变。

文件系统上的实现是「写临时目录、校验、原子换名、再更新 manifest」。它和 T17 的「数据和 checkpoint 同一事务」是同一条规则的两种载体:宁可重做,不可做一半。

安全高度Safe Height

只有区块高度低于它的数据才会被搬进仓库。它让仓库里永远不存在需要回滚的数据。

代价是仓库有固定的滞后。这个滞后是要写进文档、让使用者知道的契约,而不是一个需要偷偷解决的性能问题。

血缘Lineage

一个数字能够被追溯到它的上游:哪个分区、哪段高度、哪个版本的解码器、哪条聚合逻辑。

它的最小实现就是 manifest 里那几列。没有血缘的仓库,在「这个数为什么变了」面前只能靠猜。

动手

动手把一个协议的历史事件落成列存文件,做一次跨月份的聚合查询任意语言 + 一个列存引擎 + 公共只读 RPC 或归档数据0 元。全程只读,不发交易,不需要私钥;如果要拉较深的历史,用测试网或体量较小的协议以免打爆 RPC 配额

全程只读。 这一章不签名、不广播、不花钱。

验收标准三条,每一条都能写成自动断言:

验收 1:任意一天的分区重跑,校验和不变
验收 2:manifest 里没有洞,相邻分区的高度区间严格连续
验收 3:聚合结果与从原始日志现算的结果逐行相等

选一个小协议,划一段范围。 不要一上来就全链。挑一个事件量中等的合约,取连续三到四个月。目标是跑通流水线,不是比数据量。

先回答一个问题再动手:这段范围的起止区块高度是多少? 把它写下来,它是后面所有校验的基准。

建 Raw 层,只存不解。

按天分区,落地日志的原始字段:block_numberlog_indexblock_timetx_hashaddresstopicsdatadata 保持原始字节,不要在这一层解码。

raw_logs/dt=YYYY-MM-DD/part-0000.parquet

写入用「临时目录 → 校验 → 原子换名 → 更新 manifest」四步。这四步的顺序在这个 Lab 里会被专门验证。

建 manifest 表,把上面那张 DDL 原样用上。

每写完一个分区记一行:高度区间、行数、校验和、解码器版本、状态。

建完立刻跑那条「有没有洞」的查询。现在应该返回一堆还没跑的日期,这正是它该有的样子——你需要看到它在有洞时确实会报出来,而不是等到上线后才第一次运行它。

建 Decoded 层,只读本地 Raw,不碰 RPC。

按 ABI 把 topicsdata 解成结构化字段。这一步如果还要打 RPC,说明 Raw 层没存够——回去补。

金额一律用整数类型或高精度十进制,绝对不要用浮点。 一次浮点求和的误差在报表上可能看不出来,在对账上一定会显形。

验收 1:重跑任意一天。

挑三个不同的日期,各自重跑一次,比对 manifest 里的校验和。

三次重跑,三个校验和,必须和原值完全一致

不一致就停在这里,先查这三个地方:分区键是不是用了 now()、排序是不是不稳定、有没有把 updated_at 之类的运行时字段写进了文件。前两个是重放失败最常见的原因。

验收 2:故意造一个洞,看它能不能被发现。

删掉中间某一天的分区目录,但不要动 manifest。跑一次聚合,观察结果——它会安静地少掉一天,不报任何错。

然后把 manifest 那一行也删掉,再跑「有没有洞」的查询。这次它必须报出那一天。

这个对比就是 manifest 存在的全部理由:文件会静默缺失,manifest 不会。

建 Curated 层,算月度指标,并写下口径。

在 SQL 文件顶部用注释写清你的口径,至少覆盖前面列的那六个问题。然后跑本章那条跨 12 个月的聚合。

验收 3:和原始日志对账。

挑一天,不用任何 curated 表,直接从 Raw 层的原始日志现算同一个指标。两个数字必须相等。

不相等的话,答案几乎总是口径:你在 curated 层排除了零地址,在对账脚本里没排。这一步的真正价值不是发现 bug,是逼你把口径说清楚。

量一量收益。 同一条月度聚合,分别在业务库和列存上跑,记下耗时和扫描的数据量;再记一下两边的存储占用。

做成一张你自己测出来的表。 课程不给数字,因为它取决于你的数据;但数量级的差异你要亲眼看到一次。

AI Lab

AI Lab让 AI 写一条分析查询,你负责核对它的口径与链上原始事件是否一致Level 2 · AI Copilot

把你的表结构、口径注释和一段样例数据交给模型,分三步。

第一步:
这是我的 decoded_transfers 表结构和分区方式(按 dt 分区,分区内按 block_number 排序)。
写一条查询:过去 12 个月,按月给出活跃地址数与手续费总额。
要求利用分区裁剪,并说明你的查询会扫描多少个分区。

第二步:
列出你在这条查询里做了哪些口径假设。
每一条写成「我假设 X,如果实际是 Y,结果会怎么变」。
至少给出 6 条。

第三步:
现在告诉你真实情况:这个合约在去年 6 月做过一次升级,
升级后同一个事件多了一个字段,topic0 也变了。
你上面那条查询在 6 月前后会发生什么?怎么改?

第一步模型通常写得不错,因为这是一道被写过无数遍的 SQL 题。要盯的只有一件事:分区键有没有被函数包住。这是一个模型很容易犯、而你在结果正确时完全看不出来的错误——查询结果一模一样,代价差几十倍。

第二步是这个 Lab 真正的考题。口径假设是模型最容易沉默地替你做掉的决定:它会自己决定零地址算不算、双方都算还是只算一方,然后给你一个看起来很权威的数字。让它把假设显式列出来,你才有机会说「这条不对」。

第三步考的是链上数据独有的那一类坑:合约地址不是稳定的类型标识,同一个地址在不同时间段可能对应不同的事件结构。T16 结尾提过这件事,这里是它的正式答案——解码逻辑必须按高度区间分段,manifest 里的 decoder_version 就是为这一天准备的。模型在这一问上表现分化很大,它常常只会说「加个 union」,而说不清两段数据的字段含义能不能直接相加。

最后那条验证必须你自己做:挑一天,绕开所有 curated 表,从原始日志现算一遍。 数字对得上,这条查询才算可以拿去做报表。

AI 说完之后,你必须自己验证

  • 它的 where 条件有没有让分区键保持裸露:一旦对 dt 套了函数,分区裁剪就失效,自己看执行计划确认扫了几个分区
  • 活跃地址的去重口径:只算发起方还是双方都算,零地址排没排,和你写在注释里的口径逐条对照
  • 失败交易有没有被算进去:日志只在成功交易里产生,但如果它用的是交易表就必须显式过滤 status
  • 金额的精度:有没有在中间步骤用浮点,有没有把不同精度的代币直接相加
  • 时区:date_trunc 用的是 UTC 还是会话时区,两者在月末那天会给出不同结果
  • 重复计数:join 之后有没有产生行膨胀,用一天的数据手算一遍核对
  • 最关键的一条:挑一天,不用它的查询,直接从原始日志现算同一个数字,两个值必须相等

真实案例

解码器改了一行,历史和新数据从此不是一个口径

一个团队发现某个事件的金额字段应该按有符号整数解析,而不是无符号。他们改了解码器,从当天开始生效。

三个月后做年度对账,发现某几个月的数字明显偏大。排查花了两周,因为仓库里没有任何信息能告诉你某一行是哪个版本的解码器生成的

这正是 manifest 里 decoder_version 那一列存在的理由:改完解码器,一条 SQL 就能列出全部待重做的分区,重做完校验和会变,变了就说明确实受影响。没有这一列,你只能全量重做或者全凭记忆。

按 now() 打分区,重跑之后数据翻倍

回填脚本用系统当前日期做分区键。第一次跑的时候数据落进了「今天」,重跑的时候落进了「明天」。

两份数据同时存在,聚合结果凭空翻倍,而且每个分区单独看都是对的

教训只有一句:分区键必须是数据自身的属性,不能是运行环境的属性。 链上数据的时间戳来自区块头,这一点在 T16 就强调过,到了仓库层它的后果被放大了一个量级。

没有原子换名,半个分区躺了一个月

导出任务在写文件的过程中被容器调度杀掉,留下一个只有一半数据的分区文件,路径是正式路径。

所有查询照常返回结果,没有报错,只是那一天的数字少了六成。一个月后有人对季度报表才发现。

写一半的文件比没有文件危险得多,因为前者会被当成完整数据用。临时目录加原子换名不是洁癖,它是这一类事故唯一的通用解。

一笔交易发出上万条日志,压垮了按交易分批的流水线

某次批量操作在一笔交易里产生了极大量的日志。按「每笔交易一批」处理的流水线在这条记录上内存溢出,重启后又读到同一笔,陷入崩溃重启的循环

教训有两条:批的大小要由字节数或行数决定,不能由「一笔交易」「一个区块」这种业务单位决定;以及任何会重试的流水线都要有失败计数和退避,否则一条毒数据能让整条线永远停在同一个位置。manifest 里那列 attempts 就是干这个的。

改一个变量

如果数据量乘以 100

单机的嵌入式引擎会先在两个地方顶不住:一次查询的内存、以及单个分区的文件大小。

应对顺序是固定的,而且前两步几乎不用改架构:先把分区切得更合理(按链、按合约二级分区),再把 curated 层做厚(把常用聚合预先算好),最后才考虑换成分布式引擎。

先看执行计划再加机器。 大多数「数据量太大」的问题,真实原因是分区裁剪没生效。

如果业务要求指标的新鲜度从小时级提高到秒级

这不是把安全高度调小就能解决的——调小它,你就把重组风险引进了一个不可变的存储。

正确做法是两套系统并存:仓库照旧落后且不可变,实时指标由业务库和 T18 的实时通道提供,查询层把两段拼起来,并明确标出分界点

同时要接受一件事:实时的那一段可能会被修正。界面上要让用户知道哪一段是最终的、哪一段还会变,这正是 T17 那张「按业务风险分档」的表在仓库层的翻版。

如果要同时支持多条链

chain_id 从一列变成了贯穿全部主键和分区路径的维度,这部分是机械工作。

真正难的是口径的可比性:不同链的出块时间不同,一天的区块数差几个数量级;手续费的计价单位不同;同一个代币在不同链上是不同的地址。把它们直接加起来的报表通常是错的。

实践做法是先各链独立算,最后才合并,并且合并的那一层单独写口径。跨链的资产等价性问题本身还有更深的一层,那是下一章的内容。

如果你依赖的那个第三方数据平台改了表结构

如果它只在探索期用,这是一个下午的麻烦。如果你的生产报表依赖它,这是一次事故。

区别不在于它靠不靠谱,而在于你有没有一份自己能重放的原始数据。有 Raw 层,最坏情况是重跑几个小时;没有,你只能等对方。

这也是为什么分层里最不起眼的 Raw 层,是最不该省的一层。

带走的问题

1
它解决什么问题?

它解决什么问题?这一章解决的是「怎样让一个数字能被重新算出来」。难点从来不是查询——查询部分一个下午能学完。难的是分区、幂等、血缘这三件事,而它们全部只在有人质疑某个数字时才显形。

5
谁在支付?

谁在支付?仓库的账单有三块:归档 RPC 或节点的调用、对象存储的容量、以及查询时扫过的字节。三块里最贵的通常是第一块,而分层设计的全部意义就是让它只付一次。任何「需要重新全量拉链」的返工,都是在重复支付最贵的那一项。

17
AI 错误时谁承担损失?

AI 错误时谁承担损失?这一章的 AI Lab 里,模型最容易犯的是沉默的口径假设:它替你决定了零地址算不算、双方算不算,然后给出一个看起来很权威的数字。这类错误不报错、不 revert,只会变成一份发出去的报表。责任在署名发出这份报表的人。

本章自测

一句话带走

链上数据工程的重点是幂等与可重放的 ETL,不是查询语句的技巧。

做完这一章的动手环节了?勾上它查看全部进度

本页目录