
Tableau Visionary | Tableau Ambassdor
发布日期:2026年02月
在数据分析的日常工作中,我们经常会遇到这类需求:“既要看当前维度的聚合度量,又要看更大范围的对比参考”。也就是在一个问题中,同时包含了两个详细级别的聚合。

比如在最近的航空分析项目中,客户想看:每个机场在各年份的去重旅客数(COUNTD),同时在旁边显示该机场跨年的总去重旅客数。
如果你尝试在 SQL 中这样写:
-- 这在大多数数据库中都会报错
SELECT
dept_airport,
EXTRACT(YEAR FROM flight_date) as f_year,
COUNT(DISTINCT passenger_id) OVER(PARTITION BY dept_airport) as total_distinct
FROM fact_flight_tickets;你会收到一条冰冷的报错:
DISTINCT is not implemented for window functions为什么 SQL 关上了这扇门?为什么在 Tableau 中 TOTAL(COUNTD()) 却能轻松实现?这背后隐藏着数据分析技术中最核心的逻辑差异。

在数据库底层,典型的窗口函数都是关于排序计算,如RANK() OVER() 、ROW_NUMBER、LAG等。也有非典型的二次聚合窗口函数——之所以说不典型,是因为其他的计算也能够完成类似的场景。
而在不典型的窗口计算中,一个特别的案例是涉及到去重的二次聚合。正如开篇所讲,我们要计算不同年月不重合的旅客数量。
可惜的是,大部分数据库都不支持在窗口函数中使用去重计算,这种设定一部分是性能考虑。窗口函数的排序本来就额外占用大量的算力,如果再二次叠加去重的计算,那么窗口计算将更加缓慢。在 Tableau Desktop中,甚至不支持在明细级别的窗口函数。
如何在 SQL 中使用窗口函数完成“去重计数(COUNTD)”呢?处理这种非加性指标(Non-additive)并非窗口函数所擅长,推荐遵循“分层计算”的思维,即动用 CTE (通用表表达式) 或子查询来完成,如下所示:
-- 第一阶段:排除掉不需要的细节,强制降维
-- 这相当于 Tableau 的 EXCLUDE:排除掉具体的客票流水,降维到 (年, 城市, 旅客)
WITH passenger_grain AS (
SELECT
EXTRACT(YEAR FROM flight_date) as f_year,
dept_airport,
passenger_id
FROM fact_flight_tickets
GROUP BY 1, 2, 3 -- 这里实现了“去重”,生成了一张“旅客-城市-年份”的明细表
)
-- 第二阶段:基于降维后的结果进行窗口计算
SELECT
f_year,
dept_airport,
-- 此时的 COUNT(passenger_id) 已经是去重后的计数
COUNT(passenger_id) OVER(PARTITION BY dept_airport) as total_distinct_by_city
FROM passenger_grain;这就是 SQL 在面临多个详细级别时的常见处理逻辑:先处理粒度,后处理二次计算。
Tableau可以使用表计算 TOTAL( COUNTD(Passenger) ) 快速完成上述场景。
SQL窗口函数的“窗口”和Tableau表计算的“表”都是“范围”的意思,既然是范围,就可以设置范围的起点和终点。按照起点和终点能否变化,笔者把窗口函数由易到难分为3类,对应合计、累计汇总和移动汇总3个典型函数。

图9-44 窗口计算的3种特殊类型
很多用户会误以为total和 WINDOW_SUM 是一类,但其实不然。
在我看来,Tableau TOTAL 更像是一个 EXCLUDE LOD 计算,而非普通的窗口函数。它无关次序,结果具有“更高聚合度详细级别”的性质(当然不完全相同)(参见《数据可视化分析第二版》第十章)

所以,上述 SQL 中的 CTE 过程,本质上就是在模拟 Tableau 的 EXCLUDE:
Tableau的表计算和SQL窗口函数是可以相互转换的。

一旦明白了这样的关系,从IT转为业务分析的用户可以快速理解“表计算”的计算过程和SQL的对应关系,而像笔者一样缺乏SQL背景知识的业务分析师,则可以快速理解SQL中最重要的分析函数——窗口函数的应用原理。
在 SQL 中,还有一种看起来很奇怪的写法:SUM(SUM(Quantity)) OVER(PARTITION BY City)。
两个SUM写法虽同,但本质截然不同。
这就好比美国版的“特朗普”和中国版的“特朗普”虽然音声和手势类似,但只是形式上的某种类似。
SUM(SUM( )) OVER( )是典型的“聚合之上再开窗”,内层的 SUM 和 GROUP BY 相对应,外层的 SUM 和 OVER 相对应,我们常在计算占比(Percent of Total)时用到它。
SELECT city, year,
SUM(quantity) as current_sum,
-- 这是不典型的窗口计算:它不包含
ORDER BY
SUM(SUM(quantity)) OVER(PARTITION BY city) as total_sum_by_city
FROM orders
GROUP BY city, year;为什么说它“不典型”?
典型的窗口函数(如 RANK, LAG)依赖 ORDER BY 才能确定“谁是第一”或“谁是前一名”,从而完成排序计算。
排序是最典型的窗口函数,是其他方法难以完成的。
相比之下,SUM(SUM()) 这种写法只关心 Scope(范围)。它是在聚合结果(已经按年、城市分好的块)的基础上,再开一扇窗看透整个城市的所有年份。除了使用这种窗口函数,还可以使用嵌套子查询来更轻松地完成。
不过相比子查询,这种写法在逻辑和语法简洁度上明显更高效的,因为它让数据库在一次查询中同时完成了两个层级的聚合(明细聚合 + 范围汇总)。
可以用一个图示表示这个过程(我在“SQL别裁新解”中的图例):

一旦理解了窗口函数和子查询之间的相关性,我们甚至可以用子查询来完成任何一种窗口函数的计算,并进而理解窗口函数在逻辑上的简洁意义。
数据分析的技术工具繁多,但底层逻辑万变不离其宗:维度、度量(指标),以及计算所依赖的详细级别(颗粒度)(在 Power BI 中被称为上下文,但我个人不喜欢这个容易误解的词)。
我计划从 2026 年 启动一个更具雄心的项目——“SQL、Tableau 与 DAX 的系统性逻辑对比”,并最终推出《 SQL 别谈新解》和《 DAX 别裁新解》两本小册子(能否出版则依赖于更多的context)。

当然,广受业内关注的“国产 BI 测评”也会进入第二季,适时也会组织国产 BI 产品经理的座谈会,我们来看一下国产工具最近两三年的进步。
我希望通过这个项目,帮助更多的技术人员跳出“语法”的泥潭,进入“逻辑”的深处。无论你是精通 SQL 的数据库专家,还是擅长 DAX 的 Power BI 达人,亦或是深耕 Tableau 的分析专家,你都会发现,我们虽然用着不同的方言,但在数据逻辑的维度里,我们其实在说同一种思考逻辑。
2026 年,让我们在数据分析的技术逻辑巅峰见!
预祝大家新年快乐!