BlockVectra

数据集目录(Data Catalog)

面向直接写 SQL 查询的内部团队:本表列出当前所有可查询的 ClickHouse 表(含 v0.1 已落地的 原始表和派生表),以及已合入但本文未展开的其他数据集(§3,各有独立文档)。库名以 robinhood(Robinhood Chain 主网, chain id 4663,Arbitrum Orb…

This content is sourced from upstream and is currently available in Chinese only.

面向直接写 SQL 查询的内部团队:本表列出当前所有可查询的 ClickHouse 表(含 v0.1 已落地的 原始表和派生表),以及已合入但本文未展开的其他数据集(§3,各有独立文档)。库名以 robinhood(Robinhood Chain 主网, chain id 4663,Arbitrum Orbit L2,约 7200 万区块,代币化股票)为例;其他链换库名同理,表结构 完全相同(每条链一个独立数据库,见 0001-architecture.md §4.1)。

本文只描述"表长什么样、每列什么意思、怎么去重、数据新不新鲜、怎么查";建表/物化视图的安装步骤 见各自章节链接的 SQL 文件和 schema/clickhouse/derived/README.md, 本文不重复。列的类型/含义已对照 sql/schema.sql、 sql/traces.sql、schema/clickhouse/derived/001-003_*.sql 的建表语句 与 src/decode.rs 的解码逻辑逐列核对。

目录


1. 原始表

三张原始表由 chain-indexer backfill(历史区间一次性抽取,按段提交)和 chain-indexer follow(追平链头后逐块跟随,检测重组并回滚)两种模式共同写入, schema 完全一致,见 sql/schema.sql。

通用去重规则:三张表引擎都是 ReplacingMergeTree(version, is_deleted)。同一行业务数据 (按各自 ORDER BY 排序键判定"同一行")可能因为 backfill 断点重跑、或 follow 遇到重组而 存在多个版本;后台 merge 之前不会自动去重。所有查询必须显式加 FINAL 并过滤 is_deleted = 0(大范围扫描可用 argMax(col, version) 模式代替 FINAL,见 docs/queries.md §1.1)。version 是写入时刻的 纳秒级 Unix 时间戳(同一次 backfill 段提交或 follow 批次内所有行共享同一个 version); is_deleted = 1 表示这一行在重组中被回滚(写入方式是插入一条内容相同、is_deleted=1、 version 更高的新版本,从不做 ALTER DELETE,见 0001-architecture.md §3.4)。

新鲜度:backfill 覆盖历史区间(一次性、有限范围,跑完即静止);follow 持续追链头 (逐块写入,正常情况下滞后节点几秒内)。两者可同时对同一张表写不同区块范围,ReplacingMergeTree 按排序键自动去重重叠部分。判断某条链当前的新鲜度:SELECT max(number) FROM {db}.blocks FINAL WHERE is_deleted=0 对比节点当前高度(chain-indexer status 子命令内部就是这个查询)。

extra 字段:三张表都有 extra String(非空则是 JSON 字符串)。解码器 (src/decode.rs)对 RPC 返回的每个字段要么解析成固定列,要么原样 丢进 extra(DROP_* 列表里的字段例外,直接丢弃,见下);因此新交易类型、新链的专有字段 永远不会导致解码失败——列表之外的任何未来字段都会自动出现在 extra 里,不需要改代码。 extra 为空字符串表示"这一行没有任何未识别字段"(不是 NULL,也不是 "{}")。

1.1 blocks

用途:区块头。粒度:一行 = 一个区块。ORDER BY / 去重键:number(分区键 intDiv(number, 5000000))。

列类型含义
numberUInt64区块高度,ORDER BY 排序键
hashFixedString(32)区块哈希(原始字节,查询用 unhex()/展示用 hex(),见 §4)
parent_hashFixedString(32)父区块哈希
timestampDateTime('UTC')出块时间(来自 RPC timestamp,UTC)
minerFixedString(20)出块地址(Arbitrum Orbit 上固定是排序器地址)
gas_limitUInt64区块 gas 上限
gas_usedUInt64区块实际消耗的 gas
base_fee_per_gasNullable(UInt256)EIP-1559 基础费;理论上不支持 1559 的链/早期区块可能为 NULL(Robinhood 链目前恒非空)
state_root / transactions_root / receipts_rootFixedString(32)三棵 trie 的根哈希
tx_countUInt32本区块交易数(解码时从 transactions 数组长度算出,不是 RPC 直接字段)
sizeUInt32区块序列化字节数(RPC size)
l1_block_numberNullable(UInt64)仅 Arbitrum 链系:本区块对应的 L1(以太坊主网)区块号;非 Arbitrum 链系此列为 NULL,原始值会出现在 extra.l1BlockNumber 里
extraString(JSON)未提升为固定列的字段,例如 Arbitrum 的 sendCount/sendRoot;logsBloom 因可从 logs 重算被直接丢弃,不会出现在这里
versionUInt64写入版本号(见上文"通用去重规则")
is_deletedUInt81 = 已被重组回滚

示例查询

-- 最新已入库区块高度(判断新鲜度)
SELECT max(number) FROM robinhood.blocks FINAL WHERE is_deleted = 0;

-- 某高度区块的基本信息
SELECT number, hex(hash) AS hash_hex, tx_count, gas_used, gas_limit, l1_block_number
FROM robinhood.blocks FINAL
WHERE number = 72040000 AND is_deleted = 0;

1.2 transactions

用途:交易 + 对应回执(两者按 hash 一对一合并成一行,回执字段直接拼进同一行,不单独建表)。 粒度:一行 = 一笔交易。ORDER BY / 去重键:(block_number, tx_index)(分区键 intDiv(block_number, 5000000));hash/from/to 各有一个 bloom_filter 跳数索引 (GRANULARITY 1,用于按地址/哈希点查,不改变排序键本身)。

列类型含义
block_numberUInt64所在区块高度
tx_indexUInt32区块内交易序号(从 0 开始)
hashFixedString(32)交易哈希
block_timestampDateTime('UTC')所在区块的时间戳(冗余自 blocks,避免按时间过滤时 JOIN)
typeUInt8交易类型:0/1/2 是标准 legacy/access-list/EIP-1559;Arbitrum 链系还有 0x6a(=106,排序器内部消息等)、0x64/0x65 等 ArbOS 专有类型;4(=EIP-7702) 等新类型即使解码器不认识具体子字段,也能正常入库,未知部分进 extra(实测见 §5 验证)
fromFixedString(20)发送地址
toNullable(FixedString(20))接收地址;合约创建交易为 NULL(此时看 contract_address)
valueUInt256转账金额,链上原始整数(wei),禁止转 Float,见 §4
nonceUInt64发送方 nonce
gasUInt64交易声明的 gas 上限
gas_priceNullable(UInt256)legacy/access-list 交易的 gas 单价;EIP-1559 交易此列为 NULL(改用下面两列)
max_fee_per_gas / max_priority_fee_per_gasNullable(UInt256)EIP-1559 交易的费用参数;非 1559 交易为 NULL
inputString调用数据,原始字节(0x 前缀已在解码时去掉)
statusUInt8回执状态,1 成功 0 失败。Fail-closed:极老(pre-Byzantium,EIP-658 之前)交易的回执理论上没有 status 字段而是 root;chain-indexer 遇到回执缺 status 时直接报错中止该段抽取,不会写 status=0 兜底,因此库里出现的每一行 status 都是真实取值,不存在"缺省值伪装成失败"的情况
gas_usedUInt64本交易实际消耗的 gas(回执字段)
cumulative_gas_usedUInt64区块内截至本交易的累计 gas 消耗
effective_gas_priceNullable(UInt256)实际生效的 gas 单价
contract_addressNullable(FixedString(20))非合约创建交易为 NULL;创建成功时是新合约地址
log_countUInt32本交易产生的日志条数(解码时从回执 logs 数组长度算出)
gas_used_for_l1Nullable(UInt64)仅 Arbitrum 链系:本交易分摊的 L1 calldata 成本(gas 计价);非 Arbitrum 链系为 NULL,原值在 extra.receipt.gasUsedForL1
l1_block_numberNullable(UInt64)仅 Arbitrum 链系:回执侧的 L1 区块号(与 blocks.l1_block_number 同一概念,逐交易冗余一份)
extraString(JSON)形如 {"tx":{...},"receipt":{...}};分别装交易侧、回执侧未提升的字段(例如 EIP-7702 的 authorizationList,或 Arbitrum requestId/ticketId/refundTo 等按交易类型才有的字段);某一侧没有未识别字段就省略对应 key;两侧都没有则整列是空字符串 ""
version / is_deletedUInt64 / UInt8同 blocks

签名字段(v/r/s/yParity)和 chainId 被解码器直接丢弃(TX_DROP),不会出现在 extra 里——前者可从节点重新拉取,后者是每个库固定的常量,没必要逐行存储。

示例查询

-- 某地址发起 + 收到的交易
SELECT block_number, hex(hash) AS tx_hash_hex, toString(value) AS value_str, status
FROM robinhood.transactions FINAL
WHERE (`from` = unhex('c3c9f0171490ef0f4536fe493f3b0ebb5ee0cb5e')
    OR `to`   = unhex('c3c9f0171490ef0f4536fe493f3b0ebb5ee0cb5e'))
  AND is_deleted = 0
ORDER BY block_number DESC LIMIT 50;

-- 某笔交易未识别字段长什么样(例如 EIP-7702 交易的 authorizationList)
SELECT hex(hash) AS tx_hash_hex, type, extra
FROM robinhood.transactions FINAL
WHERE hash = unhex('057b2b239a9931b8249a462698a3dc67227868f18ee060f566f9130e1a29a439')
  AND is_deleted = 0;

1.3 logs

用途:事件日志。粒度:一行 = 一条日志(一笔交易可产生 0~N 条)。ORDER BY / 去重键: (block_number, log_index)(分区键同上);address/topic0/tx_hash 各有一个 bloom_filter 跳数索引。

列类型含义
block_numberUInt64所在区块高度
tx_indexUInt32产生该日志的交易在区块内的序号
log_indexUInt32区块内日志序号(不是交易内),解码时校验过全区块连续无跳号
tx_hashFixedString(32)产生该日志的交易哈希
block_timestampDateTime('UTC')冗余自 blocks
addressFixedString(20)产生该日志的合约地址
topic0Nullable(FixedString(32))事件签名(keccak256(EventName(argTypes)));匿名事件(anonymous,无 topic0)为 NULL
topic1 / topic2 / topic3Nullable(FixedString(32))第 1~3 个 indexed 参数;事件声明的 indexed 参数少于 3 个时,多出的列为 NULL(例如 ERC20 Transfer 只有 topic1/topic2,topic3 恒 NULL;ERC721 Transfer 把 tokenId 也声明成 indexed,三个 topic 都非空——两者能用这个形状区分,见 §2)
dataString未索引参数的 ABI 编码,原始字节;可以是空字符串(长度 0,例如 ERC721 Transfer 全部参数都是 indexed)
extraString(JSON)未提升字段;当前只有 removed(重组产生的"已移除"标记,LOG_DROP 里直接丢弃,因为 chain-indexer 自己用 is_deleted 表达同一件事)以外的未来字段会落到这里,目前该表 extra 常见为空
version / is_deletedUInt64 / UInt8同 blocks;follow 检测到重组时会把受影响区块的日志整体打上 is_deleted=1 的新版本

示例查询

-- 一笔交易的全部日志,按 log_index 排序
SELECT log_index, hex(address) AS address_hex, hex(topic0) AS topic0_hex, hex(data) AS data_hex
FROM robinhood.logs FINAL
WHERE tx_hash = unhex('057b2b239a9931b8249a462698a3dc67227868f18ee060f566f9130e1a29a439')
  AND is_deleted = 0
ORDER BY log_index;

-- 某合约在某区块区间产生的日志数
SELECT count() FROM robinhood.logs FINAL
WHERE address = unhex('4a0e65a3eccec6dbe60ae065f2e7bb85fae35eea')
  AND block_number BETWEEN 72040000 AND 72041000
  AND is_deleted = 0;

1.4 _indexer_progress(内部断点表,一般不用于业务查询)

用途:backfill 的段级断点/幂等标记,供进程重启后判断哪些段已完成(详见 0001-architecture.md §3.5)。粒度:一行 = 一个已提交完成的 backfill 段。ORDER BY / 去重键:start(无分区)。

列类型含义
start / endUInt64段区间 [start, end),end 不含
first_parent_hashFixedString(32)段内第一个区块的父哈希(用于校验段之间的链连续性)
last_hashFixedString(32)段内最后一个区块的哈希
blocks / txs / logsUInt64该段写入的行数统计
committed_atDateTime('UTC')段提交完成时刻
version / is_deletedUInt64 / UInt8同其他表

内部运维表,业务 SQL 一般不需要查;仅在排查"某段是否已回填"或"backfill 是否卡住"时用 SELECT start, end, blocks, txs, logs, committed_at FROM {db}._indexer_progress FINAL WHERE is_deleted=0 ORDER BY start。


2. 派生表

两张派生表都是从 {db}.logs 用物化视图(insert-trigger MV)实时派生,采用正确性方案 (a): MV 产出的行沿用源表 logs 完全相同的排序键片段(block_number/log_index/tx_hash 等)加上 自身语义列,并把 version/is_deleted 原样透传而不是重新计算——insert 触发时 ClickHouse 对每一批新写入 logs 的行(包括重组产生的 is_deleted=1 回滚行)跑一次同样的 SELECT transform,因此 logs 里同一条日志的哪个版本、有没有被删,erc20_transfers/erc721_transfers 里对应那一行的 version/is_deleted 完全同步,不会出现"日志被重组回滚了但派生表还留着旧数据" 或"重复插入导致派生表金额被重复统计"的问题。选这个方案(而不是方案 (b) 的定期从 FINAL 重算)是因为转账事件本身就是逐行透传、不做聚合——没有"SUM/COUNT 提前算好存表"这类会被重复 插入放大的操作,天然适合 insert-trigger MV。

去重规则:与原始表相同,FINAL + is_deleted = 0。新鲜度:跟随 logs 的写入路径 (backfill 段提交 / follow 逐块写入)实时产出,MV 触发几乎没有额外延迟;logs 有多新,这两张 表就有多新。安装范围:MV 只对建视图之后新写入 logs 的行生效,建视图之前已有的历史 数据需要用 003_backfill.sql 单独补,细节见 schema/clickhouse/derived/README.md(E 值的 取法是全篇最容易出错的一步,README 里专门有一节讲这个)。

2.1 erc20_transfers

用途:从 logs 里筛出形如 ERC20 Transfer(address,address,uint256) 的日志,解出结构化的 转账记录。粒度:一行 = 一条满足"同质代币转账形状"的日志(不是一行 = 一笔交易,一笔交易可能 有多条转账日志)。ORDER BY / 去重键:(token, block_number, log_index)(token/block_number /log_index 三者共同构成排序键,log_index 保证同一 (token, block_number) 下每条日志各占 一行;分区键 intDiv(block_number, 5000000));from/to 各有一个 bloom_filter 跳数索引 (GRANULARITY 4)。

形状判定(源 SQL 见 001_erc20_transfers.sql): topic0 = keccak256("Transfer(address,address,uint256)") 且 topic1/topic2 非空、topic3 为空、data 恰好 32 字节。这个形状与 §2.2 的 ERC721 形状互斥(topic3 是否为空是分界线), 所以一条日志只会落进这两张表中的一张,不会重复。

列类型含义
tokenFixedString(20)代币合约地址(= 产生日志的 logs.address)
from / toFixedString(20)转出/转入地址(从 topic1/topic2 这两个 32 字节、左侧补零的地址里取后 20 字节还原)
amountUInt256转账数量,链上原始整数(不知道也不存储代币 decimals);换算成人类可读数值要在查询时自己除以 10^decimals,见 §4
block_number / block_timestamp / tx_hash / tx_index / log_index同 logs 对应列冗余自源日志,定位这条转账发生在哪笔交易的哪条日志
version / is_deletedUInt64 / UInt8透传自 logs(见上文"正确性方案")

示例查询

-- 某代币最近的转账(token 是排序键第一位,直接命中,最快)
SELECT block_number, hex(tx_hash) AS tx_hash_hex, hex(`from`) AS from_hex, hex(`to`) AS to_hex,
       toString(amount) AS amount_str
FROM robinhood.erc20_transfers FINAL
WHERE is_deleted = 0 AND token = unhex('39dbed3a2bd333467115de45665cc57f813c4571')
ORDER BY block_number DESC, log_index DESC LIMIT 100;

-- 某天转账笔数最多的代币 Top 10
SELECT hex(token) AS token_hex, count() AS transfer_count
FROM robinhood.erc20_transfers FINAL
WHERE is_deleted = 0
  AND block_timestamp >= toDateTime('2026-09-25 00:00:00', 'UTC')
  AND block_timestamp <  toDateTime('2026-09-26 00:00:00', 'UTC')
GROUP BY token ORDER BY transfer_count DESC LIMIT 10;

2.2 erc721_transfers

用途:从 logs 里筛出形如 ERC721 Transfer(address,address,uint256)(tokenId 也是 indexed)的日志。粒度:一行 = 一条 NFT 转账日志。ORDER BY / 去重键: (token, block_number, log_index),分区/索引策略与 erc20_transfers 相同。

形状判定(源 SQL 见 002_erc721_transfers.sql): topic0 与 ERC20 完全相同(两种代币标准复用同一个事件签名字符串),但 topic1/topic2/topic3 三者都非空、data 长度为 0——tokenId 被当成第三个 indexed 参数而不是 data 里的未索引参数。

列类型含义
tokenFixedString(20)NFT 合约地址
from / toFixedString(20)转出/转入地址(还原方式同 ERC20)
token_idUInt256NFT 的 tokenId(从 topic3 还原,NFT id 没有小数位概念,不做任何缩放)
block_number / block_timestamp / tx_hash / tx_index / log_index同 logs 对应列同 ERC20
version / is_deletedUInt64 / UInt8同 ERC20,透传自 logs

示例查询

-- 某 NFT 合约的转账历史
SELECT block_number, hex(tx_hash) AS tx_hash_hex, toString(token_id) AS token_id_str,
       hex(`from`) AS from_hex, hex(`to`) AS to_hex
FROM robinhood.erc721_transfers FINAL
WHERE is_deleted = 0 AND token = unhex('07f44c47743a2f36414a82b9f558ecfcf0eedcef')
ORDER BY block_number DESC LIMIT 50;

-- 判断某地址当前是否持有某 tokenId(取该 tokenId 最新一次转账的 to)
SELECT hex(`to`) AS current_owner_hex
FROM robinhood.erc721_transfers FINAL
WHERE is_deleted = 0
  AND token = unhex('07f44c47743a2f36414a82b9f558ecfcf0eedcef')
  AND token_id = 12345
ORDER BY block_number DESC, log_index DESC LIMIT 1;

2.3 erc20_transfers_by_holder / erc721_transfers_by_holder(地址维度转账索引)

erc20_transfers/erc721_transfers 的排序键是 (token, block_number, log_index),适合"某 token 的历史",不适合"某地址的全部转账"——from/to 上只有 bloom_filter 跳数索引,而 ClickHouse 26.9 下只要查询带 FINAL 就完全不做 skip index 剪枝(实测:同一地址单边查询 不带 FINAL 只读 928/35,395 granule、44 ms;带 FINAL 读满 35,395/35,395、9.7 s),所以 from = X OR to = X 对任何地址都是一次全表扫描。这两张表把持有人放在排序键首位, 把该查询变成键范围读。建表/物化视图见 schema/clickhouse/derived/035_transfers_by_holder.sql, 安装顺序(Phase 1 schema + MV,Phase 2 回填在 003 之后)见 schema/clickhouse/derived/README.md。

语义:每笔转账落 2 行——direction = 0 的行的 holder 是转出的 from、 counterparty 是 to;direction = 1 的行的 holder 是转入的 to、counterparty 是 from。自转(from = to)两行都在(所以 FINAL 行数恒等于原表 FINAL 行数的 2 倍)。 version/is_deleted 原样透传:reorg 撤掉一笔转账会同时撤掉它对应的 2 行,重新上线同理。 新鲜度:insert-trigger MV(MV 链 logs → erc20/erc721_transfers → 本表),follow 写入 实时跟随;建表之前的历史由 035 的 Phase 2 回填补齐。 去重规则:与原始表相同,FINAL + is_deleted = 0。分区/排序键:与源表一致的 intDiv(block_number, 5_000_000) 分区,ORDER BY (holder, block_number, log_index, direction)。

2.3.1 erc20_transfers_by_holder

列类型含义
holderFixedString(20)持有人地址(原始字节,展示用 hex()),ORDER BY 首位
directionUInt80 = 该地址是 from(转出);1 = 该地址是 to(转入)
counterpartyFixedString(20)对手方地址(direction=0 时是 to,direction=1 时是 from)
tokenLowCardinality(FixedString(20))ERC20 合约地址
amountUInt256转账金额,链上原始整数(禁止转 Float,换算用 tokens.decimals)
block_number / block_timestampUInt64 / DateTime('UTC')所在区块号/时间
tx_hashFixedString(32)产生该转账的交易哈希
tx_index / log_indexUInt32交易在区块内序号 / 日志在区块内序号
version / is_deletedUInt64 / UInt8与源表逐行同步的版本号/删除标记,读时 FINAL + is_deleted = 0

2.3.2 erc721_transfers_by_holder

同 2.3.1,仅把 amount UInt256 换成 token_id UInt256(ERC721 的 token id, toString() 后再交给应用层)。

性能与磁盘(2026-09-26 dev 实测,CH 26.9.1,max_threads=4;样本 = 源表 3 个分区 + 五个全历史地址,见 docs/perf/2026-09-26-holder-lookup.md):

项数值
count()(15 行 / 1.7 万 / 26 万 / 1423 万 / 1.15 亿行地址)0.05 / 0.05 / 0.05 / 0.43 / 2.60 s
最新 50 条(含 payload)0.07 / 0.07 / 0.08 / 0.12 / 0.13 s(带 block_number 下界 0.02-0.04 s)
同样查询打 erc20_transfers FINAL(对照,289.6M 行样本)8.6-15.0 s 每次(全表扫描,地址多小都一样);全表口径 115-137 s/次(robinhood 2.78B 物理行 / 72.97 GiB,2026-09-26 复测,见 perf 文档 §3)
1.15 亿行地址全量 payload5.7 s(源表 15.1 s)
全量磁盘ERC20 约 92-94 GiB(32.1 B/派生行,全量 30.7 亿派生行;merge 收敛后口径,源表同时重建时镜像瞬时峰值 165-185 GiB);ERC721 实测 2.61 GiB(1.448 亿行)
回填按 5,000,000 区块(一个分区)分段执行(scripts/derived/backfill_transfers_by_holder.sh,可 --lo 续跑);ERC20 约 48 分钟、ERC721 约 3 分钟(1.07M 派生行/s,max_threads=4)

⚠️ 读法要点:holder 是排序键首位,但必须 FINAL + is_deleted = 0;分页建议带 block_number 游标下界(否则要按"每个活跃 part 取尾部 granule"的方式扫,1.15 亿行地址 约 0.13 s、2.94M 行)。不要指望在源表上加投影或提高 bloom 精度来解决:投影在 FINAL 下 被明确拒绝(Code 584),from/to 的 bloom 在 FINAL 下不剪枝。


3. 其他数据集(已合入,本文未展开)

更新(2026-09-27):本节旧版写的是"正在其他并行分支开发、尚未产出可查询的表",已过时——下列数据集的 DDL 与文档都已合入 pre-dev(以 pre-dev @5772fd7 里的文件为准),traces 已由 follow --traces(#35)在生产实时写入。 本文仍不展开它们的列定义,请看各自的文档;全部派生对象的一览表见 cookbook §1。 某个库里是否已安装、是否已补数,以该库的 system.tables 和实际行数为准(2026-09-26 生产安装进行中的快照见 docs/perf/2026-09-26-query-baseline.md §0 第 6 条,部分表当时仍是 0 行)。

数据集DDL文档
traces(调用轨迹,另有 contract_creations / native_transfers 物化视图、_trace_gaps)sql/traces.sqltraces.md。只由 follow --traces 实时写入:生产从 2026-09-25 起采集,只覆盖跟块区间,更早的历史没有 trace,缺口记在 _trace_gaps
tokens(代币元数据)sql/tokens.sqltokens.md(由 enrich-tokens 子命令填充)
erc20_balances(余额/持仓)027_erc20_balances.sql / 028_erc20_balances_backfill.sql0005-erc20-balances.md
stock_tokens(代币化股票注册表)007_stock_tokens.sqlstock-tokens.md
DEX 成交008_dex.sqldex.md
NFT(ERC1155 / 持有人,区别于 erc721_transfers 的转账流水)009_erc1155_nft_owners.sqlnft.md
contracts(合约注册表)010_contracts.sqlcontracts.md
地址标签011_address_labels.sqllabels.md
跨链桥012_bridge_flows.sqlbridge.md
手续费013_fees.sqlfees.md
活跃用户014_active_users.sqlactive-users.md
事件签名 / 方法选择器015_event_signatures.sql / 016_method_selectors.sqlevent-signatures.md / method-selectors.md
预言机价格017_oracle_prices.sql / 017_oracle_prices_backfill.sqloracle-prices.md

DDL 均在 schema/clickhouse/derived/(sql/ 开头的除外)。此后合入的 018–026、031–033 (授权、供应量、失败交易、吞吐、L1 定价、交互图、钱包画像、WETH、代理合约、DEX 价格、股票看板、新鲜度) 同样见 docs/datasets/ 下对应的文档与 cookbook §1;034_qa_status.sql 见 deploy/indexer/README.md,035_transfers_by_holder.sql 见本文 §2.3。


4. 常见坑

以下是本文档独有的补充/速记,完整版和更多例子见 docs/queries.md(含实测 ClickHouse 26.9 的坑),本节不重复展开:

  1. 二进制列不是十六进制字符串:地址(FixedString(20))、哈希/topic (FixedString(32))、data/input(String)都是原始字节。条件里用 unhex('...') (不带 0x),取出来给人看用 hex(col)。WHERE addr = '0xabc...' 永远查不到数据、也不报错, 最容易被误判为"这个地址真的没记录"。
  2. UInt256 一律 toString():value/amount/token_id/gas_price 等大数很多客户端 不能安全序列化,读出来先 toString() 转字符串再交给应用层按需转 BigInt/Decimal; ERC20 amount 换算成人类可读数值必须在查询时用代币自己的 decimals(本表不存储,也不知道)。
  3. FINAL 的代价:小范围查询(先按 block_number/tx_hash 过滤再 FINAL)够快, 默认优先用;全表/大范围扫描(例如按天聚合全表)改用 argMax(col, version) 模式,否则 FINAL 会拖慢查询甚至撑爆内存,见 docs/queries.md 例 3/5 的两种写法对比。
  4. 别名同名遮蔽踩坑(实测于 ClickHouse 26.9):SELECT hex(from) AS from ... 这种别名和 原列同名的写法,会让同一层甚至外层的 WHERE/GROUP BY 悄悄把该名字解析成别名表达式本身, 不报错、直接返回 0 行,非常容易被误判为"没有数据"。本文档和 docs/queries.md 的例子 一律用 xxx_hex/xxx_str 这类不同名的别名规避,写查询时照此约定。
  5. extra 是兜底垃圾桶,不是稳定 schema:不同链系、不同交易类型往 extra 里塞的字段完全 不同(例如 Arbitrum 内部交易 vs. EIP-7702 交易),不要假设某个 key 一定存在;用 JSONExtractString/JSONHas 之类函数按需取,取不到就是这条数据确实没有这个字段。
  6. 专链字段的 NULL 不代表"抓取失败":gas_used_for_l1/l1_block_number(Arbitrum 链系 专有)只在 family=arbitrum 的链上非空;to/contract_address 一个非空另一个必为 NULL(看是不是合约创建交易);gas_price 和 max_fee_per_gas/max_priority_fee_per_gas 互斥(legacy vs. EIP-1559)。这些都是正常的 "此字段对这一行不适用",不是数据缺失。
  7. traces 只覆盖跟块区间(更新 2026-09-27:旧版写的"还没有数据"已过时):follow --traces 从 2026-09-25 起在生产实时写入 {db}.traces,但更早的历史区块没有 trace,跟块期间超出节点可 trace 窗口的区块也永久缺失(记在 {db}._trace_gaps)。按区间查 trace 前先确认该区间在覆盖范围内、不在 _trace_gaps 里,见 §3 与 traces.md。

5. 安装与补算

本文档是纯文档交付(不新增/不修改任何表结构或物化视图),没有需要安装或补算的内容。 已有派生表(erc20_transfers/erc721_transfers)的建表、物化视图和历史补算步骤见 schema/clickhouse/derived/README.md,本文不重复。

编写与核对方式:本文所有列名/类型逐一对照 sql/schema.sql、 schema/clickhouse/derived/001-003_*.sql、sql/traces.sql 的建表语句, 以及 src/decode.rs 的字段提升/丢弃规则(*_KNOWN/*_DROP 常量与 decode_block 函数体)核对;未直接读代码猜测任何列的类型或含义。

生产环境抽样验证(只读,SETTINGS max_threads=2, max_execution_time=90,未触碰任何写路径/ 服务):

验证项结果
robinhood.blocks FINAL 行数(全表,is_deleted=0)15,152,751
robinhood.transactions FINAL 行数(全表)118,714,098
robinhood.logs FINAL 行数(全表)417,960,043
robinhood.erc20_transfers FINAL 行数(全表)202,383,546
robinhood.erc721_transfers FINAL 行数(全表)5,891,069
当前已入库最高区块72,044,491
ERC20 形状独立计算 vs 派生表,区块 [72040000, 72040200)原始 logs 按 §2.1 形状条件直接 count() = 2186;erc20_transfers FINAL 同区间 count() = 2186 —— 精确匹配
ERC721 形状独立计算 vs 派生表,同区间原始 logs 按 §2.2 形状条件 count() = 34;erc721_transfers FINAL 同区间 count() = 34 —— 精确匹配
logs 该区间原始行数 vs FINAL 行数5590 / 5590,无重复(该区间未发生重组/重跑重叠)
真实新交易类型样例(EIP-7702,type=4)tx_hash = 0x057b2b23...29a439:extra 含 {"tx":{"accessList":[],"authorizationList":[...]}},验证"未识别字段进 extra、不影响入库"的解码规则
真实 Arbitrum 内部交易样例(type=106/0x6a)tx_hash = 0x895007a3...44a0c:gas_used_for_l1=0,l1_block_number=26052901,extra=""(该交易类型的全部字段都被识别,无未提升字段)

On this page