数据集目录(Data Catalog)
面向直接写 SQL 查询的内部团队:本表列出当前所有可查询的 ClickHouse 表(含 v0.1 已落地的 原始表和派生表),以及已合入但本文未展开的其他数据集(§3,各有独立文档)。库名以 robinhood(Robinhood Chain 主网, chain id 4663,Arbitrum Orb…
面向直接写 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))。
| 列 | 类型 | 含义 |
|---|---|---|
number | UInt64 | 区块高度,ORDER BY 排序键 |
hash | FixedString(32) | 区块哈希(原始字节,查询用 unhex()/展示用 hex(),见 §4) |
parent_hash | FixedString(32) | 父区块哈希 |
timestamp | DateTime('UTC') | 出块时间(来自 RPC timestamp,UTC) |
miner | FixedString(20) | 出块地址(Arbitrum Orbit 上固定是排序器地址) |
gas_limit | UInt64 | 区块 gas 上限 |
gas_used | UInt64 | 区块实际消耗的 gas |
base_fee_per_gas | Nullable(UInt256) | EIP-1559 基础费;理论上不支持 1559 的链/早期区块可能为 NULL(Robinhood 链目前恒非空) |
state_root / transactions_root / receipts_root | FixedString(32) | 三棵 trie 的根哈希 |
tx_count | UInt32 | 本区块交易数(解码时从 transactions 数组长度算出,不是 RPC 直接字段) |
size | UInt32 | 区块序列化字节数(RPC size) |
l1_block_number | Nullable(UInt64) | 仅 Arbitrum 链系:本区块对应的 L1(以太坊主网)区块号;非 Arbitrum 链系此列为 NULL,原始值会出现在 extra.l1BlockNumber 里 |
extra | String(JSON) | 未提升为固定列的字段,例如 Arbitrum 的 sendCount/sendRoot;logsBloom 因可从 logs 重算被直接丢弃,不会出现在这里 |
version | UInt64 | 写入版本号(见上文"通用去重规则") |
is_deleted | UInt8 | 1 = 已被重组回滚 |
示例查询
-- 最新已入库区块高度(判断新鲜度)
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_number | UInt64 | 所在区块高度 |
tx_index | UInt32 | 区块内交易序号(从 0 开始) |
hash | FixedString(32) | 交易哈希 |
block_timestamp | DateTime('UTC') | 所在区块的时间戳(冗余自 blocks,避免按时间过滤时 JOIN) |
type | UInt8 | 交易类型:0/1/2 是标准 legacy/access-list/EIP-1559;Arbitrum 链系还有 0x6a(=106,排序器内部消息等)、0x64/0x65 等 ArbOS 专有类型;4(=EIP-7702) 等新类型即使解码器不认识具体子字段,也能正常入库,未知部分进 extra(实测见 §5 验证) |
from | FixedString(20) | 发送地址 |
to | Nullable(FixedString(20)) | 接收地址;合约创建交易为 NULL(此时看 contract_address) |
value | UInt256 | 转账金额,链上原始整数(wei),禁止转 Float,见 §4 |
nonce | UInt64 | 发送方 nonce |
gas | UInt64 | 交易声明的 gas 上限 |
gas_price | Nullable(UInt256) | legacy/access-list 交易的 gas 单价;EIP-1559 交易此列为 NULL(改用下面两列) |
max_fee_per_gas / max_priority_fee_per_gas | Nullable(UInt256) | EIP-1559 交易的费用参数;非 1559 交易为 NULL |
input | String | 调用数据,原始字节(0x 前缀已在解码时去掉) |
status | UInt8 | 回执状态,1 成功 0 失败。Fail-closed:极老(pre-Byzantium,EIP-658 之前)交易的回执理论上没有 status 字段而是 root;chain-indexer 遇到回执缺 status 时直接报错中止该段抽取,不会写 status=0 兜底,因此库里出现的每一行 status 都是真实取值,不存在"缺省值伪装成失败"的情况 |
gas_used | UInt64 | 本交易实际消耗的 gas(回执字段) |
cumulative_gas_used | UInt64 | 区块内截至本交易的累计 gas 消耗 |
effective_gas_price | Nullable(UInt256) | 实际生效的 gas 单价 |
contract_address | Nullable(FixedString(20)) | 非合约创建交易为 NULL;创建成功时是新合约地址 |
log_count | UInt32 | 本交易产生的日志条数(解码时从回执 logs 数组长度算出) |
gas_used_for_l1 | Nullable(UInt64) | 仅 Arbitrum 链系:本交易分摊的 L1 calldata 成本(gas 计价);非 Arbitrum 链系为 NULL,原值在 extra.receipt.gasUsedForL1 |
l1_block_number | Nullable(UInt64) | 仅 Arbitrum 链系:回执侧的 L1 区块号(与 blocks.l1_block_number 同一概念,逐交易冗余一份) |
extra | String(JSON) | 形如 {"tx":{...},"receipt":{...}};分别装交易侧、回执侧未提升的字段(例如 EIP-7702 的 authorizationList,或 Arbitrum requestId/ticketId/refundTo 等按交易类型才有的字段);某一侧没有未识别字段就省略对应 key;两侧都没有则整列是空字符串 "" |
version / is_deleted | UInt64 / 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_number | UInt64 | 所在区块高度 |
tx_index | UInt32 | 产生该日志的交易在区块内的序号 |
log_index | UInt32 | 区块内日志序号(不是交易内),解码时校验过全区块连续无跳号 |
tx_hash | FixedString(32) | 产生该日志的交易哈希 |
block_timestamp | DateTime('UTC') | 冗余自 blocks |
address | FixedString(20) | 产生该日志的合约地址 |
topic0 | Nullable(FixedString(32)) | 事件签名(keccak256(EventName(argTypes)));匿名事件(anonymous,无 topic0)为 NULL |
topic1 / topic2 / topic3 | Nullable(FixedString(32)) | 第 1~3 个 indexed 参数;事件声明的 indexed 参数少于 3 个时,多出的列为 NULL(例如 ERC20 Transfer 只有 topic1/topic2,topic3 恒 NULL;ERC721 Transfer 把 tokenId 也声明成 indexed,三个 topic 都非空——两者能用这个形状区分,见 §2) |
data | String | 未索引参数的 ABI 编码,原始字节;可以是空字符串(长度 0,例如 ERC721 Transfer 全部参数都是 indexed) |
extra | String(JSON) | 未提升字段;当前只有 removed(重组产生的"已移除"标记,LOG_DROP 里直接丢弃,因为 chain-indexer 自己用 is_deleted 表达同一件事)以外的未来字段会落到这里,目前该表 extra 常见为空 |
version / is_deleted | UInt64 / 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 / end | UInt64 | 段区间 [start, end),end 不含 |
first_parent_hash | FixedString(32) | 段内第一个区块的父哈希(用于校验段之间的链连续性) |
last_hash | FixedString(32) | 段内最后一个区块的哈希 |
blocks / txs / logs | UInt64 | 该段写入的行数统计 |
committed_at | DateTime('UTC') | 段提交完成时刻 |
version / is_deleted | UInt64 / 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 是否为空是分界线),
所以一条日志只会落进这两张表中的一张,不会重复。
| 列 | 类型 | 含义 |
|---|---|---|
token | FixedString(20) | 代币合约地址(= 产生日志的 logs.address) |
from / to | FixedString(20) | 转出/转入地址(从 topic1/topic2 这两个 32 字节、左侧补零的地址里取后 20 字节还原) |
amount | UInt256 | 转账数量,链上原始整数(不知道也不存储代币 decimals);换算成人类可读数值要在查询时自己除以 10^decimals,见 §4 |
block_number / block_timestamp / tx_hash / tx_index / log_index | 同 logs 对应列 | 冗余自源日志,定位这条转账发生在哪笔交易的哪条日志 |
version / is_deleted | UInt64 / 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 里的未索引参数。
| 列 | 类型 | 含义 |
|---|---|---|
token | FixedString(20) | NFT 合约地址 |
from / to | FixedString(20) | 转出/转入地址(还原方式同 ERC20) |
token_id | UInt256 | NFT 的 tokenId(从 topic3 还原,NFT id 没有小数位概念,不做任何缩放) |
block_number / block_timestamp / tx_hash / tx_index / log_index | 同 logs 对应列 | 同 ERC20 |
version / is_deleted | UInt64 / 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
| 列 | 类型 | 含义 |
|---|---|---|
holder | FixedString(20) | 持有人地址(原始字节,展示用 hex()),ORDER BY 首位 |
direction | UInt8 | 0 = 该地址是 from(转出);1 = 该地址是 to(转入) |
counterparty | FixedString(20) | 对手方地址(direction=0 时是 to,direction=1 时是 from) |
token | LowCardinality(FixedString(20)) | ERC20 合约地址 |
amount | UInt256 | 转账金额,链上原始整数(禁止转 Float,换算用 tokens.decimals) |
block_number / block_timestamp | UInt64 / DateTime('UTC') | 所在区块号/时间 |
tx_hash | FixedString(32) | 产生该转账的交易哈希 |
tx_index / log_index | UInt32 | 交易在区块内序号 / 日志在区块内序号 |
version / is_deleted | UInt64 / 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 亿行地址全量 payload | 5.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.sql | traces.md。只由 follow --traces 实时写入:生产从 2026-09-25 起采集,只覆盖跟块区间,更早的历史没有 trace,缺口记在 _trace_gaps |
tokens(代币元数据) | sql/tokens.sql | tokens.md(由 enrich-tokens 子命令填充) |
erc20_balances(余额/持仓) | 027_erc20_balances.sql / 028_erc20_balances_backfill.sql | 0005-erc20-balances.md |
stock_tokens(代币化股票注册表) | 007_stock_tokens.sql | stock-tokens.md |
| DEX 成交 | 008_dex.sql | dex.md |
NFT(ERC1155 / 持有人,区别于 erc721_transfers 的转账流水) | 009_erc1155_nft_owners.sql | nft.md |
contracts(合约注册表) | 010_contracts.sql | contracts.md |
| 地址标签 | 011_address_labels.sql | labels.md |
| 跨链桥 | 012_bridge_flows.sql | bridge.md |
| 手续费 | 013_fees.sql | fees.md |
| 活跃用户 | 014_active_users.sql | active-users.md |
| 事件签名 / 方法选择器 | 015_event_signatures.sql / 016_method_selectors.sql | event-signatures.md / method-selectors.md |
| 预言机价格 | 017_oracle_prices.sql / 017_oracle_prices_backfill.sql | oracle-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 的坑),本节不重复展开:
- 二进制列不是十六进制字符串:地址(
FixedString(20))、哈希/topic (FixedString(32))、data/input(String)都是原始字节。条件里用unhex('...')(不带0x),取出来给人看用hex(col)。WHERE addr = '0xabc...'永远查不到数据、也不报错, 最容易被误判为"这个地址真的没记录"。 UInt256一律toString():value/amount/token_id/gas_price等大数很多客户端 不能安全序列化,读出来先toString()转字符串再交给应用层按需转BigInt/Decimal; ERC20amount换算成人类可读数值必须在查询时用代币自己的decimals(本表不存储,也不知道)。FINAL的代价:小范围查询(先按block_number/tx_hash过滤再FINAL)够快, 默认优先用;全表/大范围扫描(例如按天聚合全表)改用argMax(col, version)模式,否则FINAL会拖慢查询甚至撑爆内存,见docs/queries.md例 3/5 的两种写法对比。- 别名同名遮蔽踩坑(实测于 ClickHouse 26.9):
SELECT hex(from) AS from ...这种别名和 原列同名的写法,会让同一层甚至外层的WHERE/GROUP BY悄悄把该名字解析成别名表达式本身, 不报错、直接返回 0 行,非常容易被误判为"没有数据"。本文档和docs/queries.md的例子 一律用xxx_hex/xxx_str这类不同名的别名规避,写查询时照此约定。 extra是兜底垃圾桶,不是稳定 schema:不同链系、不同交易类型往extra里塞的字段完全 不同(例如 Arbitrum 内部交易 vs. EIP-7702 交易),不要假设某个 key 一定存在;用JSONExtractString/JSONHas之类函数按需取,取不到就是这条数据确实没有这个字段。- 专链字段的
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)。这些都是正常的 "此字段对这一行不适用",不是数据缺失。 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=""(该交易类型的全部字段都被识别,无未提升字段) |