【问题标题】:Randomly generated values in CASE statements return NULLCASE 语句中随机生成的值返回 NULL
【发布时间】:2015-05-05 13:24:22
【问题描述】:

根据this post,SQL Server中生成随机值的正确方法是:

ABS(CHECKSUM(NewId())) % 14   -- Returns a value between 0 and 13

但是,当在 case 语句中使用此表达式时,例如:

SELECT
    CASE ABS(CHECKSUM(NEWID())) % 4 
        WHEN 0 THEN 'String A' 
        WHEN 1 THEN 'String B' 
        WHEN 2 THEN 'String C' 
        WHEN 3 THEN 'String D'
    END AS RandomString -- Returns String A, B, C, D and NULLs.
FROM sys.all_objects

outputtet RandomString 列包含一些 NULL,如 SQL fiddle 所示。我发现我可以将随机化表达式包装在 CTE 中以避免输出中出现 NULL,但我仍然想知道为什么上面的代码返回 NULL?

WITH RandomNumber AS (
    SELECT ABS(CHECKSUM(NEWID())) % 4 AS N FROM sys.all_objects
)
SELECT TOP 100
    CASE N
        WHEN 0 THEN 'String A' 
        WHEN 1 THEN 'String B' 
        WHEN 2 THEN 'String C' 
        WHEN 3 THEN 'String D'
    END AS RandomString -- Does not return any NULLs. Only String A, B, C and D.
FROM RandomNumber

我尝试使用稍微不同的方法生成随机数,但结果是一样的:

CAST(RAND(CHECKSUM(NEWID())) * 4 AS INT)  -- Returns a value between 0 and 3

这似乎是SQL Server 2014上的问题,我没有在其他版本上测试过。

【问题讨论】:

  • 为了获得更好质量的随机数,您可能需要考虑CRYPT_GEN_RANDOM

标签: sql-server tsql random case


【解决方案1】:

它会生成NULLs,因为无法保证特定表达式将被计算多少次。

您希望 SQL Server 执行以下操作:

let x = GenerateRandomNumber()
if x = 1 then 'String 1'
if x = 2 then 'String 2'
if x = 3 then 'String 3'
if x = 4 then 'String 4'

(其中GenerateRandomNumber()ABS(CHECKSUM(NEWID())) % 4);但 SQL Server 实际做的是:

if GenerateRandomNumber() = 1 then 'String 1'
if GenerateRandomNumber() = 2 then 'String 2'
if GenerateRandomNumber() = 3 then 'String 3'
if GenerateRandomNumber() = 4 then 'String 4'

因此,如果您碰巧为一个特定的比较操作选择了正确的随机数,您只会得到非NULL 的结果。

我认为即使使用 CTE,也无法保证 SQL Server 永远不会生成类似于上面第二个代码块的内容。如果您想要一个稳定,生成一次随机数,您需要安排将该值存储到某个地方(例如,在表变量或临时表中)。


我之所以关注保证,是因为您不想最终根据当前观察到的行为编写代码。当 SQL Server 2008 停止“排序”导致我们使用 TOP 100 PERCENT ... ORDER BY 技巧的视图时报告了很多“问题”——这在 2005 年及更早的版本中(大部分)发生了,但不再这样做了。

同样,如果有人向我询问返回数字 5 的表达式,我可以向他们提供表达式 DATEPART(day,GETUTCDATE()) 并让他们在接下来的 8 小时内对任意多的行运行任意数量的查询 -但这并不意味着我会推荐它作为他们问题的解决方案。

此外,我们知道 SQL Server 对evaluation order 的决定可能会令人惊讶。

【讨论】:

    【解决方案2】:

    表达式正在内部重写为与

    相同的逻辑
    CASE WHEN ABS(CHECKSUM(NEWID())) % 4 = 1 THEN x 
         WHEN ABS(CHECKSUM(NEWID())) % 4 = 2 THEN y 
         WHEN ...
    

    这就是问题中的语法永远不会起作用的原因。 正在为每个 WHEN 评估表达式。

    你可以修复它的方法是:

    SELECT 
        CASE x % 4 
            WHEN 0 THEN 'String A' 
            WHEN 1 THEN 'String B' 
            WHEN 2 THEN 'String C' 
            WHEN 3 THEN 'String D'
        END AS RandomString -- Returns String A, B, C, D and NULLs.
    FROM sys.all_objects
    CROSS APPLY (SELECT ABS(CHECKSUM(NEWID())) x) y
    

    使用 CROSS APPLY 只会计算一次

    【讨论】:

    • 我不确定即使CROSS APPLY 是否保证 只对表达式求值一次。 This question 似乎相关。
    • @Damien_The_Unbeliever x 只计算一次,尝试 SELECT x,x,x 代替。这将始终是相同的值
    • 我可以运行您的测试 10000 次,是的,请注意,该值始终相同。但是在第 10001 次,我可能会得到不同的结果,无论出于何种原因(错误修复/服务包、一天中的时间、服务器上的负载等)。这就是为什么我关注保证而不是观察到的行为。毕竟,SQL Server本可以采用 OP 的原始查询并按照他们预期的方式工作 - 那么您会说 OP 的原始查询是正确的吗?
    • 但同样,这是观察到的行为,而不是保证的行为。如果 记录 对每个表达式进行一次评估,我就不会提出这个问题。但这就是重点,它没有如此记录。
    • @Damien_The_Unbeliever 但是多次阅读该表达式是没有意义的。这只会违背数据的一致性和效率。对表的连接将采用读锁以确保数据一致。 select 语句在所有连接和 where 之后处理。 blog.sqlauthority.com/2009/04/06/…
    【解决方案3】:

    至于为什么,我不太确定。但就您的空问题而言,使其成为派生表并进行查询。真的不知道为什么。

    SELECT  CASE rand_num
                WHEN 0 THEN 'String A' 
                WHEN 1 THEN 'String B' 
                WHEN 2 THEN 'String C' 
                WHEN 3 THEN 'String D'
        END AS RandomString -- Returns String A, B, C, D and NULLs.
    FROM
    (
        SELECT ABS(CHECKSUM(NEWID())) % 4 rand_num
        FROM sys.all_objects
    ) A
    WHERE rand_num BETWEEN 0 AND 3
    

    【讨论】:

    • 再次,与 CTE 和 CROSS APPLY 一样,虽然事情可能似乎稳定,但我不确定是否有 保证 在考虑CASE 表达式时,不会多次计算随机表达式。
    • 那么 where 子句将保证没有 NULL 值。至于准确的随机化,我没有检查。
    • 感谢@Stephan 您的意见。关于准确的随机化,我怀疑如果对每个WHEN 评估一次随机化表达式,那么“String A”的出现频率将略高于“String B”,“String B”的出现频率高于“String C”等。跨度>
    【解决方案4】:

    这是关于随机数和空值的深思熟虑的讨论。我在这个问题上看到的最佳答案和解释来自 Aaron Bertrand 的文章 "Dirty Secrets of the CASE Expression,",该文章发表在 sqlperformance.com。在我的情况下,我将 Query DCROSS APPLY 结合起来以获得我正在寻找的结果。

    【讨论】:

      猜你喜欢
      • 2022-10-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多