Создание случайных значений из равномерного распределения с установкой семени в 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)
по своему усмотрению.
Эта формула основана на этом математическом приближении.