SQL Server - Plusieurs totaux cumulés

8

J'ai une table de base avec des transactions et je dois créer une table avec des totaux cumulés. J'ai besoin qu'ils soient par compte et que j'ai également quelques totaux cumulés pour chaque compte (en fonction du type de transaction), et à l'intérieur, certains totaux cumulés par sous-compte.

Ma table de base a ces champs (plus ou moins):

AccountID  |  SubAccountID   |  TransactionType  |  TransactionAmount

Considérant que j'ai environ 4 types de totaux en cours d'exécution par compte / type de transaction et 2 autres totaux en cours d'exécution par compte / sous-compte / type de transaction, et j'ai environ 2 millions de comptes avec environ 10 sous-comptes chacun, et j'obtiens environ 10 000 transactions chaque minute (à charge maximale), comment feriez-vous?

Il est également indispensable que cela s'exécute de manière asynchrone via un travail SQL, créant les agrégations sans faire partie des transactions elles-mêmes.

Je suis assez coincé en utilisant un curseur ici - ce qui prend beaucoup trop de temps. J'apprécierais vraiment tous les conseils / articles qui font plus ou moins la même chose.

AvnerSo
la source
1
L'approche standard comptable consiste à conserver les totaux cumulés déjà dans un tableau. Je stocke à chaque transaction non seulement l'ancienne valeur mais également la nouvelle valeur du compte. Vous n'êtes pas bloqué en utilisant un curseur ici, car cela peut être fait dans une instruction sql SELECT.
TomTom
3
Êtes-vous sur SQL Server 2000 ou existe-t-il d'autres restrictions vous empêchant d'utiliser les fonctions de la fenêtre (ROW_NUMBER, RANK, etc.)?
Bryan
1
Notre système de comptabilité a rencontré des problèmes lors du stockage des totaux dans une table physique distincte. Le logiciel de notre fournisseur pourrait mettre à jour les transactions réelles sans mettre à jour le tableau de solde réel, ce qui entraînerait un solde d'exploitation hors de contrôle. Un système bien conçu peut éviter cela, mais soyez prudent et considérez l'importance de la précision si vous optez pour l'approche de table séparée.
Ben Brocka
Pourquoi est-ce une exigence et que tente-t-on d'accomplir? Selon les besoins, vous pouvez probablement interroger la table de transactions sur demande pour les données spécifiées (`` actuelles '') et déplacer / agréger des lignes à la fin de la journée (entreposage de données, pour lequel je suis sûr que SQL Server fournit des utilitaires).
Clockwork-Muse
Je suis limité à SQL Server 2005. Je n'ai pas besoin d'avoir le dernier total toujours précis, mais je dois garder tous les totaux en cours pour chaque action effectuée - un tableau "Historique". TomTom - Je ne garderai pas cela avec la table d'origine - J'ai besoin de quelques totaux cumulés de différents types de transaction et ils n'appartiennent pas à la table d'origine. Je ne pense pas que cela puisse être fait uniquement avec un SELECT - c'est soit un curseur soit une boucle while. J'adorerais apprendre autrement. X-Zero - Il s'agit d'une sorte de procédure d'entreposage de données. J'ai juste besoin que cela soit fait toutes les minutes et pas une fois par jour.
AvnerSo

Réponses:

7

Asynchrone implique que les totaux en cours d'exécution n'ont pas besoin d'être complètement précis à tout moment, ou que vos modèles de changement de données sont tels qu'une construction totale unique en cours d'exécution sera valide et précise jusqu'à la prochaine charge. Quoi qu'il en soit, je suis sûr que vous avez réfléchi à cette partie, donc je ne m'attarderai pas sur le sujet.

Vos principales options pour une méthode haute performance prise en charge sont une fonction / procédure SQLCLR ou une UPDATEméthode d'itération basée sur des ensembles d'Hugo Kornelis. La méthode SQLCLR (implémentée dans une procédure, mais raisonnablement facile à traduire) peut être trouvée ici .

Je n'ai pas pu trouver la méthode d'Hugo en ligne, mais elle est détaillée dans l'excellent MVP Deep Dives (Volume 1). Un exemple de code pour illustrer la méthode d'Hugo (copié à partir de l'un de mes messages sur un autre site pour lequel vous n'avez peut-être pas de connexion) est illustré ci-dessous:

-- A work table to hold the reformatted data, and
-- ultimately, the results
CREATE  TABLE #Work
    (
    Acct_No         VARCHAR(20) NOT NULL,
    MonthDate       DATETIME NOT NULL,
    MonthRate       DECIMAL(19,12) NOT NULL,
    Amount          DECIMAL(19,12) NOT NULL,
    InterestAmount  DECIMAL(19,12) NOT NULL,
    RunningTotal    DECIMAL(19,12) NOT NULL,
    RowRank         BIGINT NOT NULL
    );

-- Prepare the set-based iteration method
WITH    Accounts
AS      (
        -- Get a list of the account numbers
        SELECT  DISTINCT Acct_No 
        FROM    #Refunds
        ),
        Rates
AS      (
        -- Apply all the accounts to all the rates
        SELECT  A.Acct_No,
                R.[Year],
                R.[Month],
                MonthRate = R.InterestRate / 12
        FROM    #InterestRates R
        CROSS 
        JOIN    Accounts A
        ),
        BaseData
AS      (
        -- The basic data we need to work with
        SELECT  Acct_No = ISNULL(R.Acct_No,''),
                MonthDate = ISNULL(DATEADD(MONTH, R.[Month], DATEADD(YEAR, R.[year] - 1900, 0)), 0),
                R.MonthRate,
                Amount = ISNULL(RF.Amount,0),
                InterestAmount = ISNULL(RF.Amount,0) * R.MonthRate,
                RunningTotal = ISNULL(RF.Amount,0)
        FROM    Rates R
        LEFT
        JOIN    #Refunds RF
                ON  RF.Acct_No = R.Acct_No
                AND RF.[Year] = R.[Year]
                AND RF.[Month] = R.[Month]
        )
-- Basic data plus a rank id, numbering the rows by MonthDate, and resetting to 1 for each new Account
INSERT  #Work
        (Acct_No, MonthDate, MonthRate, Amount, InterestAmount, RunningTotal, RowRank)
SELECT  BD.Acct_No, BD.MonthDate, BD.MonthRate, BD.Amount, BD.InterestAmount, BD.RunningTotal,
        RowRank = RANK() OVER (PARTITION BY BD.Acct_No ORDER BY MonthDate)
FROM    BaseData BD;

-- An index to speed the next stage (different from that used with the Quirky Update method)
CREATE UNIQUE CLUSTERED INDEX nc1 ON #Work (RowRank, Acct_No);

-- Iteration variables
DECLARE @Rank       BIGINT,
        @RowCount   INTEGER;

-- Initialize
SELECT  @Rank = 1,
        @RowCount = 1;

-- This is the iteration bit, processes a rank id per iteration
-- The number of rows processed with each iteration is equal to the number of groups in the data
-- More groups --> greater efficiency
WHILE   (1 = 1)
BEGIN
        SET @Rank = @Rank + 1;

        -- Set-based update with running totals for the current rank id
        UPDATE  This
        SET     InterestAmount = (Previous.RunningTotal + This.Amount) * This.MonthRate,
                RunningTotal = Previous.RunningTotal + This.Amount + (Previous.RunningTotal + This.Amount) * This.MonthRate
        FROM    #Work This
        JOIN    #Work Previous
                ON  Previous.Acct_No = This.Acct_No
                AND Previous.RowRank = @Rank - 1
        WHERE   This.RowRank = @Rank;

        IF  (@@ROWCOUNT = 0) BREAK;
END;

-- Show the results in natural order
SELECT  *
FROM    #Work
ORDER   BY
        Acct_No, RowRank;

Dans SQL Server 2012, vous pouvez utiliser les extensions de fonction de fenêtrage, par exemple SUM OVER (ORDER BY).

Paul White 9
la source
5

Je ne sais pas pourquoi vous voulez asynchrone, mais quelques vues indexées ressemblent juste au ticket ici. Si vous voulez un SUM simple pour un groupe, c'est: définir le total cumulé.

Si vous voulez vraiment asynchrone, avec 160 nouvelles lignes par seconde, vos totaux cumulés seront toujours obsolètes. Asynchrone signifierait aucun déclencheur ou vue indexée

gbn
la source
5

Le calcul des totaux cumulés est notoirement lent, que vous le fassiez avec un curseur ou avec une jointure triangulaire. Il est très tentant de dénormaliser, de stocker les totaux en cours dans une colonne, surtout si vous la sélectionnez fréquemment. Cependant, comme d'habitude lorsque vous dénormalisez, vous devez garantir l'intégrité de vos données dénormalisées. Heureusement, vous pouvez garantir l'intégrité des totaux cumulés avec des contraintes - tant que toutes vos contraintes sont fiables, tous vos totaux cumulés sont corrects.

De cette façon, vous pouvez facilement vous assurer que le solde actuel (totaux cumulés) n'est jamais négatif - l'application par d'autres méthodes peut également être très lente. Le script suivant illustre la technique.

    CREATE TABLE Data.Inventory(InventoryID INT NOT NULL IDENTITY,
      ItemID INT NOT NULL,
      ChangeDate DATETIME NOT NULL,
      ChangeQty INT NOT NULL,
      TotalQty INT NOT NULL,
      PreviousChangeDate DATETIME NULL,
      PreviousTotalQty INT NULL,
      CONSTRAINT PK_Inventory PRIMARY KEY(ItemID, ChangeDate),
      CONSTRAINT UNQ_Inventory UNIQUE(ItemID, ChangeDate, TotalQty),
      CONSTRAINT UNQ_Inventory_Previous_Columns UNIQUE(ItemID, PreviousChangeDate, PreviousTotalQty),
      CONSTRAINT FK_Inventory_Self FOREIGN KEY(ItemID, PreviousChangeDate, PreviousTotalQty)
        REFERENCES Data.Inventory(ItemID, ChangeDate, TotalQty),
      CONSTRAINT CHK_Inventory_Valid_TotalQty CHECK(TotalQty >= 0 AND (TotalQty = COALESCE(PreviousTotalQty, 0) + ChangeQty)),
      CONSTRAINT CHK_Inventory_Valid_Dates_Sequence CHECK(PreviousChangeDate < ChangeDate),
      CONSTRAINT CHK_Inventory_Valid_Previous_Columns CHECK((PreviousChangeDate IS NULL AND PreviousTotalQty IS NULL)
                OR (PreviousChangeDate IS NOT NULL AND PreviousTotalQty IS NOT NULL))
    );
    GO
    -- beginning of inventory for item 1
    INSERT INTO Data.Inventory(ItemID,
      ChangeDate,
      ChangeQty,
      TotalQty,
      PreviousChangeDate,
      PreviousTotalQty)
    VALUES(1, '20090101', 10, 10, NULL, NULL);
    -- cannot begin the inventory for the second time for the same item 1
    INSERT INTO Data.Inventory(ItemID,
      ChangeDate,
      ChangeQty,
      TotalQty,
      PreviousChangeDate,
      PreviousTotalQty)
    VALUES(1, '20090102', 10, 10, NULL, NULL);


Msg 2627, Level 14, State 1, Line 10
Violation of UNIQUE KEY constraint 'UNQ_Inventory_Previous_Columns'. Cannot insert duplicate key in object 'Data.Inventory'.
The statement has been terminated.

-- add more
DECLARE @ChangeQty INT;
SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090103', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = 3;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090104', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = -4;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090105', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

-- try to violate chronological order

SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20081231', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

Msg 547, Level 16, State 0, Line 4
The INSERT statement conflicted with the CHECK constraint "CHK_Inventory_Valid_Dates_Sequence". The conflict occurred in database "Test", table "Data.Inventory".
The statement has been terminated.


SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- -----
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 5           15          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           18          2009-01-03 00:00:00.000 15
2009-01-05 00:00:00.000 -4          14          2009-01-04 00:00:00.000 18


-- try to change a single row, all updates must fail
UPDATE Data.Inventory SET ChangeQty = ChangeQty + 2 WHERE InventoryID = 3;
UPDATE Data.Inventory SET TotalQty = TotalQty + 2 WHERE InventoryID = 3;
-- try to delete not the last row, all deletes must fail
DELETE FROM Data.Inventory WHERE InventoryID = 1;
DELETE FROM Data.Inventory WHERE InventoryID = 3;

-- the right way to update

DECLARE @IncreaseQty INT;
SET @IncreaseQty = 2;
UPDATE Data.Inventory SET ChangeQty = ChangeQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN @IncreaseQty ELSE 0 END,
  TotalQty = TotalQty + @IncreaseQty,
  PreviousTotalQty = PreviousTotalQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN 0 ELSE @IncreaseQty END
WHERE ItemID = 1 AND ChangeDate >= '20090103';

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- ----------------
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 7           17          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           20          2009-01-03 00:00:00.000 17
2009-01-05 00:00:00.000 -4          16          2009-01-04 00:00:00.000 20

Copié de mon blog

AK
la source