SQL Server - Mehrere laufende Summen


8

Ich habe eine Basistabelle mit Transaktionen und muss eine Tabelle mit laufenden Summen erstellen. Ich brauche sie pro Konto und habe auch ein paar laufende Summen für jedes Konto (abhängig vom Transaktionstyp) und darin einige laufende Summen pro Unterkonto.

Meine Basistabelle enthält folgende Felder (mehr oder weniger):

AccountID  |  SubAccountID   |  TransactionType  |  TransactionAmount

Wenn man bedenkt, dass ich ungefähr 4 Arten von laufenden Summen pro Konto / Transaktionstyp und 2 weitere laufende Summen pro Konto / Unterkonto / Transaktionstyp habe, habe ich ungefähr 2 Millionen Konten mit jeweils ungefähr 10 Unterkonten und ich bekomme ungefähr 10.000 Transaktionen Wie würden Sie es jede Minute (bei maximaler Belastung) tun?

Es ist auch ein Muss, dass dies asynchron über einen SQL-Job ausgeführt wird und die Aggregationen erstellt werden, ohne Teil der Transaktionen selbst zu sein.

Ich stecke hier ziemlich fest mit einem Cursor - was viel zu lange dauert. Ich würde mich über Ratschläge / Artikel sehr freuen, die mehr oder weniger dasselbe tun.


1
Der Standardansatz des Rechnungswesens besteht darin, die laufenden Summen bereits in einer Tabelle zu halten. Ich speichere bei jeder Transaktion nicht nur den alten Wert, sondern auch den neuen Wert des Kontos. Sie müssen hier keinen Cursor verwenden, da dies in einer SQL SELECT-Anweisung erfolgen kann.
TomTom

3
Befinden Sie sich in SQL Server 2000 oder gibt es andere Einschränkungen, die Sie von der Verwendung der Fensterfunktionen abhalten (ROW_NUMBER, RANK usw.)?
Bryan

1
Unser Buchhaltungssystem hatte Probleme, als laufende Summen in einer separaten physischen Tabelle gespeichert wurden. Die Software unseres Anbieters kann tatsächliche Transaktionen aktualisieren, ohne die tatsächliche Bilanztabelle zu aktualisieren, was zu einem aus dem Gleichgewicht geratenen Betriebsbilanzsaldo führt. Ein gut konzipiertes System kann dies vermeiden. Seien Sie jedoch vorsichtig und überlegen Sie, wie wichtig die Genauigkeit ist, wenn Sie den separaten Tabellenansatz wählen.
Ben Brocka

Warum ist dies eine Voraussetzung und was wird versucht, erreicht zu werden? Je nach Bedarf können Sie wahrscheinlich die Transaktionstabelle auf Anfrage nach den ('aktuellen') angegebenen Daten abfragen und Zeilen am Ende des Tages verschieben / aggregieren (Data Warehousing, für das SQL Server sicher Dienstprogramme bereitstellt).
Uhrwerk-Muse

Ich bin auf SQL Server 2005 beschränkt. Ich muss nicht immer die letzte Summe genau haben, aber ich muss alle laufenden Summen für jede durchgeführte Aktion behalten - eine "Verlauf" -Tabelle. TomTom - Ich werde dies nicht mit der Originaltabelle beibehalten - Ich benötige einige laufende Summen verschiedener Transaktionstypen und sie gehören nicht in die Originaltabelle. Ich denke nicht, dass dies nur mit einem SELECT möglich ist - es ist entweder ein Cursor oder eine while-Schleife. Ich würde gerne etwas anderes lernen. X-Zero - Dies ist eine Art Data Warehousing-Verfahren. Ich brauche es nur ungefähr jede Minute und nicht einmal am Tag.
AvnerSo

Antworten:


7

Asynchron bedeutet, dass die laufenden Summen nicht immer vollständig genau sein müssen, oder dass Ihre Datenänderungsmuster so sind, dass ein einmaliger laufender Gesamtaufbau bis zum nächsten Laden gültig und genau ist. Wie auch immer, ich bin sicher, Sie haben diesen Teil durchdacht, also werde ich mich nicht darum kümmern.

Ihre Hauptoptionen für eine leistungsstarke, unterstützte Methode sind eine SQLCLR-Funktion / -Prozedur oder eine UPDATEauf der satzbasierten Iterationsmethode von Hugo Kornelis basierende. Die SQLCLR-Methode (in einer Prozedur implementiert, aber relativ einfach zu übersetzen) finden Sie hier .

Ich konnte Hugos Methode nicht online finden, aber sie ist in den hervorragenden MVP Deep Dives (Band 1) beschrieben. Der folgende Beispielcode zur Veranschaulichung der Hugo-Methode (kopiert von einem meiner Beiträge auf einer anderen Website, für die Sie möglicherweise kein Login haben) ist unten dargestellt:

-- 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;

In SQL Server 2012 können Sie die Fensterfunktionserweiterungen verwenden, z SUM OVER (ORDER BY).


5

Ich bin mir nicht sicher, warum Sie asynchron sein möchten, aber ein paar indizierte Ansichten klingen hier wie das Ticket. Wenn Sie eine einfache SUMME für eine Gruppe wünschen, definieren Sie: Laufende Summe definieren.

Wenn Sie wirklich asynchron sein möchten, sind Ihre laufenden Summen mit 160 neuen Zeilen pro Sekunde immer veraltet. Asynchron würde keine Trigger oder indizierten Ansichten bedeuten


5

Die Berechnung laufender Summen ist notorisch langsam, egal ob Sie dies mit einem Cursor oder mit einer dreieckigen Verknüpfung tun. Es ist sehr verlockend, laufende Summen zu denormalisieren und in einer Spalte zu speichern, insbesondere wenn Sie sie häufig auswählen. Wie üblich beim Denormalisieren müssen Sie jedoch die Integrität Ihrer denormalisierten Daten gewährleisten. Glücklicherweise können Sie die Integrität laufender Summen mit Einschränkungen garantieren. Solange alle Ihre Einschränkungen vertrauenswürdig sind, sind alle laufenden Summen korrekt.

Auf diese Weise können Sie auch leicht sicherstellen, dass der aktuelle Kontostand (laufende Summen) niemals negativ ist. Die Durchsetzung durch andere Methoden kann ebenfalls sehr langsam sein. Das folgende Skript demonstriert die Technik.

    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

Von meinem Blog kopiert

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.