钱包画像数据集(wallet_profiles)
来源:schema/clickhouse/derived/024_wallet_profiles.sql。 面向直接用 SQL 分析链上活跃地址特征的内部团队:单地址终生画像、生命周期统计、累计交易与失败数、合约部署数、累计消耗的手续费、交互对手方去重数、触达的 ERC-20 代币种类数。
来源:schema/clickhouse/derived/024_wallet_profiles.sql。
面向直接用 SQL 分析链上活跃地址特征的内部团队:单地址终生画像、生命周期统计、累计交易与失败数、合约部署数、累计消耗的手续费、交互对手方去重数、触达的 ERC-20 代币种类数。
依赖 {db}.transactions(见 sql/schema.sql)与 {db}.erc20_transfers(见 schema/clickhouse/derived/001_erc20_transfers.sql)。
1. 字段说明与计算口径
{db}.wallet_profiles 为终生画像聚合视图(普通 VIEW),底层由按天去重存储的中间表 {db}.wallet_daily_activity(ReplacingMergeTree)驱动。
| 字段 | 类型 | 说明 |
|---|---|---|
address | FixedString(20) | 钱包地址(原始字节,查询时用 hex() 输出) |
first_seen_block | UInt64 | 该钱包首次作为发送方(from)发交易的区块高度 |
first_seen_time | DateTime('UTC') | 首次发交易的时间戳(UTC) |
last_seen_block | UInt64 | 该钱包最近一次发交易的区块高度 |
last_seen_time | DateTime('UTC') | 最近一次发交易的时间戳(UTC) |
tx_sent | UInt64 | 终生累计发起交易数(count()) |
tx_failed | UInt64 | 终生累计失败交易数(countIf(status = 0)) |
contracts_created | UInt64 | 终生累计顶层创建合约数(countIf(to IS NULL AND contract_address IS NOT NULL)) |
fees_paid | UInt256 | 终生累计消耗交易手续费(wei),纯整数运算无浮点 |
distinct_counterparties | UInt64 | 终生直接交互的去重对手方地址数(发起交易的非空 to 地址集合的并集基数) |
erc20_tokens_touched | UInt64 | 终生触达的去重 ERC-20 代币合约数(作为发送方或接收方的代币合约去重) |
1.1 "钱包" 的纳入范围(Scope)
- 发起行为定界:只有在
{db}.transactions.from出现过至少一次的主动发起方,才会被收入本表。只作为交易接收方(to)的合约地址,或者从未发过交易但接收过转账的纯被动地址,不会生成画像。 - ERC-20 代币触达(
erc20_tokens_touched):在满足前述发起条件的前提下,钱包在erc20_transfers中作为转出方(from)或转入方(to)所涉及的所有 ERC-20 代币合约地址均计入统计(跨天状态通过uniqExactMerge准确求并集,不会因同一代币在多日出现而重复计数)。 - 对手方去重(
distinct_counterparties):仅统计普通交易的直接接收方(to IS NOT NULL)。顶层部署合约交易(to IS NULL)不增加对手方计数。
1.2 金额精度安全性(Money-safety)
手续费计算严格遵守项目规范,全程使用整数:
sum(toUInt256(gas_used) * ifNull(effective_gas_price, ifNull(gas_price, toUInt256(0)))) AS fees_paidgas_used 与 effective_gas_price 均为链上原始无损整数,乘积精确保留至 UInt256,禁止使用任何浮点数、缩放或浮点除法。
2. 正确性设计:为什么采用日粒度 ReplacingMergeTree + 最终视图,而非 AggregatingMergeTree
在设计每日刷新派生表时,面临两种经典方案的选择:
2.1 为什么不能用插入触发型物化视图(Double-counting)
源表 {db}.transactions 和 {db}.erc20_transfers 均为 ReplacingMergeTree(version, is_deleted)。在 backfill 断点续跑重叠插入或链发生深度重组写入墓碑行(is_deleted=1, 更高版本)时,插入触发型 MV 会在去重前直接累加,导致重复计数或无法撤销已回滚的交易。
2.2 为什么拒绝按 address 聚合的 AggregatingMergeTree
直觉上可能倾向于建立一张以 address 为主键的 AggregatingMergeTree,各字段存 AggregateFunction 状态(如 sumState、minState、uniqExactState),每天用物化视图刷新。该方案已被实证否定:
AggregatingMergeTree在后台 merge 时对同 key 记录只能做累加合并(combine),无法做"替换(replace)"。- 为了在链重组或修复时自动自愈,定时刷新通常包含滑动回看窗口(如
LOOKBACK_DAYS = 3)。如果使用AggregatingMergeTree,同一天的数据被重跑 3 次,其tx_sent、fees_paid等状态就会被累加 3 次,产生严重漂移。 - 即便按
(day, address)建AggregatingMergeTree,由于minState/maxState/uniqExactState等状态不可逆,一旦某天重组导致交易减少,也无法通过累加扣减。
2.3 选定架构:ReplacingMergeTree(refreshed_at) 日粒度局部汇总 + 视图终化
- 日粒度中间表
{db}.wallet_daily_activity:- 引擎:
ReplacingMergeTree(refreshed_at),主键ORDER BY (day, address)。 - 数据源对齐:通过
tx_day FULL OUTER JOIN erc20_day USING (day, address)统一按天汇总发送交易与代币触达。即使某地址在某日只有 ERC-20 代币转入/转出而未主动发交易,当天的触达代币集合状态也会完整保存入库。 - 数值指标(
first_seen_block/time,last_seen_block/time):类型为Nullable,在仅有 ERC-20 交互的日期为NULL,防止被 0 或 1970 纪元值污染。 - 交易标量(
tx_sent,tx_failed,contracts_created,fees_paid):非空整数,无交易日默认补 0。 - 集合指标(
distinct_counterparties_state,erc20_tokens_touched_state)保存为uniqExactState。 - 幂等替换:无论是定时重算回看窗口还是手动重跑历史,重新插入同一
(day, address)会写入新的refreshed_at,在FINAL/merge 时整行覆盖旧版本,完全免疫重复运行与状态漂移。
- 引擎:
- 终化汇总视图
{db}.wallet_profiles:- 普通 VIEW,执行
FROM {db}.wallet_daily_activity FINAL GROUP BY address HAVING tx_sent > 0。 - 发起者边界保障:通过
HAVING tx_sent > 0严格保障仅纳入主动发起过交易的钱包,纯被动接收代币的地址自动排除。 - 跨天自然正交:每个交易严格归属于某一天,因此数值指标跨天执行
min、max、sum绝对不会跨日重复计算;首末现高度与时间通过assumeNotNull(min(...))精确恢复为非空类型。 - 跨天集合合并:对手方与触达代币通过
uniqExactMerge准确求全生命周期的并集去重,即使代币触达分散在无交易日也能完整并入。
- 普通 VIEW,执行
3. 已知边界与局限说明
- 已收盘日约束(Closed Days):
刷新物化视图只处理严格已收盘的 UTC 日期(
toDate(block_timestamp) < today()),当天尚未收盘的数据不会计入画像,需等次日 02:00 UTC 定时刷新。 - 重组至零活跃的极端场景:
若某地址在某日仅发送了 1 笔交易,随后该交易被重组回滚且该地址当天再无其他交易,则重跑该日时查询结果为 0 行,ClickHouse 不会写入新行,因此无法自动覆盖旧版本(与
004_daily_chain_stats、005_daily_token_stats属同一已知特性)。在实际 L2 环境中极端罕见。
4. 安装与补算
4.1 前置依赖
{db}.transactions(由 raw loader /chain-indexer创建)。{db}.erc20_transfers(由schema/clickhouse/derived/001_erc20_transfers.sql创建并回填)。
4.2 执行安装与历史补算
替换 {db}(例如 robinhood)后执行 024_wallet_profiles.sql:
sed 's/{db}/robinhood/g' schema/clickhouse/derived/024_wallet_profiles.sql | clickhouse-client --multiquery如果通过 HTTP 接口执行(例如 curl),请将文件中的语句拆分为独立请求依次发送:
CREATE TABLE {db}.wallet_daily_activityCREATE MATERIALIZED VIEW {db}.mv_wallet_daily_activityINSERT INTO {db}.wallet_daily_activity ...(一次性回填全量已收盘历史< today())CREATE VIEW {db}.wallet_profiles
4.3 调度刷新与运维
- 物化视图定义了
REFRESH EVERY 1 DAY OFFSET 2 HOUR RANDOMIZE FOR 30 MINUTE APPEND,在每日 UTC 02:00(带随机抖动)自动重新计算过去 3 天的数据追加写入。 - 手动触发重跑:
SYSTEM REFRESH VIEW {db}.mv_wallet_daily_activity; -- 查看刷新执行情况 SELECT database, view, status, last_refresh_result, next_refresh_time FROM system.view_refreshes WHERE database = 'robinhood' AND view = 'mv_wallet_daily_activity'; - 补算幂等性验证:多次重新执行第 3 步的
INSERT语句或多次触发刷新,中间表因ReplacingMergeTree(refreshed_at)机制保证在FINAL视图查询时结果完全一致,不会产生数据漂移。
5. 查询示例
5.1 累计消耗手续费最高的 10 个钱包
SELECT
hex(address) AS wallet,
tx_sent,
tx_failed,
contracts_created,
fees_paid,
distinct_counterparties,
erc20_tokens_touched
FROM robinhood.wallet_profiles
ORDER BY fees_paid DESC
LIMIT 10;5.2 部署合约最多的活跃钱包
SELECT
hex(address) AS deployer,
contracts_created,
tx_sent,
first_seen_time,
last_seen_time
FROM robinhood.wallet_profiles
WHERE contracts_created > 0
ORDER BY contracts_created DESC, tx_sent DESC
LIMIT 10;5.3 查询指定钱包地址画像
SELECT
hex(address) AS wallet,
first_seen_block,
first_seen_time,
last_seen_block,
last_seen_time,
tx_sent,
tx_failed,
contracts_created,
fees_paid,
distinct_counterparties,
erc20_tokens_touched
FROM robinhood.wallet_profiles
WHERE address = unhex('40FFC583EFEF7396F623F73DBC37F5CA79744AEB');6. 本地验证结果(真实链上数据)
在本地 ClickHouse(wallet_profiles_wt49 库)中回填了真实 Robinhood 链(chain id 4663)跨收盘日与当前日的 4,000 个区块(71780000–71781999 归属 2026-09-24 已收盘日,16,333 笔交易;72060000–72061999 归属 2026-09-25 正在进行中,13,288 笔交易)。
6.1 5 个典型地址全字段独立双路比对
选取 5 个具有不同行为特征的代表性地址(包含合约创建者、多代币交互者、高失败率账户、单笔部署账户等),分别从(A){db}.wallet_profiles 视图,以及(B)直接针对原始 {db}.transactions FINAL 与 {db}.erc20_transfers FINAL 执行独立 SQL 查询进行对比。
全部 5 个地址的 11 个字段逐项 100% 精确匹配:
| 地址(hex) | 首现区块 / 时间 | 末现区块 / 时间 | 发送交易数 | 失败交易数 | 部署合约数 | 累计手续费 (wei) | 对手方数 | 触达代币数 | 比对结果 |
|---|---|---|---|---|---|---|---|---|---|
3A19F687A521BA84003279D04373F1FC433F3B99 | 717808642026-09-24 23:57:31 | 717809792026-09-24 23:57:42 | 4 | 0 | 2 | 315,668,711,840,000 | 2 | 0 | ✅ 11/11 精确匹配 |
40FFC583EFEF7396F623F73DBC37F5CA79744AEB | 717802302026-09-24 23:56:28 | 717817422026-09-24 23:58:59 | 9 | 0 | 1 | 71,124,732,662,000 | 7 | 8 | ✅ 11/11 精确匹配 |
8DEBBF0E76A8C8499760BC6448B31813592EB5AD | 717800432026-09-24 23:56:09 | 717819572026-09-24 23:59:21 | 38 | 35 | 0 | 76,548,289,214,000 | 2 | 0 | ✅ 11/11 精确匹配 |
BD4734584862D13C303296355DF23D9F4B5CF5BB | 717803882026-09-24 23:56:44 | 717819792026-09-24 23:59:24 | 53 | 0 | 0 | 464,791,213,744,000 | 34 | 35 | ✅ 11/11 精确匹配 |
8FA127024719C7A9B429002C38855F90320FD08E | 717818012026-09-24 23:59:05 | 717818012026-09-24 23:59:05 | 1 | 0 | 1 | 23,473,758,628,000 | 0 | 1 | ✅ 11/11 精确匹配 |
6.2 链上证据追踪(真实交易哈希)
- 地址
0x3a19f687a521ba84003279d04373f1fc433f3b99:- 区块 71780864 发起交易
0x0229abc5e5110e45fe40d19fa7384c1c90d079f83e4e5925f11619a4dc294619部署合约0xd22d0fa778ba24daf7459df4af93f77aa894f68e(gas_used=3,730,937, effective_gas_price=42,844,000)。 - 区块 71780904 发起交易
0xe9d1f0ca455445f712cdfb212fed337de87accf1396bf3ed16cbf38a6495f143部署合约0x9198e4c13fdc2e445cbba4c33ce77d21bdddab106(gas_used=3,023,069, effective_gas_price=43,052,000)。
- 区块 71780864 发起交易
- 地址
0x8debbf0e76a8c8499760bc6448b31813592eb5ad(高失败率典型):- 区块 71780043 发起失败交易
0x1c06467d03496167cc4b9981d841ffd3652fc9d32781664de31c93dc4cbdbe81(status=0, gas_used=34,071, price=43,138,000)。 - 区块 71780225 发起失败交易
0x6709a4703571b37e3d0ad36a34cc7971f8c545de30a11008250de095350baeb4(status=0, gas_used=33,591, price=43,120,000)。
- 区块 71780043 发起失败交易
- 地址
0x8fa127024719c7a9b429002c38855f90320fd08e:- 区块 71781801 仅发 1 笔交易
0xb56d0c78f857c3e0ef4741900e5fb3a09ce80756c252ec02da518062505fcbfd部署合约0x6a23d69cd3272007be82a35b16f37524d1869e51。
- 区块 71781801 仅发 1 笔交易
6.3 幂等性与重跑验证
在已完成初始安装与回填的前提下,重复执行全量已收盘日回填 INSERT 语句至第 3 次:
wallet_daily_activity表物理行数因追加增加至 14,172 行。wallet_profiles视图经过FINAL折叠后:- 活跃钱包总数:4,724
- 累计发送交易总数:16,333
- 累计失败交易总数:3,948
- 累计合约部署总数:10
- 累计消耗手续费总和:112,014,177,042,136,000 wei 所有汇总数字与单次执行完全一致,零误差,零漂移。
6.4 跨天多日代币触达全量比对验证(5 天 / 5,000 区块 / 11,982 钱包)
为了彻底验证地址在非交易发送日的 ERC-20 转账代币能否被准确捕获,使用真实链上 5,000 个区块(60,222 笔交易、72,152 笔 ERC-20 转账)分布于 5 个已收盘日(today()-8, today()-5 位于回看窗口外;today()-3, today()-2, today()-1 位于回看窗口内)进行全量独立重新计算与双路比对:
- 全量 11,982 个活跃钱包全字段比对:
- 视图
wallet_profiles与直接针对原始transactions FINAL+erc20_transfers FINAL独立聚合的结果执行FULL OUTER JOIN逐字段比对。 - 全部 11 个字段在全部 11,982 个钱包上 100% 精确一致(首现高度、首现时间、末现高度、末现时间、发送交易数、失败交易数、部署合约数、手续费、对手方去重数、触达代币去重数等差值均为 0)。
- 视图
- 典型跨天被动接收代币地址抽检:
- 地址
0xCD6B980029E6E6E0733AC8EC3E02BE9410D09799仅在 2026-09-22 发送了 7 笔交易(涉及 7 种代币),但在 5 个不同日期均存在 ERC-20 接收/转出记录,终生触达 24 种不同代币。 - 修复前因
LEFT JOIN导致无主动交易日的 17 种代币被丢弃(仅显示 7);修复后FULL OUTER JOIN准确汇总全部 5 日状态,wallet_profiles正确返回 24。
- 地址
- 幂等性与重组自愈验证:
- 执行 2 次历史补算追加 + 2 次强制物化视图刷新(
SYSTEM REFRESH VIEW)后执行OPTIMIZE TABLE wallet_daily_activity FINAL,物理记录精确折叠为 26,019 行(唯一(day, address)主键数),wallet_profiles结果完全一致。 - 对回看窗口内某钱包的交易注入重组墓碑记录(
is_deleted=1, version=version+1),触发物化视图刷新后,对手方数与手续费等指标正确回退,全量比对保持 0 误差。
- 执行 2 次历史补算追加 + 2 次强制物化视图刷新(
用户活跃度数据集(active users)
面向"每日活跃地址数(DAU)/ 新增地址 / 周月活跃(WAU/MAU)/ D1-D7-D30 留存"这一类 增长分析查询,按 已收盘的 UTC 自然日 维度预计算好,避免内部团队每次都要现写一遍 "排除系统地址 + 排除协议自动交易 + 去重版本 + 处理 tombstone"这一整套口径。
交互图谱数据集(interaction_edges_daily / token_flow_edges_daily)
交互图谱数据集按 已收盘的 UTC 自然日 聚合链上地址之间的互动关系,产出两张核心聚合表: 1. interaction_edges_daily:基于顶层交易(transactions)的外层调用边(from -> to),记录每日调用频次、原生代币金额、Gas 消耗与成功笔数。 2. token_flo…