在 SQL Server 中一次性获取 DISTINCT COUNT
Posted
技术标签:
【中文标题】在 SQL Server 中一次性获取 DISTINCT COUNT【英文标题】:Get DISTINCT COUNT in one pass in SQL Server 【发布时间】:2018-05-23 22:11:00 【问题描述】:我有一张如下表:
Region Country Manufacturer Brand Period Spend
R1 C1 M1 B1 2016 5
R1 C1 M1 B1 2017 10
R1 C1 M1 B1 2017 20
R1 C1 M1 B2 2016 15
R1 C1 M1 B3 2017 20
R1 C2 M1 B1 2017 5
R1 C2 M2 B4 2017 25
R1 C2 M2 B5 2017 30
R2 C3 M1 B1 2017 35
R2 C3 M2 B4 2017 40
R2 C3 M2 B5 2017 45
...
我写了下面的查询来聚合它们:
SELECT [Region]
,[Country]
,[Manufacturer]
,[Brand]
,Period
,SUM([Spend]) AS [Spend]
FROM myTable
GROUP BY [Region]
,[Country]
,[Manufacturer]
,[Brand]
,[Period]
ORDER BY 1,2,3,4
产生如下内容:
Region Country Manufacturer Brand Period Spend
R1 C1 M1 B1 2016 5
R1 C1 M1 B1 2017 30 -- this row is an aggregate from raw table above
R1 C1 M1 B2 2016 15
R1 C1 M1 B3 2017 20
R1 C2 M1 B1 2017 4 -- aggregated result
R1 C2 M2 B4 2017 25
R1 C2 M2 B5 2017 30
R2 C3 M2 B4 2017 40
R2 C3 M2 B5 2017 45
我想在上表中添加另一列,显示由Region
、Country
、Manufacturer
和Period
分组的Brand
的DISTINCT COUNT
。所以决赛桌会变成这样:
Region Country Manufacturer Brand Period Spend UniqBrandCount
R1 C1 M1 B1 2016 5 2 -- two brands by R1, C1, M1 in 2016
R1 C1 M1 B1 2017 30 1
R1 C1 M1 B2 2016 15 2 -- same as first row's result
R1 C1 M1 B3 2017 20 1
R1 C2 M1 B1 2017 4 1
R1 C2 M2 B4 2017 25 2
R1 C2 M2 B5 2017 30 2
R2 C3 M2 B4 2017 40 2
R2 C3 M2 B5 2017 45 2
我知道如何通过三个步骤获得最终结果。
运行此查询(查询 #1):
选择 [区域] ,[国家] ,[制造商] ,[时期] ,COUNT(DISTINCT [品牌]) AS [品牌数量] 进入温度1 从我的表 按 [地区] 分组 ,[国家] ,[制造商] ,[期间]
运行此查询(查询 #2)
选择 [区域] ,[国家] ,[制造商] ,[品牌] ,YEAR([Period]) AS 期间 ,SUM([花费]) AS [花费] 进入温度2 从我的表 按 [地区] 分组 ,[国家] ,[制造商] ,[品牌] ,[期间]
然后LEFT JOIN
Temp2
和Temp1
从后者引入[BrandCount]
,如下所示:
选择一个。* ,b.* 从 Temp2 作为 LEFT JOIN Temp1 AS b ON a.[Region] = b.[Region] 和 a.[国家] = b.[国家] 和 a.[广告商] = b.[广告商] AND a.[期间] = b.[期间]
我很确定有一种更有效的方法可以做到这一点,是吗?提前感谢您的建议/回答!
【问题讨论】:
【参考方案1】:从这个问题中大量借鉴:https://dba.stackexchange.com/questions/89031/using-distinct-in-window-function-with-over
Count Distinct 不起作用,因此需要 dense_rank。将品牌按正序和倒序排列,然后减 1 得出不同的计数。
您的 sum 函数也可以使用 PARTITION BY
逻辑重写。这样您就可以为每个聚合使用不同的分组级别:
SELECT
[Region]
,[Country]
,[Manufacturer]
,[Brand]
,[Period]
,dense_rank() OVER
(PARTITION BY
[Region]
,[Country]
,[Manufacturer]
,[Period] Order by Brand)
+ dense_rank() OVER
(PARTITION BY
[Region]
,[Country]
,[Manufacturer]
,[Period] Order by Brand Desc)
- 1
AS [BrandCount]
,SUM([Spend]) OVER
(PARTITION BY
[Region]
,[Country]
,[Manufacturer]
,[Brand]
,[Period]) as [Spend]
from
myTable
ORDER BY 1,2,3,4
然后您可能需要减少输出中的行数,因为此语法提供的行数与 myTable 相同,但聚合总数显示在它们适用的每一行上:
R1 C1 M1 B1 2016 2 5
R1 C1 M1 B1 2017 2 30 --dup1
R1 C1 M1 B1 2017 2 30 --dup1
R1 C1 M1 B2 2016 2 15
R1 C1 M1 B3 2017 2 20
R1 C2 M1 B1 2017 1 5
R1 C2 M2 B4 2017 2 25
R1 C2 M2 B5 2017 2 30
R2 C3 M1 B1 2017 1 35
R2 C3 M2 B4 2017 2 40
R2 C3 M2 B5 2017 2 45
从此输出中选择不同的行可以满足您的需求。
dense_rank 技巧的工作原理
考虑这些数据:
Col1 Col2
B 1
B 1
B 3
B 5
B 7
B 9
dense_rank() 根据当前项之前的不同项的数量加 1 对数据进行排名。所以:
1->1、3->2、5->3、7->4、9->5。
以相反的顺序(使用desc
)产生相反的模式:
1->5, 3->4, 5->3, 7->2, 9->1:
将这些等级加在一起得到相同的值:
1+5 = 2+4 = 3+3 = 4+2 = 5+1 = 6
这里的措辞很有帮助,
(number of distinct items before + 1) + (number of distinct items after + 1)
= number of distinct OTHER items before AND after + 2
= Total number of distinct items + 1
因此,要获得不同项目的总数,请将 ascending
和 descending
dense_ranks 加在一起并减去 1。
【讨论】:
感谢您的全面回答!立即看到为什么DESNSE_RANK
forward 和 reverse - 1 会与 Brand 的 DISTINCT COUNT
相同,这有点令人费解。如果有任何资源可以解释它为什么起作用,你能分享一下吗?我将验证结果并选择您的回复作为答案(因为它是最全面的)。我也将尝试在这里写下我在今晚思考后了解到为什么DENSE_RANK
技巧有效的评论。非常感谢!
非常感谢您的解释!现在对我来说为什么DENSE_RANK
应该起作用是有道理的。尽管 Martin 在下面的回答也很好(并且不需要之后对行进行重复数据删除),但我会接受这个。对于那些寻找答案的人,也请查看下面的 Martin 的答案。非常感谢,@mjsqu!
没问题,我一开始不明白,所以我必须创建一个工作示例来找出原因!【参考方案2】:
您问题的标签;
窗口函数
表明你有一个不错的主意。
对于按地区、国家、制造商和时期分组的品牌的 DISTINCT COUNT:您可以写:
Select Region
,Country
,Manufacturer
,Brand
,Period
,Spend
,DENSE_RANK() Over (Partition By Region, Country, Manufacturer, Period Order By Brand asc)
+ DENSE_RANK() Over (Partition By Region, Country, Manufacturer, Period Order By Brand desc)
-1 UniqBrandCount
From myTable T1
Order By 1,2,3,4
【讨论】:
【参考方案3】:双重dense_rank
想法意味着您需要两种排序(假设不存在提供排序顺序的索引)。假设没有 NULL
品牌(就像这个想法一样),您可以使用单个 dense_rank
和窗口 MAX
如下(demo)
WITH T1
AS (SELECT *,
DENSE_RANK() OVER (PARTITION BY [Region], [Country], [Manufacturer], [Period] ORDER BY Brand) AS [dr]
FROM myTable),
T2
AS (SELECT *,
MAX([dr]) OVER (PARTITION BY [Region], [Country], [Manufacturer], [Period]) AS UniqBrandCount
FROM T1)
SELECT [Region],
[Country],
[Manufacturer],
[Brand],
Period,
SUM([Spend]) AS [Spend],
MAX(UniqBrandCount) AS UniqBrandCount
FROM T2
GROUP BY [Region],
[Country],
[Manufacturer],
[Brand],
[Period]
ORDER BY [Region],
[Country],
[Manufacturer],
[Period],
Brand
上面有一些不可避免的假脱机(不可能以 100% 流式处理),但只有一种。
奇怪的是,最终的 order by 子句需要将排序数保持为 1(如果存在合适的索引,则为 0)。
【讨论】:
非常感谢您分享另一种(并且可能更有效)的方法!根据您的解释,我解释说使用DENSE_RANK
会导致一种排序,但MAX
不会(我的意思是,我可以在查询分析器输出中看到这一点,但只是想知道如何在没有排序的情况下找到MAX
值)?我仍然是学习 SQL 的新手,所以也许有一天,我将能够更好地理解内部工作原理。 :) 还有一个问题,假脱机是因为MAX
部分必须等待DENSE_RANK
部分完成?非常感谢!
@user1330974 - 从右到左从表中读取行并按[Region], [Country], [Manufacturer], [Period], Brand
的顺序排序。接下来的两个段和序列项目运算符计算dense_rank。具有这个计算出的 dense_rank 值的行被读入一个假脱机。一旦到达一个新的[Region], [Country], [Manufacturer], [Period]
组,那么前一个组的假脱机中的行就会计算它们的 MAX,然后重放假脱机中的行并添加这个 MAX 值。
更多关于dense_rank sqlblog.com/blogs/paul_white/archive/2010/07/28/…和窗口聚合sqlblog.com/blogs/paul_white/archive/2010/07/28/…
非常感谢您对如何阅读查询计划的详细说明以及分享一个非常好的资源/博客以阅读有关窗口功能的更多信息!由于您的解释,查询计划现在对我来说有部分意义,我可以确认您建议的解决方案有效并且只需要大约 6-8 秒!旁注:我接受了上面的另一个解决方案,因为它比你的要早一些,而且它也有效。希望你能理解。 :)以上是关于在 SQL Server 中一次性获取 DISTINCT COUNT的主要内容,如果未能解决你的问题,请参考以下文章
SQL Server中是否可以准确获取最后一次索引重建的时间?
分享一次在Windows Server2012 R2中安装SQL Server2008