【问题标题】:Generating random values from uniform distribution with setting a seed in T-SQL通过在 T-SQL 中设置种子从均匀分布生成随机值
【发布时间】:2017-03-01 22:56:16
【问题描述】:

我想为 T-SQL 中给定数据表的每一行从均值 = 0 和标准偏差 = 1 的均匀分布中生成一个随机值。此外,我想设置种子以确保分析的可重复性。以下是无效的想法:

  1. 将函数RAND() 与声明的数字一起使用并不能实现此目标:为数据集的每一行生成相同的随机值。

  2. 这样的解决方案:

    SELECT ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) AS [RandomNumber]

也不能解决问题,因为它不可重现。

编辑:

性能确实很重要,因为我的表有数亿条记录。

【问题讨论】:

标签: sql-server tsql random random-seed


【解决方案1】:

恕我直言,这里的主要问题是您如何看待“可重复性”?或者换一种方式问:是什么“驱动”了随机性?我可以设想一个解决方案,只要数据不改变,每次运行的每条记录都使用相同的随机数。但是,如果数据发生变化,您预计会发生什么?

为了好玩,我在一个有 100 万行的(不是很有代表性的)测试表上做了以下测试:

-- seed
SELECT Rand(0)

-- will show the same random number for EVERY record
SELECT Number, blah = Convert(varchar(100), NewID()), random = Rand()
  INTO #test
  FROM master.dbo.fn_int_list(1, 1000000)

CREATE UNIQUE CLUSTERED INDEX uq0_test ON #test (Number)

SET NOCOUNT ON

GO
DECLARE @start_time datetime = CURRENT_TIMESTAMP,
        @c_number int

-- update each record (one by one) and set the random number based on 'the next Rand()' value
-- => the order of the records drives the distribution of the Rand() value !

-- seed
SELECT @c_number = Rand(0) 

-- update 1 by 1 
DECLARE cursor_no_transaction CURSOR LOCAL STATIC
    FOR SELECT Number
          FROM #test
         ORDER BY Number
OPEN cursor_no_transaction 
FETCH NEXT FROM cursor_no_transaction INTO @c_number
WHILE @@FETCH_STATUS = 0
    BEGIN
        UPDATE #test 
           SET random = Rand()
         WHERE Number = @c_number

        FETCH NEXT FROM cursor_no_transaction INTO @c_number
    END
CLOSE cursor_no_transaction 
DEALLOCATE cursor_no_transaction 

PRINT 'Time needed (no transaction) : ' + Convert(nvarchar(100), DateDiff(ms, @start_time, CURRENT_TIMESTAMP)) + ' ms.'

SELECT _avg = AVG(random), _stdev = STDEV(random) FROM #test

GO

DECLARE @start_time datetime = CURRENT_TIMESTAMP,
        @c_number int

BEGIN TRANSACTION

-- update each record (one by one) and set the random number based on 'the next Rand()' value
-- => the order of the records drives the distribution of the Rand() value !

-- seed
SELECT @c_number = Rand(0) 

-- update 1 by 1 but all of it inside 1 single transaction
DECLARE cursor_single_transaction CURSOR LOCAL STATIC
    FOR SELECT Number
          FROM #test
         ORDER BY Number
OPEN cursor_single_transaction 
FETCH NEXT FROM cursor_single_transaction INTO @c_number
WHILE @@FETCH_STATUS = 0
    BEGIN
        UPDATE #test 
           SET random = Rand()
         WHERE Number = @c_number

        FETCH NEXT FROM cursor_single_transaction INTO @c_number
    END
CLOSE cursor_single_transaction 
DEALLOCATE cursor_single_transaction 

COMMIT TRANSACTION

PRINT 'Time needed (single transaction) : ' + Convert(nvarchar(100), DateDiff(ms, @start_time, CURRENT_TIMESTAMP)) + ' ms.'

SELECT _avg = AVG(random), _stdev = STDEV(random) FROM #test

GO

DECLARE @start_time datetime = CURRENT_TIMESTAMP

-- update each record (single operation), use the Number column to reseed the Rand() function for every record
UPDATE #test 
    SET random = Rand(Number)

PRINT 'Time needed Rand(Number) : ' + Convert(nvarchar(100), DateDiff(ms, @start_time, CURRENT_TIMESTAMP)) + ' ms.'

SELECT _avg = AVG(random), _stdev = STDEV(random) FROM #test

GO

DECLARE @start_time datetime = CURRENT_TIMESTAMP

-- update each record (single operation), use 'a bunch of fields' to reseed the Rand() function for every record
UPDATE #test 
    SET random = Rand(BINARY_CHECKSUM(Number, blah))

PRINT 'Time needed Rand(BINARY_CHECKSUM(Number, blah)) : ' + Convert(nvarchar(100), DateDiff(ms, @start_time, CURRENT_TIMESTAMP)) + ' ms.'

SELECT _avg = AVG(random), _stdev = STDEV(random) FROM #test

结果或多或少符合预期:

Time needed (no transaction) : 24570 ms.
_avg                   _stdev
---------------------- ----------------------
0.499630943538644      0.288686960086461

Time needed (single transaction) : 14813 ms.
_avg                   _stdev
---------------------- ----------------------
0.499630943538646      0.288686960086461

Time needed Rand(Number) : 1203 ms.
_avg                   _stdev
---------------------- ----------------------
0.499407423620328      0.291093824839539

Time needed Rand(BINARY_CHECKSUM(Number, blah)) : 1250 ms.
_avg                   _stdev
---------------------- ----------------------
0.499715398881586      0.288579510523627

所有这些都是“可重复的”,问题是“可重复”是否意味着您想要的意思。我一直坚持使用 AVG() 和 STDEV() 来大致了解分布,我会留给你看它们是否真的符合要求(如果不符合,如何改进它=)

对于 100 万行恕我直言,100 万行的 1.2 秒听起来还不错。也就是说,如果您的表格包含额外的列,它将占用更多空间,因此需要更多时间!

希望这能让你开始......

【讨论】:

    【解决方案2】:

    Rand() 函数可以在开始时通过传递一个整数 seed 值来播种。如果您在生成任何随机数之前执行此操作一次,则随机数序列将是可重复的。单独生成值将确保 Rand() 函数按顺序返回数字。以下将产生 n 个均值 = 0 且标准差 = 1 的伪随机数的均匀分布:

        DECLARE @Mean    FLOAT = 0.0; 
        DECLARE @stDev   FLOAT = 1.0; 
        DECLARE @n   INT = 100000;   -- count of random numbers to generate
        DECLARE @U   TABLE(x FLOAT); -- table of random numbers
    
        DECLARE @SEED    INT = 123456;    -- seed to ensure list is reproducible
        SELECT RAND(@Seed);
    
        SET NOCOUNT ON;
        BEGIN TRAN
        DECLARE @x INT = 0; -- counter
        WHILE @x < @n
          BEGIN
          INSERT INTO @U (x)
            SELECT @Mean + (2 * SQRT(3) * @stDev) * (RAND() - 0.5)
          SET @x = @x + 1;
          END;
        COMMIT
    
    -- Check the results    
        SELECT * from @U;
    
        SELECT AVG([@U].x) AS mean,
            STDEV([@U].x) AS stDev
            FROM @U;
    

    您可以使用游标遍历现有表中的记录,并对每条记录执行更新,而不是在 while 循环中插入临时表。如 cmets 中所述,性能可能是一个问题,但它满足“均值=0 和标准偏差=1 的均匀分布”“再现性”的要求。 Rand() 函数的工作方式强制“1 by 1”更新。

    下面是一个替代 Rand() 函数的替代方案,它具有更好的性能(应该在 2 秒内运行 100 万行)。这允许在单个UPDATE 中更新记录,但依赖于表中唯一的数字ID 字段并更新名为RandomNumber 的字段。 Rand() 函数被 ( (ID * @SEED ) % 1000 ) / 1000 取代,这可能会有所改进。

    DECLARE @Mean    FLOAT = 0.0; 
    DECLARE @stDev   FLOAT = 1.0; 
    DECLARE @SEED numeric(18,0)    = 1234567890.0;    -- seed to ensure list is reproducible
    
    SET NOCOUNT ON;
    BEGIN TRAN
    UPDATE TestTable
       set Randomnumber = @Mean + (2 * SQRT(3) * @stDev) * (( (ID * @SEED ) % 1000 ) / 1000 - 0.5) 
    COMMIT
    -- Check the results    
    SELECT AVG(RandomNumber) AS mean,
        STDEV(RandomNumber ) AS stDev
        FROM TestTable;
    

    【讨论】:

    • “使用游标对每条记录进行更新”。尽管是真的,但他确实提到了“数亿行”和“性能问题”。即使您将更新封装在较大的事务块中,您仍然会看到较长的运行时间。
    • 我同意,“1 by 1”更新会影响性能。我添加了一个替代方案来避免这种情况并进行一次更新(以减少“随机性”为代价),但仍然满足“均值 = 0 和标准偏差 = 1 的均匀分布”和“再现性”要求。
    【解决方案3】:
    DECLARE @userReportId BIGINT
    SET @userReportId = FLOOR(RAND()*(10000000000000-1) + 1);
    

    【讨论】:

      【解决方案4】:

      可重复的随机数 - 很可能 - 需要重复测试出错的情况以重现异常情况。

      以下建议将使用位置和随机数填充物理表(添加索引!)。

      将此列表与一个简单的连接一起使用,将您的每一行与一个随机数连接起来。

      每次调用都会将相同的随机数绑定到给定的行。

      可以通过使用新的随机位置重新定位随机数来更改此设置(或者您截断重新填充或删除重新创建表)。

      这应该很快...

      CREATE TABLE dbo.MyRepeatableRandoms(CurrentPosition BIGINT,RandomNumber BIGINT);
      GO
      DECLARE @CountOfNumbers INT=5; --set a fitting max count here
      WITH Tally AS
      (
       SELECT TOP(@CountOfNumbers) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS Nr 
       FROM master..spt_values 
            CROSS JOIN master..spt_values X 
            CROSS JOIN master..spt_values Y
      )
      INSERT INTO dbo.MyRepeatableRandoms
      SELECT Nr,CAST(CAST(NEWID() AS VARBINARY(8)) AS BIGINT) FROM Tally;
      
      --Use this list with a simple join to bind it to the rows of your table
      SELECT * FROM dbo.MyRepeatableRandoms ORDER BY CurrentPosition;
      
      --Re-Position the list
      WITH UpdateableCTE AS
      (
       SELECT ROW_NUMBER() OVER(ORDER BY A.posOrder) AS NewPos
             ,CurrentPosition
       FROM dbo.MyRepeatableRandoms
       CROSS APPLY(SELECT NEWID() AS posOrder) AS A
      )
      UPDATE UpdateableCTE SET CurrentPosition=NewPos;
      
      --The same random numbers at new positions
      SELECT * FROM MyRepeatableRandoms ORDER BY CurrentPosition;
      
      GO
      DROP TABLE dbo.MyRepeatableRandoms
      

      结果

          RandomNumber
      1   -1939965404062448822
      2   2786711671511266125
      3   -3236707863137400753
      4   -6029509773149087675
      5   7815987559555455297
      

      重新定位后

          RandomNumber
      1   7815987559555455297
      2   -1939965404062448822
      3   2786711671511266125
      4   -6029509773149087675
      5   -3236707863137400753
      

      【讨论】:

        【解决方案5】:

        这是一个纯 SQL 的近似值:

        select iif(rand(rand(id)) < .5, -1, 1) * sqrt(1 - exp(-1.27323954474*rand(id)*rand(id) * 
          (1 + 0.0586276296*rand(id)*rand(id)) / (1 + 0.0886745239*rand(id)*rand(id))))
        from mytable
        

        我选择了id 列作为种子,但您可以选择最适合您的列。即把rand(id)改成rand(some_other_column)随你喜欢。

        此公式基于this mathematical approximation

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2020-11-05
          • 1970-01-01
          • 2020-11-25
          • 1970-01-01
          • 2013-12-09
          • 1970-01-01
          • 1970-01-01
          • 2013-07-22
          相关资源
          最近更新 更多