Создание случайных значений из равномерного распределения с установкой семени в T-SQL

Я хотел бы создать случайное значение из равномерного распределения со средним значением = 0 и стандартным отклонением = 1 для каждой строки данной таблицы данных в T-SQL. Кроме того, я хотел бы установить семя, чтобы обеспечить воспроизводимость анализа. Вот идеи, которые не сработали:

  • Использование функции RAND() с объявленным номером не соответствует этой цели: одно и то же случайное значение генерируется для каждой строки набора данных.

  • Такое решение:

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

не решает проблему либо потому, что она не воспроизводима.

EDIT:

Производительность имеет значение, так как моя таблица содержит сотни миллионов записей.

Ответ 1

Функция Rand() может быть засеяна в начале, передав ей целое начальное значение. Если вы делаете это один раз перед генерированием случайных чисел, последовательность случайных чисел будет повторяемой. Генерация значений индивидуально гарантирует, что функция 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, вы можете прокручивать записи в своей существующей таблице с помощью курсора и обновлять каждую запись. Как отмечалось в комментариях, производительность может быть проблемой, но она отвечает требованиям "равномерное распределение со средним значением = 0 и стандартным отклонением = 1" и "воспроизводимость". Способ работы Rand() заставляет обновить "1 к 1".

Ниже приведена альтернатива, которая будет иметь гораздо лучшую производительность (должна работать менее 2 секунд с 1 миллионом строк) с заменой функции Rand(). Это позволяет обновлять записи в одном 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;

Ответ 2

Главный вопрос здесь ИМХО - это то, как вы видите "повторяемость"? Или спросил по-другому: что "управляет" случайностью? Я могу представить себе решение, которое фиксирует одно и то же случайное число для каждой записи для каждого прогона, пока данные не изменятся. Однако, что вы ожидаете, если данные будут изменены?

Для удовольствия от этого я провел следующие тесты на (не очень представительной) тестовой таблице с 1 миллионом строк:

-- 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(), чтобы получить грубую идею о распределении, я оставлю это до вас, чтобы убедиться, что они действительно соответствуют счету (а если нет, как улучшить его =)

1,2 секунды для 1 миллиона строк не звучат слишком плохо для 1 миллиона строк IMHO. Тем не менее, если ваша таблица содержит дополнительные столбцы, это займет больше места и, следовательно, займет больше времени!

Надеюсь, вам это поможет...

Ответ 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) по своему усмотрению.

Эта формула основана на этом математическом приближении.