SQL est un langage basé sur des ensembles et les boucles doivent être un dernier recours. Ainsi, l'approche basée sur un ensemble consisterait à générer d'abord toutes les dates dont vous avez besoin et à les insérer en une seule fois, plutôt que de boucler et d'insérer une à la fois. Aaron Bertrand a écrit une excellente série sur la génération d'un ensemble ou d'une séquence sans boucles :
- Générer un ensemble ou une séquence sans boucles – partie 1
- Générer un ensemble ou une séquence sans boucles – partie 2
- Générer un ensemble ou une séquence sans boucles – partie 3
La partie 3 est particulièrement pertinente car elle traite des dates.
En supposant que vous n'ayez pas de table Calendar, vous pouvez utiliser la méthode CTE empilée pour générer une liste de dates entre vos dates de début et de fin.
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
WITH N1 (N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2)
SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY N) - 1, @StartDate)
FROM N3;
J'ai sauté quelques détails sur la façon dont cela fonctionne car il est couvert dans l'article lié, essentiellement il commence par une table codée en dur de 10 lignes, puis joint cette table avec elle-même pour obtenir 100 lignes (10 x 10) puis rejoint cette table de 100 lignes à lui-même pour obtenir 10 000 lignes (je me suis arrêté à ce stade, mais si vous avez besoin de lignes supplémentaires, vous pouvez ajouter d'autres jointures).
À chaque étape, la sortie est une seule colonne appelée N
avec une valeur de 1 (pour garder les choses simples). En même temps que de définir comment générer 10 000 lignes, je dis en fait à SQL Server de ne générer que le nombre nécessaire en utilisant TOP
et la différence entre vos dates de début et de fin - TOP(DATEDIFF(DAY, @StartDate, @EndDate) + 1)
. Cela évite des travaux inutiles. J'ai dû ajouter 1 à la différence pour m'assurer que les deux dates étaient incluses.
Utilisation de la fonction de classement ROW_NUMBER()
J'ajoute un numéro incrémental à chacune des lignes générées, puis j'ajoute ce numéro incrémental à votre date de début pour obtenir la liste des dates. Depuis ROW_NUMBER()
commence à 1, je dois en déduire 1 pour m'assurer que la date de début est incluse.
Ensuite, il s'agirait simplement d'exclure des dates qui existent déjà en utilisant NOT EXISTS
. J'ai joint les résultats de la requête ci-dessus dans leur propre CTE appelé dates
:
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
WITH N1 (N) AS (SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Dates AS
( SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY N) - 1, @StartDate)
FROM N3
)
INSERT INTO MyTable ([TimeStamp])
SELECT Date
FROM Dates AS d
WHERE NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE d.Date = t.[TimeStamp])
Si vous deviez créer une table de calendrier (comme décrit dans les articles liés), il n'est peut-être pas nécessaire d'insérer ces lignes supplémentaires, vous pouvez simplement générer votre jeu de résultats à la volée, quelque chose comme :
SELECT [Timestamp] = c.Date,
t.[FruitType],
t.[NumOffered],
t.[NumTaken],
t.[NumAbandoned],
t.[NumSpoiled]
FROM dbo.Calendar AS c
LEFT JOIN dbo.MyTable AS t
ON t.[Timestamp] = c.[Date]
WHERE c.Date >= @StartDate
AND c.Date < @EndDate;
ADDENDA
Pour répondre à votre question, votre boucle serait écrite comme suit :
DECLARE @StartDate AS DATETIME
DECLARE @EndDate AS DATETIME
DECLARE @CurrentDate AS DATETIME
SET @StartDate = '2015-01-01'
SET @EndDate = GETDATE()
SET @CurrentDate = @StartDate
WHILE (@CurrentDate < @EndDate)
BEGIN
IF NOT EXISTS (SELECT 1 FROM myTable WHERE myTable.Timestamp = @CurrentDate)
BEGIN
INSERT INTO MyTable ([Timestamp])
VALUES (@CurrentDate);
END
SET @CurrentDate = DATEADD(DAY, 1, @CurrentDate); /*increment current date*/
END
Exemple sur SQL Fiddle
Je ne préconise pas cette approche, ce n'est pas parce que quelque chose n'est fait qu'une seule fois que je ne dois pas démontrer la bonne façon de le faire.
COMPLÉMENT D'EXPLICATION
Étant donné que la méthode CTE empilée peut avoir trop compliqué l'approche basée sur les ensembles, je vais la simplifier en utilisant la table système non documentée master..spt_values
. Si vous exécutez :
SELECT Number
FROM master..spt_values
WHERE Type = 'P';
Vous verrez que vous obtenez tous les nombres de 0 à 2047.
Maintenant, si vous exécutez :
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P';
Vous obtenez toutes les dates de votre date de début à 2047 jours dans le futur. Si vous ajoutez une autre clause where, vous pouvez la limiter aux dates antérieures à votre date de fin :
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate;
Maintenant que vous avez toutes les dates dont vous avez besoin dans une seule requête basée sur un ensemble, vous pouvez éliminer les lignes qui existent déjà dans votre table en utilisant NOT EXISTS
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate
AND NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE t.[Timestamp] = DATEADD(DAY, number, @StartDate));
Enfin, vous pouvez insérer ces dates dans votre tableau en utilisant INSERT
DECLARE @StartDate DATE = '2015-01-01',
@EndDate DATE = GETDATE();
INSERT YourTable ([Timestamp])
SELECT Date = DATEADD(DAY, number, @StartDate)
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY, number, @StartDate) <= @EndDate
AND NOT EXISTS (SELECT 1 FROM MyTable AS t WHERE t.[Timestamp] = DATEADD(DAY, number, @StartDate));
Espérons que cela montre en partie que l'approche basée sur les ensembles est non seulement beaucoup plus efficace, mais aussi plus simple.