【问题标题】:SQL Server CHOOSE() function behaving unexpectedly with RAND() functionSQL Server CHOOSE() 函数与 RAND() 函数的行为异常
【发布时间】:2021-12-13 21:31:15
【问题描述】:

我在尝试使用 RANDCHOOSE 函数在 T-sql 中生成随机值时遇到了一个有趣的 SQL 服务器行为。

我的目标是尝试使用 RAND() 作为 rng 返回两个给定值之一。很简单吧?

对于那些不知道的人,CHOOSE 函数接受一个索引号 (int) 以及一组值,并返回指定索引处的值。很简单。

第一次尝试我的 SQL 看起来像这样:

    select choose(ceiling((rand()*2)) ,'a','b')

令我惊讶的是,这个表达式返回了三个值之一:null、'a' 或 'b'。由于我没想到我开始挖掘空值。 RAND() 函数返回一个范围从 0(包括)到 1(不包括)的浮点数。因为我将它乘以 2,所以它应该返回从 0(包括)到 2(不包括)范围内的任何值。因此使用 CEILING 函数后的最终值应该是以下之一:0,1,2。在意识到我通过'c'扩展了值列表以检查是否可能会返回。我还查看了CEILING 的文档页面并了解到:

返回值与 numeric_expression 的类型相同。

我假设 CEILING 函数返回 int,但在这种情况下,这意味着该值在用于 CHOOSE 之前被隐式转换为 int,这在文档页面上确实有说明:

如果提供的索引值具有非 int 的数值数据类型, 然后将值隐式转换为整数。

以防万一我添加了明确的演员表。我的 SQL 查询现在看起来像这样:

    select choose(cast(ceiling((rand()*2)) as int) ,'a','b','c')

但是,结果集没有改变。为了检查哪些值导致了问题,我尝试预先生成该值并在 CHOOSE 结果旁边选择它。它看起来像这样:

    declare @int int = cast(ceiling((rand()*2)) as int)
    select @int,choose( @int,'a','b','c') 

有趣的是,现在结果集更改为 (1,a), (2,b),这是我最初的目标。在深入研究 CHOOSE 文档页面并进行一些测试后,我了解到在以下两种情况之一中会返回“null”:

  1. 给定索引为空
  2. 给定的索引超出范围

在这种情况下,这意味着在 SELECT 语句中生成的索引值是 0 或大于 2/3(我假设这里不可能有负数,并且从 1 开始选择函数索引)。正如我之前所说,0 应该是以下可能性之一:

    ceiling((rand()*2))

,但由于某种原因,它永远不会为 0(至少当我像这样尝试 100 万次以上时)

    set nocount on
    
    declare @test table(ceiling_rand int)
    declare @counter int = 0
    
    while @counter<1000000
    begin
    insert into @test 
    select ceiling((rand()*2))
    
    set @counter=@counter+1
    end
    
    select distinct ceiling_rand from @test

因此我假设 SELECT 中生成的值大于 2/3 或 NULL。为什么只有在 SELECT 语句中生成时才会这样?也许在 SELECT 中解析 CAST、CELING 或 RAND 的顺序与看起来不同?确实我只尝试了有限的次数,但在这一点上,它成为统计波动的可能性非常小。它在某种程度上是一个浮点错误吗?我真的很迷茫,期待任何解释。

TL;DR:在 SELECT 语句中生成随机数时,可能值的结果集与在 SELECT 语句之前生成时不同。

干杯, 南加州大学

编辑:格式化

【问题讨论】:

    标签: sql-server tsql random rounding


    【解决方案1】:

    如果你查看执行计划,你可以看到发生了什么。

    SET SHOWPLAN_TEXT ON
    
    GO
    
    SELECT (select choose(ceiling((rand()*2)) ,'a','b'))
    

    返回

      |--Constant Scan(VALUES:((CASE WHEN CONVERT_IMPLICIT(int,ceiling(rand()*(2.0000000000000000e+000)),0)=(1) THEN 'a' ELSE CASE WHEN CONVERT_IMPLICIT(int,ceiling(rand()*(2.0000000000000000e+000)),0)=(2) THEN 'b' ELSE NULL END END)))
    

    CHOOSE 扩展为

    SELECT CASE
             WHEN ceiling(( rand() * 2 )) = 1 THEN 'a'
             ELSE
               CASE
                 WHEN ceiling(( rand() * 2 )) = 2 THEN 'b'
                 ELSE NULL
               END
           END 
    

    并且rand() 被引用了两次。每次评估都可以返回不同的结果。

    下面的重写也被扩展了,你也会遇到同样的问题

      SELECT CASE ceiling(( rand() * 2 ))
             WHEN 1 THEN 'a'
             WHEN 2 THEN 'b'
           END 
    

    避免CASE 用于此及其任何变体。

    一种方法是

    SELECT JSON_VALUE ( '["a", "b"]' , CONCAT('$[', FLOOR(rand()*2) ,']') )  
    

    【讨论】:

    • 非常感谢,这非常有见地。如果您不介意我问,您是否也知道为什么ceiling(( rand() * 2 )) 永远不会为 0 的答案?
    • 如果rand() 正好为零,它只能是0。由于rand() 返回类似0.512158037153074 的结果,您必须对其进行多次评估才能期望出现确切的零值
    • "返回一个从 0 到 1 的伪随机浮点值,独占。"注意独家
    • @JohnCappelletti 哦,是的,我一定是看错了,幸运的是,这并没有改变手头的事情,但感谢您的敏锐眼光:)
    • 是的,关于“排他性”是否仅适用于 1 端,似乎在网络上存在相互矛盾的信息,但无论哪种方式,我都不会评估大量随机数来查找出去。所以要么不可能,要么不太可能!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-18
    • 2022-12-04
    • 2018-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多