J'essaie de générer une heure aléatoire entre 8h00 et 8h00 PM pour chaque ligne sélectionnée dans un jeu de données. Cependant, je reçois toujours le identique valeur aléatoire pour chaque ligne - Je veux que ce soit différent pour chaque ligne .
Schéma de table et données:
╔══════╦════════════════╗
║ ID ║ CREATED_DATE ║
╠══════╬════════════════╣
║ ID/1 ║ 26/04/2014 ║
║ ID/2 ║ 26/04/2014 ║
║ ID/3 ║ 26/04/2014 ║
║ ID/4 ║ 26/04/2014 ║
║ ID/5 ║ 26/04/2014 ║
╚══════╩════════════════╝
Instruction SQL actuelle:
SELECT [ID]
, MyFunction.dbo.AddWorkDays(14, [CREATED_DATE]) AS [New Date]
, CONVERT(VARCHAR, DATEADD(MILLISECOND, CAST(43200000 * Rand() AS INT), CONVERT(TIME, '08:00')), 114) AS [New Time]
FROM [RandomTable]
Résultats actuels ( identique heure pour chaque ligne de la colonne [New Time]
):
╔══════╦════════════════╦════════════════╗
║ ID ║ New Date ║ New Time ║
╠══════╬════════════════╬════════════════╣
║ ID/1 ║ 10/05/2014 ║ 09:41:43 ║
║ ID/2 ║ 10/05/2014 ║ 09:41:43 ║
║ ID/3 ║ 10/05/2014 ║ 09:41:43 ║
║ ID/4 ║ 10/05/2014 ║ 09:41:43 ║
║ ID/5 ║ 10/05/2014 ║ 09:41:43 ║
╚══════╩════════════════╩════════════════╝
Résultats souhaités ( différent heure pour chaque ligne de la colonne [New Time]
):
╔══════╦════════════════╦════════════════╗
║ ID ║ New Date ║ New Time ║
╠══════╬════════════════╬════════════════╣
║ ID/1 ║ 10/05/2014 ║ 09:41:43 ║
║ ID/2 ║ 10/05/2014 ║ 15:05:23 ║
║ ID/3 ║ 10/05/2014 ║ 10:01:05 ║
║ ID/4 ║ 10/05/2014 ║ 19:32:45 ║
║ ID/5 ║ 10/05/2014 ║ 08:43:15 ║
╚══════╩════════════════╩════════════════╝
Des idées pour résoudre le problème? Tout ce qui précède n’est que des exemples de données - ma vraie table contient environ 2800 enregistrements ( je ne suis pas sûr que cela changera les suggestions de quiconque ).
La question dit:
Maintenant, tenez compte des points suivants:
Il y a une certaine ambiguïté dans les domaines suivants:
Compte tenu des informations ci-dessus, il existe plusieurs façons d'interpréter la demande:
Rand(CAST(NEWID() AS VARBINARY)) * 43200
ABS(CHECKSUM(NewId()) % 43201)
CAST(43200000 * (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) as int)
J'ai donc basé ma réponse sur l'idée que:
Si la situation nécessite des temps uniques, cela ne peut être garanti par aucune méthode de génération de valeurs réellement aléatoires. J'aime beaucoup l'utilisation de CRYPT_GEN_RANDOM
par @Vladimir Baranov, mais il est presque impossible de générer un ensemble unique de valeurs:
DECLARE @Table TABLE (Col1 BIGINT NOT NULL UNIQUE);
INSERT INTO @Table (Col1)
SELECT CONVERT(BIGINT, CRYPT_GEN_RANDOM(4))
FROM [master].sys.objects so
CROSS JOIN [master].sys.objects so2
CROSS JOIN [master].sys.objects so3;
-- 753,571 rows
Augmenter la valeur aléatoire à 8 octets semble fonctionner:
DECLARE @Table TABLE (Col1 BIGINT NOT NULL UNIQUE);
INSERT INTO @Table (Col1)
SELECT CONVERT(BIGINT, CRYPT_GEN_RANDOM(8))
FROM [master].sys.objects so
CROSS JOIN [master].sys.objects so2
CROSS JOIN [master].sys.objects so3;
-- 753,571 rows
Bien sûr, si nous passons à la seconde, il n’ya que 86 400 d’entre eux. La réduction de la portée semble aider car ce qui suit fonctionne parfois:
DECLARE @Table TABLE (Col1 BIGINT NOT NULL UNIQUE);
INSERT INTO @Table (Col1)
SELECT TOP (86400) CONVERT(BIGINT, CRYPT_GEN_RANDOM(4))
FROM [master].sys.objects so
CROSS JOIN [master].sys.objects so2
CROSS JOIN [master].sys.objects so3;
Cependant, les choses deviennent un peu plus difficiles si l'unicité a besoin de par jour (ce qui semble être une exigence raisonnable pour ce type de projet, et non unique pour tous les jours). Mais un générateur de nombres aléatoires ne saura pas réinitialiser à chaque nouveau jour.
S'il est acceptable d'avoir simplement l'apparence d'être aléatoire, nous pouvons garantir l'unicité pour chaque date sans:
Rand()
, NEWID()
ou CRYPT_GEN_RANDOM()
La solution suivante utilise le concept de Modular Multiplicative Inverses (MMI) que j'ai appris dans cette réponse: génère un ID numérique unique apparemment aléatoire dans SQL Server . Bien entendu, cette question n’avait pas une fourchette de valeurs aussi définie que celle que nous avons ici, avec seulement 86 400 d’entre elles par jour. J'ai donc utilisé une plage de 86400 (en tant que "Modulo") et essayé quelques valeurs de "coprime" (en tant que "Integer") dans un calculateur en ligne pour obtenir leurs MMI:
J'utilise ROW_NUMBER()
dans un CTE, partitionné (c'est-à-dire groupé) par CREATED_DATE
comme moyen d'attribuer une valeur à chaque seconde du jour.
Cependant, alors que les valeurs générées pour les secondes 0, 1, 2, etc., etc., apparaissent en séquence, elles apparaissent de manière aléatoire, sur des jours différents, cette seconde en particulier mappant sur la même valeur. Ainsi, le deuxième CTE (nommé "WhichSecond") décale le point de départ de chaque date en convertissant la date en un INT (convertissant les dates en un décalage séquentiel du 1900-01-01), puis en le multipliant par 101.
DECLARE @Data TABLE
(
ID INT NOT NULL IDENTITY(1, 1),
CREATED_DATE DATE NOT NULL
);
INSERT INTO @Data (CREATED_DATE) VALUES ('2014-10-05');
INSERT INTO @Data (CREATED_DATE) VALUES ('2014-10-05');
INSERT INTO @Data (CREATED_DATE) VALUES ('2014-10-05');
INSERT INTO @Data (CREATED_DATE) VALUES ('2014-10-05');
INSERT INTO @Data (CREATED_DATE) VALUES ('2014-10-05');
INSERT INTO @Data (CREATED_DATE) VALUES ('2015-03-15');
INSERT INTO @Data (CREATED_DATE) VALUES ('2016-10-22');
INSERT INTO @Data (CREATED_DATE) VALUES ('2015-03-15');
;WITH cte AS
(
SELECT tmp.ID,
CONVERT(DATETIME, tmp.CREATED_DATE) AS [CREATED_DATE],
ROW_NUMBER() OVER (PARTITION BY tmp.CREATED_DATE ORDER BY (SELECT NULL))
AS [RowNum]
FROM @Data tmp
), WhichSecond AS
(
SELECT cte.ID,
cte.CREATED_DATE,
((CONVERT(INT, cte.[CREATED_DATE]) - 29219) * 101) + cte.[RowNum]
AS [ThisSecond]
FROM cte
)
SELECT parts.*,
(parts.ThisSecond % 86400) AS [NormalizedSecond], -- wrap around to 0 when
-- value goes above 86,400
((parts.ThisSecond % 86400) * 39539) % 86400 AS [ActualSecond],
DATEADD(
SECOND,
(((parts.ThisSecond % 86400) * 39539) % 86400),
parts.CREATED_DATE
) AS [DateWithUniqueTime]
FROM WhichSecond parts
ORDER BY parts.ID;
Résultats:
ID CREATED_DATE ThisSecond NormalizedSecond ActualSecond DateWithUniqueTime
1 2014-10-05 1282297 72697 11483 2014-10-05 03:11:23.000
2 2014-10-05 1282298 72698 51022 2014-10-05 14:10:22.000
3 2014-10-05 1282299 72699 4161 2014-10-05 01:09:21.000
4 2014-10-05 1282300 72700 43700 2014-10-05 12:08:20.000
5 2014-10-05 1282301 72701 83239 2014-10-05 23:07:19.000
6 2015-03-15 1298558 2558 52762 2015-03-15 14:39:22.000
7 2016-10-22 1357845 61845 83055 2016-10-22 23:04:15.000
8 2015-03-15 1298559 2559 5901 2015-03-15 01:38:21.000
Si nous voulons générer uniquement des heures entre 8h00 et 20h00, nous n'avons besoin que de quelques ajustements mineurs:
28800
au deuxième paramètre de la DATEADD
sous la forme d'un décalage de 8 heuresLe résultat sera un changement en une seule ligne (puisque les autres sont en diagnostic):
-- second parameter of the DATEADD() call
28800 + (((parts.ThisSecond % 43200) * 39539) % 43200)
Un autre moyen de changer chaque jour d'une manière moins prévisible serait d'utiliser la fonction Rand()
en passant sous la forme INT de CREATED_DATE
dans le CTE "WhichSecond". Cela donnerait un décalage stable pour chaque date puisque Rand(x)
renverra la même valeur y
pour la même valeur de x
passée, mais renverra une valeur différente y
pour une valeur différente de x
passée. Signification:
Rand (1) = y1
Rand (2) = y2
Rand (3) = y3
Rand (2) = y2
La deuxième fois que Rand(2)
a été appelé, il a toujours renvoyé la même valeur de y2
que lors de son premier appel.
Par conséquent, le CTE "WhichSecond" pourrait être:
(
SELECT cte.ID,
cte.CREATED_DATE,
(Rand(CONVERT(INT, cte.[CREATED_DATE])) * {some number}) + cte.[RowNum]
AS [ThisSecond]
FROM cte
)
Le problème que OP _ avait lors de l'utilisation de Rand()
est dû à son évaluation une fois par requête.
À partir de documentation :
Si graine n'est pas spécifié, le moteur de base de données SQL Server affecte une valeur graine au hasard. Pour une valeur graine spécifiée, le résultat renvoyé est toujours le même.
L'approche décrite ci-dessous supprime l'optimisation et supprime ce problème. Rand()
est donc évalué une fois par ligne:
dateadd( second
, Rand(cast(newid() as varbinary)) * 43200
, cast('08:00:00' as time) )
newid()
génère une valeur unique de type uniqueidentifier
;cast
pour être utilisée en tant que graine dans Rand([seed])
function afin de générer une valeur pseudo-aléatoire float
de 0 à 1, et en tant que graine est toujours unique, la valeur de retour est également unique.Vous pouvez aussi utiliser:
SELECT DATEADD(s, ABS(CHECKSUM(NewId()) % 43201), CAST('08:00:00' AS Time))
La ABS(CHECKSUM(NewId()) % 43201)
génère un nombre aléatoire entre 0
et 43200
. Voir Discussion ici .
Configuration du schéma MS SQL Server 2008 :
Requête 1 :
SELECT DATEADD(s, ABS(CHECKSUM(NewId()) % 43201), CAST('08:00:00' AS Time)) AS [RandomTime]
FROM
( VALUES (1), (2), (3), (4), (5)
) Y(A)
CROSS JOIN
( VALUES (1), (2), (3), (4), (5)
) Z(A)
| RANDOMTIME |
|------------------|
| 16:51:58.0000000 |
| 10:42:44.0000000 |
| 14:01:38.0000000 |
| 13:33:51.0000000 |
| 18:00:51.0000000 |
| 11:29:03.0000000 |
| 10:21:14.0000000 |
| 16:38:27.0000000 |
| 09:55:37.0000000 |
| 13:21:13.0000000 |
| 11:29:37.0000000 |
| 10:57:49.0000000 |
| 14:56:42.0000000 |
| 15:33:11.0000000 |
| 18:49:45.0000000 |
| 16:23:28.0000000 |
| 09:00:05.0000000 |
| 09:20:01.0000000 |
| 11:26:23.0000000 |
| 15:26:23.0000000 |
| 10:38:44.0000000 |
| 11:46:30.0000000 |
| 16:00:59.0000000 |
| 09:29:18.0000000 |
| 09:09:19.0000000 |
Il y a plusieurs méthodes:
NEWID
function pour fournir une valeur de départ pour Rand
. Il convient de l'utiliser avec prudence, car il n'y a aucune garantie quant à la distribution des valeurs NEWID. Une des meilleures méthodes pour le rendre plus ou moins uniformément distribué est via la variable CHECKSUM
: Rand(CHECKSUM(NEWID()))
. La bonne chose à propos de cette méthode est que la fonction NEWID est disponible depuis SQL Server 2000.NEWID
, utilisez, par exemple, MD5 d'une colonne en tant que valeur de départ pour Rand
: Rand(CHECKSUM(HASHBYTES('MD5', CAST(SomeID AS varbinary(4)))))
ou simplement le numéro de ligne: Rand(CHECKSUM(HASHBYTES('MD5', CAST(ROW_NUMBER() OVER(ORDER BY ...) AS varbinary(4)))))
. Cette méthode est disponible depuis au moins SQL Server 2005. La principale différence avec la méthode NEWID
est que vous avez le contrôle total sur la séquence aléatoire. Vous ne pouvez pas contrôler ce que NEWID
renvoie et vous ne pouvez pas redémarrer la séquence aléatoire à partir du même nombre. Si vous fournissez les mêmes ensembles de numéros de ligne, par exemple, en utilisant PARTITION BY
, vous obtiendrez les mêmes ensembles de nombres aléatoires. Cela peut être utile dans les cas où vous devez utiliser plusieurs fois la même séquence de nombres aléatoires. Il est possible d'obtenir le même nombre aléatoire pour deux graines différentes. Je l'ai testé pour des numéros de ligne compris entre 1 et 1 000 000. MD5
d'entre eux sont tous différents. CHECKSUM
of MD5
entraîne 122 collisions. Rand
de cette CHECKSUM
résultent en 246 collisions. Lorsqu’il a été testé avec des numéros de ligne compris entre 1 et 100 000, CHECKSUM
a eu 1 collision, Rand
a eu 3 collisions.Random
class ou RNGCryptoServiceProvider
class .CRYPT_GEN_RANDOM
.Je vais décrire la dernière méthode en détail, car je pense que c'est une très bonne solution pour SQL Server 2008 et versions ultérieures. CRYPT_GEN_RANDOM
est appelé pour chaque ligne du jeu de résultats, par opposition à Rand
, appelé une seule fois.
CRYPT_GEN_RANDOM (Transact-SQL)
Renvoie un nombre aléatoire cryptographique généré par Crypto API (CAPI). La sortie est un nombre hexadécimal du nombre spécifié de Octets.
De plus, CRYPT_GEN_RANDOM
devrait fournir des valeurs aléatoires bien meilleures que Rand
. Mieux en termes de distribution et de crypto-force. Exemple:
(CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5)
Cela génère 4 octets aléatoires sous la forme varbinary
. Nous devons explicitement les convertir en int
en premier. Le résultat est ensuite transformé en un nombre flottant compris entre 0 et 1.
Donc, la requête originale aimerait ceci:
SELECT ID AS [ID]
, MyFunction.dbo.AddWorkDays(14, S.CREATED_DATE) AS [New Date]
, CONVERT(VARCHAR, DATEADD(MILLISECOND,
CAST(43200000 * (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) as int),
CONVERT(TIME, '08:00')), 114) AS [New Time]
FROM RandomTable
Voici un exemple autonome facile à copier-coller et à essayer (j'ai utilisé la requête d'une autre réponse de @ Steve Ford):
SELECT DATEADD(millisecond,
CAST(43200000 * (CAST(CRYPT_GEN_RANDOM(4) as int) / 4294967295.0 + 0.5) as int),
CAST('08:00:00' AS Time)) AS [RandomTime]
FROM
( VALUES (1), (2), (3), (4), (5)
) Y(A)
CROSS JOIN
( VALUES (1), (2), (3), (4), (5)
) Z(A)
Voici le résultat:
RandomTime
10:58:24.7200000
19:40:06.7220000
11:04:29.0530000
08:57:31.6130000
15:03:14.9470000
09:15:34.9380000
13:46:43.1250000
11:27:00.8940000
14:42:23.6100000
15:07:56.2120000
11:39:09.8830000
08:16:44.3960000
14:23:38.4820000
17:28:31.7440000
16:29:31.4320000
09:09:15.0210000
12:31:09.8370000
11:23:09.8430000
15:35:45.5480000
17:42:49.3390000
08:07:05.4930000
18:17:16.2980000
11:49:08.2010000
10:20:21.7620000
15:56:58.6110000
Lorsque j'ai lu la question initiale, je ne pensais pas qu'il était vraiment nécessaire de veiller à ce que tous les nombres aléatoires générés soient uniques. J'ai interprété le mot "différent" dans la question comme un vague opposé pour voir le même nombre. dans chaque ligne du résultat que vous voyez lorsque vous utilisez une simple SELECT Rand()
. Je pense que dans de nombreux cas, peu importe s'il y a peu de nombres aléatoires en collision. Dans de nombreux cas, le comportement serait correct.
Donc, si j'ai bien compris, la nécessité d'une séquence de uniques nombres aléatoires équivaut en quelque sorte à la tâche suivante. Nous avons un ensemble de valeurs/lignes, par exemple, un ensemble d’ID uniques ou toutes les 86 400 secondes d’un jour ou 2800 lignes pour un jour donné. Nous voulons mélanger ces valeurs/rangées. Nous voulons réorganiser ces lignes dans un ordre aléatoire.
Pour mélanger un ensemble de lignes donné, nous avons simplement besoin de ORDER BY
nombres aléatoires (ces nombres aléatoires peuvent avoir un nombre raisonnable de collisions ici). Des nombres aléatoires peuvent être générés par n'importe quelle méthode. Quelque chose comme ça:
ROW_NUMBER() OVER ([optional PARTITION BY ...] ORDER BY CRYPT_GEN_RANDOM(4))
ou littéralement
SELECT ...
FROM ...
ORDER BY CRYPT_GEN_RANDOM(4)
selon où et comment il est utilisé.
Testez ceci:
Declare @t table(ID int,CREATED_DATE datetime)
insert into @t values
(1 , '04/26/2014'),
(2 , '04/26/2014'),
(3 , '04/26/2014'),
(4 , '04/26/2014')
;WITH CTE AS
(
SELECT *,CONVERT(VARCHAR, DATEADD(SECOND, Rand(CAST(NEWID() AS VARBINARY)) * 43200,
CAST('08:00:00' AS TIME)),114) AS [New Time] FROM @t WHERE ID=1
UNION ALL
SELECT *,CONVERT(VARCHAR, DATEADD(SECOND, Rand(CAST(NEWID() AS VARBINARY)) * 43200,
CAST('08:00:00' AS TIME)), 114) FROM @t WHERE ID>1 AND ID<=5
)
SELECT * FROM CTE
Voici une autre option qui vous donne un peu plus de contrôle sur la façon dont l’heure est générée. Vous pouvez spécifier l’intervalle entre les heures aléatoires. Elle n’utilise pas non plus la fonction Rand
.
DECLARE @StartTime VARCHAR(10) = '08:00',
@EndTime VARCHAR(10) = '20:00',
@Interval INT = 5 --(In Seconds)
WITH times AS(
SELECT CONVERT(TIME, @StartTime) AS t
UNION ALL
SELECT DATEADD(SECOND, @Interval, t)
FROM times
WHERE t < @EndTime
)
SELECT *,
(SELECT TOP 1 t FROM times WHERE d.Id > 0 ORDER BY NEWID())
FROM #data d
option (maxrecursion 0)
Sur une note de côté:
Si vous supprimez la clause WHERE
dans la sous-requête ci-dessus (WHERE d.Id > 0
), la même valeur d’heure est renvoyée pour toutes les lignes, c’est-à-dire le même problème que celui avec lequel vous avez commencé
Tout,
Je pensais partager la réponse à ma question. Je ne me souviens pas exactement où j'ai trouvé les détails - je pense que c'était via l'un des liens fournis par sgeddes.
J'ai utilisé la requête suivante pour obtenir une heure aléatoire entre 8h et 19h55 (environ)
SELECT convert(varchar,CONVERT(varchar, DATEADD(ms, dbo.MyRand(335 ,830) * 86400, 0), 114),114)
La fonction MyRand est ci-dessous:
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE FUNCTION dbo.myRand(@Min INT, @Max INT) RETURNS decimal(18,15) AS
BEGIN
DECLARE @BinaryFloat BINARY(8)
SELECT @BinaryFloat = CAST(Id AS BINARY) FROM vwGuid
DECLARE
@PartValue TINYINT,
@Mask TINYINT,
@Mantissa FLOAT,
@Exponent SMALLINT,
@Bit TINYINT,
@Ln2 FLOAT,
@BigValue BIGINT,
@RandomNumber FLOAT
SELECT
@Mantissa = 1,
@Bit = 1,
@Ln2 = LOG(2),
@BigValue = CAST(@BinaryFloat AS BIGINT),
@Exponent = (@BigValue & 0x7ff0000000000000) / EXP(52 * @Ln2)
WHILE @Part <= 8
BEGIN
SELECT
@PartValue = CAST(SUBSTRING(@BinaryFloat, @Part, 1) AS TINYINT),
@Mask =
WHILE @Mask > 0
BEGIN
IF @PartValue & @Mask > 0
SET @Mantissa = @Mantissa + EXP(-@Bit * @Ln2)
SELECT
@Mask = @Mask / 2
END
END
SET @RandomNumber = CASE @Exponent WHEN 0 THEN 0 ELSE CAST(@Exponent AS FLOAT) / 2047 END
RETURN CAST((@RandomNumber * (@Max - @Min)) + @Min AS DECIMAL(18,15))
END
GO
END
J'espère que ça aide. Comme je n'ai pas lu beaucoup des réponses ci-dessus, je vous prie de m'excuser si quelqu'un a une meilleure réponse.
Merci