【问题标题】:Fastest Way to Count Distinct Values in a Column, Including NULL Values计算列中不同值(包括 NULL 值)的最快方法
【发布时间】:2011-11-14 15:34:44
【问题描述】:

Transact-Sql Count Distinct 操作对列中的所有非空值进行计数。我需要计算一组表中每列的不同值的数量,包括空值(所以如果列中有空值,结果应该是(Select Count(Distinct COLNAME) From TABLE) + 1

这将在数据库中每个表的每一列中重复。包括数百个表,其中一些表超过 1M 行。因为这需要在每一列上完成,所以为每一列添加索引不是一个好的选择。

这将作为 ASP.net 站点的一部分完成,因此与代码逻辑的集成也可以(即:这不必作为一个查询的一部分完成,但如果可以以良好的性能完成的话,然后更好)。

最有效的方法是什么?


测试后更新

我根据一张具有代表性的表格给出的答案测试了不同的方法。该表有 320 万条记录,几十列(少数有索引,大多数没有)。一列有 320 万个唯一值。其他列的范围从所有 Null(一个值)到最多 40K 个唯一值。对于每种方法,我进行了四次测试(每次尝试多次,平均结果):一次 20 列,一次 5 列,1 列有很多值(3.2M)和 1 列有少量值( 167)。以下是结果,按从快到慢的顺序排列

  1. Count/GroupBy (Cheran)
  2. CountDistinct+SubQuery (Ellis)
  3. dense_rank (Eriksson)
  4. Count+Max (Andriy)

测试结果(以秒为单位):

   Method          20_Columns   5_Columns   1_Column (Large)   1_Column (Small)
1) Count/GroupBy      10.8          4.8            2.8               0.14       
2) CountDistinct      12.4          4.8            3                 0.7         
3) dense_rank        226           30              6                 4.33 
4) Count+Max          98.5         44             16                12.5        

注意事项:

  • 有趣的是,最快的两种方法(到目前为止,两者之间只有很小的差异)都是为每列提交单独查询的方法(在结果 #2 的情况下,查询包括一个子查询,因此每列实际上提交了两个查询)。可能是因为与内存需求方面的性能损失相比,通过限制表扫描次数获得的收益很小(只是猜测)。
  • 虽然dense_rank方法绝对是最优雅的,但它似乎并不能很好地扩展(见20列的结果,这是迄今为止四种方法中最差的),即使是小规模也不能与Count 的表现一较高下。

感谢您的帮助和建议!

【问题讨论】:

  • 这是一个非常令人沮丧的问题。 COUNT(DISTINCT ...) 几乎完成了所需的操作,甚至通知您它已经丢弃了 NULLWarning: Null value is eliminated by an aggregate or other SET operation. 但这只会在 Cheran 的答案同样有效的单列查询中使用。即使列不是NULL-able,但有时COUNT(DISTINCT ...) 是最好的,有时是DENSERANK 版本。然而,这两种计划形状似乎都很固定。
  • 如果您对具有n 列的表中的所有列执行此操作,则dense_rank 版本将有效地复制整个表并以n 方式对副本进行排序。我希望最好的用例是在执行(非索引)列的子集时,其中未包含的列存储在数据页中并且非常宽。

标签: asp.net sql sql-server database tsql


【解决方案1】:
SELECT COUNT(*)
FROM (SELECT ColumnName
      FROM TableName
      GROUP BY ColumnName) AS s;

GROUP BY 选择不同的值,包括 NULL。 COUNT(*) 将包含 NULL,而 COUNT(ColumnName) 会忽略 NULL。

【讨论】:

  • +1 更喜欢 Mikael 的答案,但这应该比 OP 提出的方法更快,特别是在该列没有被索引且不包含任何 NULL 的情况下,那么这需要 1 次扫描而不是超过 2 次完整扫描。
【解决方案2】:

我认为您应该尽量减少表扫描次数,并一次性计算一张表中的所有列。这样的事情值得一试。

;with C as
(
  select dense_rank() over(order by Col1) as dnCol1,
         dense_rank() over(order by Col2) as dnCol2
  from YourTable
)
select max(dnCol1) as CountCol1,
       max(dnCol2) as CountCol2
from C       

SE-Data 测试查询

【讨论】:

  • documentation for dense rank 声明“如果两个或更多行在同一分区中的排名相同,则每个并列的行都会获得相同的排名”。因此,这意味着如果 10 行表在 X {Null, 1, 1, 1, 2, 2, 2, 3, 3, 3} 列中具有以下值,则该列的 max(dense_rank()) 将为2(因为值 1、2 和 3 都将排在 1,而 Null 将排在 2)。如果是这种情况,那么如何将其用作衡量列中值数量的准确方法?
  • @Yaakov - 1,2,3 具有不同的值,因此排名不同。
【解决方案3】:

OP 自己的解决方案的开发:

SELECT
  COUNT(DISTINCT acolumn) + MAX(CASE WHEN acolumn IS NULL THEN 1 ELSE 0 END)
FROM atable

【讨论】:

  • 这确实需要对MAX 位进行单独扫描,但看起来无论列数如何,都只需要一次这样的额外扫描,并且它确实允许使用索引来避免排序操作,如果它们存在(如果不存在,则更窄,内存消耗更少)。
  • @Martin Smith:确实,有两次扫描,我对此感到有些困惑。不过,不能否认事实。不管怎样,感谢您的透彻分析!
  • 虽然我下面的方法需要两次扫描,但实际上它是否会因为它只返回第一行(如果有)而在其中包含 NULL 的事实而获得任何性能提升。对于没有 NULL 的列,这需要进行全表扫描。但是,如果列中有任何 NULL,那么它只需要扫描表的一个子集(在一个 300 万行的表中,如果第一个 NULL 出现在 10K 记录处,您最终扫描的行会少得多)。你怎么看?
  • @Yaakov Ellis:这个解决方案一开始就不是最有效的解决方案(至少可以说),当仅应用于一列时,可能不会击败你的解决方案.但是,如果您需要计算多个列(在同一个表中)的不同值,则您的方法将需要对每列计数进行两次扫描,而据我所见,我的方法将在每个 COUNT( DISTINCT) 加一为所有 MAX。
【解决方案4】:

运行一个查询,计算不同值的数量,如果列中有任何 NULL,则加 1(使用子查询)

Select Count(Distinct COLUMNNAME) +
       Case When Exists 
                 (Select * from TABLENAME Where COLUMNNAME is Null) 
            Then 1 Else 0 End
From TABLENAME

【讨论】:

  • 第 3 步可以使用 EXISTS 检查是否存在任何 NULL 值。
【解决方案5】:

你可以试试:

count(
distinct coalesce(
    your_table.column_1, your_table.column_2
    -- cast them if you want replace value from column are not same type
    )
) as COUNT_TEST

函数coalesce 帮助您将两列合并为replace not null 值。

我在我的案例中使用了这个,并成功获得了正确的结果。

【讨论】:

    【解决方案6】:

    不确定这是否是最快的,但可能值得测试。用例给 null 一个值。显然,您需要为实际数据中不会出现的空值选择一个值。根据查询计划,这将是 Cheran S 提出的 count(*) (group by) 解决方案的死胡同。

        SELECT 
            COUNT( distinct
                     (case when [testNull] is null then 'dbNullValue' else [testNull] end)
                 )
        FROM [test].[dbo].[testNullVal]
    

    用这种方法也可以统计多列

        SELECT 
            COUNT( distinct
                     (case when [testNull1] is null then 'dbNullValue' else [testNull1] end)
                 ),
            COUNT( distinct
                     (case when [testNull2] is null then 'dbNullValue' else [testNull2] end)
                 )
        FROM [test].[dbo].[testNullVal]
    

    【讨论】:

    • 这将无法使用索引来避免排序操作,您可以使用什么哨兵值来保证不会出现在数据中并且适用于所有数据类型。
    • @Martin Smith 显然可以找到一个几乎唯一的文本值。甚至 GUID 也不能保证是唯一的,并且在实践中被认为是唯一的。
    • @Martin Smith 我正在使用 SQL Express 2008 R2。我在列上放置了一个索引,查询计划显示它正在使用该索引。查询计划也有排序操作。
    • @martin Smith 我查看了查询计划并使用索引在 count() (分组依据)上避免了排序。使用索引 count() (分组依据)将获胜。使用我建议的方法与使用 count()(分组依据)的多个查询相比,可能值得在一个查询中测试多个列,但 count()(分组依据)可能会胜出。
    猜你喜欢
    • 2021-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多