别只盯着面板了,Dune Analytics 进阶指南,亲手写SQL,把链上数据玩出花
先问你一个问题,打开Dune,你是在搜索框里找现成的看板,还是直接点开那些热门查询,改改日期就截图发推?如果你只停留在这一步,那说实话,你看到的只是别人嚼过的馒头,链上数据真正的价值,藏在那些没人帮你算好的细节里,Dune进阶的核心,就是放下鼠标,拿起键盘,用SQL去跟区块链直接对话,这件事不玄乎,但确实得花点功夫,今天这篇,不聊概念,直接上手,从建库到查钱包,带你把SQL这把钥匙磨利。

第一步:别怕改代码,从复制开始
我知道,让你从零写一段SELECT语句,压力很大,但Dune的妙处在于,它有一个庞大的开源查询库,你看到的任何一个热门看板,点击进去,右下角都有“Query”按钮,点开它,那就是某个大神的完整SQL,你的第一步,不是关掉它,而是点“Fork”按钮,把它复制到你的工作区。
然后干什么?改,把WHERE后面的地址,换成你自己的钱包地址,把date_trunc('day', block_time)改成date_trunc('hour', block_time),看看粒度变细是什么效果,哪怕只是把LIMIT 100改成LIMIT 5,你也在理解这段逻辑的边界,这个过程叫“拆解”,你看别人怎么写DECODE函数来处理交易类型,看别人怎么用PIVOT函数把长表转成宽表,这比你上十节课都管用,复制不丢人,复制完不动脑子才丢人。
第二步:搞懂你的数据源——从Raw到Decoded的跳跃
Dune最强大的地方,在于它保留了原始数据表(Raw Tables),什么叫原始?就是区块链上那个data字段,一长串十六进制,比如0x095ea7b3...,普通人看这个就是天书,但高手看这个,眼里全是金矿,因为解码后的表(Decoded Tables),是Dune团队或社区通过ABI(接口定义)帮你翻译好的,比如erc20."ERC20_evt_Transfer",你直接能看到from、to、value三个字段,多清爽。
但进阶的瓶颈来了,当你想查一个冷门项目,他没有解码表,怎么办?这时候你就要冲进Raw数据的世界,比如你要查某个新出的DEX(去中心化交易所)的兑换记录,没有现成的Swap事件表,这时候,你得去看它的合约地址,拿到它的ABI,然后自己用UNNEST和CAST去手动解析data字段里的address和uint256,这个过程很痛苦,但一旦你解析成功,你就掌握了别人没有的数据渠道,这就叫信息差。
第三步:核心函数三件套——block_time、WALLET和DECODE
开始写查询时,你只需要记住三个最常用的东西,第一个,block_time,这不仅是时间戳,它是你所有时间序列分析的锚点,你想知道某个巨鲸每天买多少币,你得GROUP BY date_trunc('day', block_time),注意,不要用group by time,Dune里没这个函数,要用date_trunc或者date_bin(在Dune V2里更推荐date_bin,因为它更灵活,能自定义间隔)。
第二个,钱包地址,链上地址是不分大小写的,但Dune的底层存储是分大小写的,你查的时候,要么全大写,要么全小写,千万别混着用lower()函数包一下,更关键的是,要理解地址的格式,在Dune V2里,地址是0x开头的20字节十六进制,如果你直接写WHERE address = '0xXXXX',可能查不到,因为某些表里地址是bytea类型,你得用decode('0xXXXX', 'hex')或者直接写'\xXXXX'::bytea来比较,这个坑,十个人里八个会踩。
第三个,DECODE函数,这个跟数据库里的解码不一样,Dune里的DECODE用于把数字映射成文本,比如交易里有个type字段,0代表转账,1代表合约创建,你想看用户行为,就得用DECODE(type, 0, 'Transfer', 1, 'Create'),这能让你把枯燥的数字变成可读的标签,然后才能做图表。
第四步:性能优化——别让你的查询跑十分钟
Dune用的是Trino(原PResto)引擎,它很强大,但不是无脑的,你写一个JOIN,如果不加过滤条件,它会把两个全表进行笛卡尔积,直接跑死,进阶的关键在于“下推”,什么意思?就是在JOIN之前,先用WHERE把每个子表的数据量缩小。
举个例子,你要关联ethereum.transactions和ethereum.blocks,如果你直接JOIN再WHERE,那么两个千万级的大表会先拼接,再过滤,内存直接爆炸,正确做法是,先写子查询,在子查询里用WHERE block_time > now() - interval '7' day把数据限制在7天内,然后再去LEFT JOIN,这叫“先瘦身,再结婚”。能不用DISTINCT就不用,用GROUP BY代替,因为DISTINCT在Trino里执行效率更低,还有,避免在WHERE子句里写计算函数,比如WHERE block_time + 1 > now(),这会阻止索引使用,写成WHERE block_time > now() - interval '1' second。
第五步:实战案例——查一个巨鲸的买卖行为
光说不练假把式,我们来拆解一个典型需求:找出某个地址在Uniswap V3上,过去30天,每天买入和卖出的USDC总量。
第一步,找到数据源,Uniswap V3的兑换事件在uniswap_v3."Pair_evt_Swap"表里,注意,是Pair不是Pool,V2是Pair,V3是Pool,别搞混了,这张表里有amount0In、amount0Out、amount1In、amount1Out,我们的关键逻辑是:如果amount0In > 0且amount1Out > 0,说明这笔交易是卖出了token0换来了token1,反之亦然。
但这里有个坑,amount0In和amount0Out是带小数点的,单位是原始精度,你需要除以1e6(USDC是6位小数),或者除以1e18(ETH是18位),你不可能在SQL里写死,因为不同币精度不同,所以你要先查Pair_evt_Swap关联的tokens表,拿到精度。
好,我们简化一下,假设我们只关心token是USDC(地址0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48)的那个池子,查询思路是:
WITH swap_events AS (
SELECT
block_time,
erc20_pairs."token0" AS token0_address,
erc20_pairs."token1" AS token1_address,
"amount0In" AS amount0_in,
"amount1In" AS amount1_in,
"amount0Out" AS amount0_out,
"amount1Out" AS amount1_out
FROM uniswap_v3."Pair_evt_Swap"
WHERE block_time > now() - interval '30' day
)
SELECT
date_trunc('day', block_time) AS day,
SUM(
CASE
WHEN amount0_in > 0 AND amount1_out > 0
THEN amount1_out / 1e6 -- 卖出token0得到USDC
ELSE 0
END
) AS usdc_bought,
SUM(
CASE
WHEN amount1_in > 0 AND amount0_out > 0
THEN amount1_in / 1e6 -- 买入token0花费USDC
ELSE 0
END
) AS usdc_sold
FROM swap_events
WHERE token0_address = decode('a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 'hex')
OR token1_address = decode('a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 'hex')
GROUP BY 1
ORDER BY 1;
看这段SQL,我用了WITH子句先过滤时间,再用CASE WHEN做条件求和,重点在于,我在最底层的WHERE里就先限定了token0或token1是USDC,这样就不会去扫描所有其他币种的池子,性能直接提升百倍,这就是进阶思维:先定位,再聚合。
第六步:善用WALLET标签和Ethereum.Traces
你想查一个人干了什么,但他是通过合约交互的,比如他点击了Uniswap前端,但实际上他的钱包地址并没有直接出现在Swap事件里,而是出现了一个中间合约地址,这时候,你需要用到labels.wallet_address_to_labels表,把那个中间合约地址标成“用户A的执行代理”。
更高级的,你要查合约内部调用,比如一个DEX聚合器,内部其实拆成了5笔子交易,这时候普通的transactions表不够用了,得用ethereum.traces表,这个表记录了每一笔内部调用。
举个例子,你想看某个地址在1inch上花了多少Gas费,你不能只看transactions里的gas_used,因为那只是外层调用的Gas,你得去ethereum.traces里,找call_type = 'call'且to地址是1inch合约的记录,然后累加gas_used,这才算清楚他真正为这个交互付了多少手续费,用traces表时,记得用tx_hash和block_time做关联,并且要加WHERE success = true,因为失败的交易Gas也用了,但通常我们不统计。
第七步:把你的查询结果“物化”成看板
SQL写好了,只是完成了50%,你要去创建可视化。Dune的可视化类型就那么几种:条形图、折线图、饼图、表格、计数器,别贪多,你查了一个时间序列,就用折线图,你查了一个分布,就用柱状图,你查了一个占比,就用饼图。
最关键的一点,图表的X轴和Y轴的数据类型要跟查询结果匹配,你查出来day是timestamp类型,那么X轴就选“Time”并选day字段,如果你查出来是uint256类型,你直接把它放在Y轴上,数值会溢出显示,因为Trino里uint256是decimal类型,你需要CAST(amount AS DOUBLE)才能正常显示,别问我怎么知道的,我被这个坑了整整一下午。
还有,调整颜色和单位,Dune默认的柱状图颜色是深蓝色的,很丑,你可以自定义颜色代码,比如#0055ff,数值单位如果是美元,用前缀,并在Y轴格式里设置“Currency”,这样看起来专业,比默认的干巴巴数字强一百倍。
第八步:把查询变成别人填参数的“动态看板”
这是区分新手和老手的分水岭,在Dune里,你可以定义变量,比如在查询顶部写:
-- 定义一个参数
-- {{address}} 表示一个文本框输入
-- {{time_interval}} 表示一个下拉选择
WITH data AS (
SELECT
block_time,
CASE
WHEN '{{address}}' = '0x...' THEN 'default_address'
ELSE '{{address}}'
END AS wallet
FROM ethereum.transactions
WHERE block_time > now() - interval '{{time_interval}}'
)
...
这里的{{address}}和{{time_interval}}是Dune的模板语法,你在看板页面添加一个“控件”,选择“文本输入”绑定address,选择“下拉菜单”绑定time_interval,选项填上7 day、30 day、90 day,这样,任何来看你看板的人,都可以在页面上直接输入一个钱包地址,选择时间范围,图表就自动更新,这不叫技术,这叫产品思维,你把一个死的查询,变成了一个活的工具。
第九步:犯错是常态,你只需要一个“纠错”习惯
写SQL,每个人都会报错。Error: Column 'token_amount' cannot be resolved,这种错误,不用慌,检查两件事:第一,你的表里真的有这列吗?去Dune的左侧边栏,找到那一张表,展开看看字段名,第二,你的字段名打错了一个字母。amount0In和amount0_in是不一样的,严格区分大小写。
还有一种经常出的问题:date_trunc 的单位写错了,是'day',不是'days',是'hour',不是'hours',Dune的Trino引擎只接受单数形式。
括号匹配问题,复杂的CASE WHEN嵌套,最容易漏掉右括号,我的习惯是,写完一个子查询,先单独执行一遍,确认无误后再放到上层JOIN里,这叫做“自底向上测试”,千万别把500行SQL一次性写完再执行,那调试到天亮都找不出错。
最后一步:多看、多抄、多改
Dune的生态里,有一批顶尖的查询作者,比如hildobby、superland,他们的SQL写得像艺术品,你要做的,就是把他们的查询存下来,然后每天打开看,拆解他们的思路,看他们怎么用LAG函数计算环比变化,看他们怎么用ROW_NUMBER()和PARTITION BY来去重取最新值。
链上分析的价值,不是看已经发生的,而是从数据的缝隙里,嗅出接下来要发生的,比如追踪聪明钱,你需要写SQL去找出那些在Uniswap上频繁交易但从不亏钱的钱包,然后看你自己的钱包有没有在他们后面跟进,这一切,都从你写出第一条自定义SQL开始,别等教程了,现在就去Dune,点开一个热门查询,点Fork,改掉那个WHERE条件,把日期改小,看结果变化,那才是你的进阶第一步。






