views:

270

answers:

5

I am writing a procedure where each call it needs to get a single random number. This procedure is called from our .net web service.

I tried implementing this using rand(). However, when I have multiple calls to the stored procedure within milliseconds, I am getting a lot of collisions in that the same random number is being generated. If there is a space of about 20 or 30 ms between subsequent calls it appears to work ok.

It appears that rand() is reseeded each stored procedure call by SqlServer. From what I understand this is a problem because one should seed a random number generator once and that one doesn't get a good sequence of pseudo-random numbers if one is reseeding each call to rand. Also, it appears that calls to the same sp that are within 1 or 2 milliseconds get seeded with the same value.

Here is the statement itself in the stored procedure.

DECLARE @randomNumber char(9)

SET @randomNumber = RIGHT('00000' + CAST(CAST(rand()*100000 AS INT) AS VARCHAR(5)),5)
+ RIGHT('00000' + CAST(CAST(rand()*10000 AS INT) AS VARCHAR(4)),4)

Does anyone have a suggestion for fixing this?

Will I have to write my own random number generator that is seeded once and saves its state in a table across calls? How does SQL Server seed rand()? Is it truly random or if you call an sp within 1 or 2 milliseconds of each other on separate connections will it be seeded with the same seed causing a collision?

+3  A: 

In your example, replace rand()*10000 with ABS(CHECKSUM(NEWID())) % 9999

However, for char(9):

SELECT RIGHT('000000000' + CAST(ABS(CHECKSUM(NEWID()) % 999999999) AS char(9), 9)

To seed RAND randomly...

RAND(CHECKSUM(NEWID()))

Edit:

Note, RAND is badly implemented in SQL Server. Don't use it.

gbn
+! Works, though I wonder if there is any guarantee about the distribution of newid() or checksum(newid()). Wasn't the MAC address used in the calculation of newid()?
Andomar
newid = GUID, so same statistics. CHECKSUM = signed 32 bit = 4 billion.
gbn
Seems like the best solution, so far. Although, I am also looking into a clr stored procedure that uses RNGCryptoServiceProvider.
@Jeff: I answered about RNGCryptoServiceProvider in SQL here http://stackoverflow.com/questions/1521835/how-to-generate-a-rngcryptoserviceprovider-like-number-in-tsql/1521929#1521929
gbn
A: 

The RAND() function has an optional seed parameter that you could use for this. If you pass the last generated random value as a seed to the next call to rand(), you are guaranteed to get a new random number.

Thanks to gbn for pointing out that the seed is an integer, while rand() returns a float. With that knowledge, here's a working example! First create a table:

create table RandomNumber (number float)
insert into RandomNumber values (rand())

Then grab a random number and store the new number in a transaction:

declare @new float
begin transaction
select @new = rand(-2147483648 + 4294967295 * number)
    from RandomNumber with (updlock, holdlock)
update RandomNumber set number = @new
commit transaction
print 'Next bingo number is: ' + cast(cast(@new*100 as int) as varchar)

An SQL Server integer varies between -2147483648 and 2147483647, and a random number is a float between 0.0 and 1.0. So -2147483648 + 4294967295 * number should cover the full range of available integers.

The transaction ensures that only one connection at a time reads and stores a new number. So the numbers are random even on different connections to SQL Server. (By the way, I voted for gbn's answer, seems much easier.)

Andomar
It's an interesting idea, but then I would have to save this state, in a table and look it up for each call in the sp. Seems expensive. Also, will this produce a good pseudo-random sequence of numbers if I set the seed in this manner?
@Andomar: RAND takes an int seed, so you are seeding the same for each itertion. See an old answer of mine here: http://stackoverflow.com/questions/1038681/sql-random-number-not-working/1354125#1354125
gbn
A: 

Have you tried to write a CLR stored procedure and use the random number ability of the .NET framework?

StarShip3000
Well, originally we were using System.Random. But we ran into problems since it uses the timestamp as the seed and we have requests very close together in time.I haven't looked in detail at RNGCryptoServiceProvider.
A: 

You could use a table with just an identifier field for creating unique nunbers to use as seed:

declare
  @randomNumber char(9),
  @seed1 int,
  @seed2 int

insert into SeedTable () values ()
set @seed1 = scope_identity()

insert into SeedTable () values ()
set @seed2 = scope_identity()

set @randomNumber = right('00000' + 
    cast(cast(rand(@seed1) * 100000 as int) as varchar(5)), 5) +
    right('00000' + 
    cast(cast(rand(@seed2) * 10000 as int) as varchar(4)), 4)

if (@seed2 > 10000) truncate table SeedTable
Guffa
Did you intend to create a table called SeedTable?
Andomar
@Andomar: Yes, that's the table with just an identifier that I was talking about.
Guffa
@Guffy: and truncate table resets identity columns?
Andomar
@Andomar: Yes, it does. If you want the identity to continue you could use a delete to keep the size down instead.
Guffa
+1  A: 

If you are using SQL Server 2008, then you can use the CRYPT_GEN_RANDOM() function. This will randomize data for every row even if you were trying to calculate millions of random numbers in one query execution and doesn't have any seeding issues:

SELECT CAST(RIGHT(CAST(CAST(CRYPT_GEN_RANDOM(1) AS INT) AS VARCHAR(100)), 1) AS INT)

Here's the link to the BOL article:

http://msdn.microsoft.com/en-us/library/cc627408.aspx

Registered User
+1. This is new to me. Very good. I like it.
gbn