Agréger uniquement les enregistrements adjacents avec T-SQL
-
04-07-2019 - |
Question
J'ai (simplifié pour l'exemple) une table avec les données suivantes
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-04 01 38
4 2008-10-04 2008-10-05 01 23
5 2008-10-05 2008-10-06 03 14
6 2008-10-06 2008-10-07 02 3
7 2008-10-07 2008-10-08 02 8
8 2008-10-08 2008-11-08 03 19
Les dates représentent une période, l'ID est l'état dans lequel se trouvait le système pendant cette période et le montant correspond à une valeur liée à cet état.
Ce que je veux faire est d'agréger les lignes des montants pour adjacentes avec le même identifiant , tout en conservant la même séquence afin de pouvoir combiner des exécutions contiguës. Ainsi, je veux me retrouver avec des données telles que:
Row Start Finish ID Amount
--- --------- ---------- -- ------
1 2008-10-01 2008-10-02 01 10
2 2008-10-02 2008-10-03 02 20
3 2008-10-03 2008-10-05 01 61
4 2008-10-05 2008-10-06 03 14
5 2008-10-06 2008-10-08 02 11
6 2008-10-08 2008-11-08 03 19
Je suis à la recherche d'une solution T-SQL pouvant être intégrée à un SP, mais je ne vois pas comment procéder avec de simples requêtes. Je suppose que cela peut nécessiter une itération, mais je ne veux pas aller dans cette voie.
La raison pour laquelle je souhaite effectuer cette agrégation est que l'étape suivante du processus consiste à regrouper SUM () et Count () en fonction des identifiants uniques de la séquence, afin que mes données finales ressemblent à :
ID Counts Total
-- ------ -----
01 2 71
02 2 31
03 2 33
Cependant si je fais un simple
SELECT COUNT(ID), SUM(Amount) FROM data GROUP BY ID
Sur la table d'origine, je reçois quelque chose comme
ID Counts Total
-- ------ -----
01 3 71
02 3 31
03 2 33
Ce qui n'est pas ce que je veux.
Pas de solution correcte
Autres conseils
Si vous avez lu le livre "Développement d'applications de base de données temporelles en SQL", par RT Snodgrass (dont le fichier pdf est disponible sur son site Web, sous Publications), et jusqu’à la figure 6.25 de la p165-166, vous trouverez le code SQL non trivial qui peut être utilisé dans l’exemple en cours pour regrouper les différentes lignes ayant la même valeur d’ID et des intervalles de temps continus.
Le développement de la requête ci-dessous est proche de la correction, mais un problème a été détecté à la fin, qui a sa source dans la première instruction SELECT. Je n'ai pas encore compris pourquoi on donnait une réponse incorrecte. [Si quelqu'un peut tester le code SQL de son SGBD et me dire si la première requête y fonctionne correctement, ce serait d'une grande aide!]
Cela ressemble à quelque chose comme:
-- Derived from Figure 6.25 from Snodgrass "Developing Time-Oriented
-- Database Applications in SQL"
CREATE TABLE Data
(
Start DATE,
Finish DATE,
ID CHAR(2),
Amount INT
);
INSERT INTO Data VALUES('2008-10-01', '2008-10-02', '01', 10);
INSERT INTO Data VALUES('2008-10-02', '2008-10-03', '02', 20);
INSERT INTO Data VALUES('2008-10-03', '2008-10-04', '01', 38);
INSERT INTO Data VALUES('2008-10-04', '2008-10-05', '01', 23);
INSERT INTO Data VALUES('2008-10-05', '2008-10-06', '03', 14);
INSERT INTO Data VALUES('2008-10-06', '2008-10-07', '02', 3);
INSERT INTO Data VALUES('2008-10-07', '2008-10-08', '02', 8);
INSERT INTO Data VALUES('2008-10-08', '2008-11-08', '03', 19);
SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)));
Le résultat de cette requête est:
01 2008-10-01 2008-10-02
01 2008-10-03 2008-10-05
02 2008-10-02 2008-10-03
02 2008-10-06 2008-10-08
03 2008-10-05 2008-10-06
03 2008-10-05 2008-11-08
03 2008-10-08 2008-11-08
Modifié : il existe un problème avec l'avant-dernière ligne: il ne devrait pas être là. Et je ne sais pas (encore) d'où ça vient.
Nous devons maintenant traiter cette expression complexe comme une expression de requête dans la clause FROM d'une autre instruction SELECT, qui additionnera les valeurs d'un montant donné pour un ID donné sur les entrées qui chevauchent les plages maximales indiquées ci-dessus.
SELECT M.ID, M.Start, M.Finish, SUM(D.Amount)
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS (SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS (SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS (SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish)
OR (T2.Start <= L.Finish AND L.Finish < T2.Finish)))) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
ORDER BY M.ID, M.Start;
Cela donne:
ID Start Finish Amount
01 2008-10-01 2008-10-02 10
01 2008-10-03 2008-10-05 61
02 2008-10-02 2008-10-03 20
02 2008-10-06 2008-10-08 11
03 2008-10-05 2008-10-06 14
03 2008-10-05 2008-11-08 33 -- Here be trouble!
03 2008-10-08 2008-11-08 19
Modifié : il s'agit presque du jeu de données correct sur lequel effectuer l'agrégation COUNT et SUM demandée par la question d'origine. La réponse finale est donc:
SELECT I.ID, COUNT(*) AS Number, SUM(I.Amount) AS Amount
FROM (SELECT M.ID, M.Start, M.Finish, SUM(D.Amount) AS Amount
FROM Data AS D,
(SELECT DISTINCT F.ID, F.Start, L.Finish
FROM Data AS F, Data AS L
WHERE F.Start < L.Finish
AND F.ID = L.ID
-- There are no gaps between F.Finish and L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS M
WHERE M.ID = F.ID
AND F.Finish < M.Start
AND M.Start < L.Start
AND NOT EXISTS
(SELECT *
FROM Data AS T1
WHERE T1.ID = F.ID
AND T1.Start < M.Start
AND M.Start <= T1.Finish))
-- Cannot be extended further
AND NOT EXISTS
(SELECT *
FROM Data AS T2
WHERE T2.ID = F.ID
AND ((T2.Start < F.Start AND F.Start <= T2.Finish) OR
(T2.Start <= L.Finish AND L.Finish < T2.Finish)))
) AS M
WHERE D.ID = M.ID
AND M.Start <= D.Start
AND M.Finish >= D.Finish
GROUP BY M.ID, M.Start, M.Finish
) AS I
GROUP BY I.ID
ORDER BY I.ID;
id number amount
01 2 71
02 2 31
03 3 66
Consulter : Oh! Drat ... l'entrée pour 3 a deux fois le "montant" qu'il devrait avoir. Les parties 'éditées' précédentes indiquent où les choses ont commencé à aller mal. Il semble que la première requête soit légèrement fausse (elle est peut-être destinée à une question différente) ou que l'optimiseur avec lequel je travaille fonctionne mal. Néanmoins, il devrait y avoir une réponse étroitement liée à cela qui donnera les valeurs correctes.
Pour mémoire: testé sur IBM Informix Dynamic Server 11.50 sur Solaris 10. Cependant, il devrait fonctionner correctement sur tout autre SGBD SQL modérément conforme aux normes.
Il est probablement nécessaire de créer un curseur et de parcourir les résultats, en gardant une trace de l'identifiant avec lequel vous travaillez et en accumulant les données le long du chemin. Lorsque l'id change, vous pouvez insérer les données accumulées dans une table temporaire et renvoyer la table à la fin de la procédure (tout sélectionner). Une fonction basée sur une table peut être meilleure car vous pouvez ensuite simplement insérer dans la table de retour au fur et à mesure.
Je pense que cela peut nécessiter une itération, mais je ne veux pas suivre cette voie.
Je pense que c'est la route que vous devrez emprunter. Utilisez un curseur pour renseigner une variable de table. Si vous avez un grand nombre d'enregistrements, vous pouvez utiliser une table permanente pour stocker les résultats. Lorsque vous devez récupérer les données, vous ne pouvez traiter que les nouvelles données.
Je voudrais ajouter un champ de bits avec une valeur par défaut de 0 à la table source pour garder trace des enregistrements qui ont été traités. En supposant que personne n'utilise select * dans la table, l'ajout d'une colonne avec une valeur par défaut n'affectera pas le reste de votre application.
Ajoutez un commentaire à ce message si vous souhaitez obtenir de l'aide pour coder la solution.
Eh bien, j’ai décidé de suivre la route des itérations en utilisant un mélange de jointures et de curseurs. En joignant la table de données contre elle-même, je peux créer une liste de liens ne contenant que les enregistrements consécutifs.
INSERT INTO #CONSEC
SELECT a.ID, a.Start, b.Finish, b.Amount
FROM Data a JOIN Data b
ON (a.Finish = b.Start) AND (a.ID = b.ID)
Ensuite, je peux dérouler la liste en le parcourant avec un curseur et en effectuant des mises à jour dans la table de données à ajuster (et supprimer les enregistrements superflus de la table de données)
DECLARE CCursor CURSOR FOR
SELECT ID, Start, Finish, Amount FROM #CONSEC ORDER BY Start DESC
@Total = 0
OPEN CCursor
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
WHILE @FETCH_STATUS = 0
BEGIN
@Total = @Total + @Amount
@Start_Last = @Start
@Finish_Last = @Finish
@ID_Last = @ID
DELETE FROM Data WHERE Start = @Finish
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
IF (@ID_Last<> @ID) OR (@Finish<>@Start_Last)
BEGIN
UPDATE Data
SET Amount = Amount + @Total
WHERE Start = @Start_Last
@Total = 0
END
END
CLOSE CCursor
DEALLOCATE CCursor
Tout cela fonctionne et offre des performances acceptables pour les données classiques que j'utilise.
J'ai trouvé un petit problème avec le code ci-dessus. À l'origine, je mettais à jour la table de données sur chaque boucle passant par le curseur. Mais cela n'a pas fonctionné. Il semble que vous ne puissiez effectuer qu'une seule mise à jour sur un enregistrement et que plusieurs mises à jour (afin de continuer à ajouter des données) reviennent à la lecture du contenu original de l'enregistrement.