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.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top