在 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

我想在上表中添加另一列,显示由RegionCountryManufacturerPeriod 分组的BrandDISTINCT 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 JOINTemp2Temp1 从后者引入[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

因此,要获得不同项目的总数,请将 ascendingdescendingdense_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中是否可以准确获取最后一次索引重建的时间?

在 SQL Server 游标中获取多个值

SQL Server获取索引创建时间&重建时间&重组时间

分享一次在Windows Server2012 R2中安装SQL Server2008

sql server使用cte递归查询获取树形的父节点/子节点

记一次SQL Server的清理过程