SQL Server: più totali in esecuzione


8

Ho una tabella di base con le transazioni e ho bisogno di creare una tabella con i totali correnti. Ho bisogno che siano per account e che abbiano anche alcuni totali in esecuzione per ciascun account (a seconda del tipo di transazione) e, al suo interno, alcuni totali in esecuzione per account secondario.

La mia tabella di base ha questi campi (più o meno):

AccountID  |  SubAccountID   |  TransactionType  |  TransactionAmount

Considerando che ho circa 4 tipi di totali in esecuzione per Account / TransactionType e altri 2 totali in esecuzione per Account / SubAccount / TransactionType e ho circa 2 milioni di account con circa 10 account secondari ciascuno e sto ottenendo circa 10.000 transazioni ogni minuto (al massimo carico), come lo faresti?

È anche necessario che questo avvenga in modo asincrono tramite un processo SQL, creando le aggregazioni senza far parte delle transazioni stesse.

Sono piuttosto bloccato usando un cursore qui - che impiega troppo tempo. Gradirei davvero qualsiasi consiglio / articolo che sta facendo più o meno lo stesso.


1
L'approccio standard contabile è quello di mantenere i totali correnti già in una tabella. Conservo con ogni transazione non solo il vecchio valore ma anche il nuovo valore dell'account. Non sei bloccato usando un cursore qui, poiché ciò può essere fatto in un'istruzione SELECT sql.
TomTom,

3
Sei su SQL Server 2000 o ci sono altre restrizioni che ti impediscono di utilizzare le funzioni della finestra (ROW_NUMBER, RANK, ecc.)?
Bryan,

1
Il nostro sistema di contabilità ha avuto problemi quando i totali in esecuzione sono stati memorizzati in una tabella fisica separata. Il software del nostro fornitore potrebbe aggiornare le transazioni effettive senza aggiornare la tabella dei saldi effettivi, risultando in un bilancio operativo fuori di testa. Un sistema ben progettato può evitarlo, ma fai attenzione e considera quanto sia importante la precisione se segui un approccio a tabella separata.
Ben Brocka,

Perché questo è un requisito e cosa sta tentando di realizzare? A seconda delle necessità, è possibile eseguire una query sulla tabella delle transazioni su richiesta per i dati ("correnti") specificati e spostare / aggregare le righe alla fine della giornata (data warehousing, di cui sono sicuro che SQL Server fornisce utilità).
Clockwork-Muse

Sono limitato a SQL Server 2005. Non è necessario che l'ultimo totale sia sempre accurato, ma devo mantenere tutti i totali in esecuzione per ogni azione effettuata: una tabella "Cronologia". TomTom - Non lo terrò con la tabella originale - Ho bisogno di alcuni totali in esecuzione di diversi tipi di transazione e non appartengono alla tabella originale. Non penso che ciò possa essere fatto solo con un SELECT: è un cursore o un ciclo while. Mi piacerebbe imparare diversamente. X-Zero: si tratta di un tipo di procedura di archiviazione dei dati. Ho solo bisogno di farlo ogni minuto e non una volta al giorno.
Avner, quindi

Risposte:


7

Asincrono implica che i totali in esecuzione non devono essere sempre completamente precisi o che i modelli di modifica dei dati sono tali che una build totale in esecuzione una tantum sarà valida e accurata fino al successivo carico. Ad ogni modo, sono sicuro che hai pensato a quella parte, quindi non affronterò il punto.

Le opzioni principali per un metodo supportato e ad alte prestazioni sono una funzione / procedura SQLCLR o un UPDATEmetodo di iterazione basato su set basato su Hugo Kornelis. Il metodo SQLCLR (implementato in una procedura, ma ragionevolmente facile da tradurre) può essere trovato qui .

Non sono stato in grado di trovare il metodo di Hugo online, ma è dettagliato nell'eccellente MVP Deep Dives (Volume 1). Di seguito è mostrato un esempio di codice per illustrare il metodo di Hugo (copiato da uno dei miei post su un altro sito per il quale potresti non avere il login):

-- 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, è possibile utilizzare le estensioni della funzione di windowing ad es SUM OVER (ORDER BY).


5

Non sono sicuro del motivo per cui vuoi essere asincrono, ma un paio di viste indicizzate sembrano solo il biglietto qui. Se si desidera una SOMMA semplice per un gruppo che è: definire il totale parziale.

Se vuoi davvero asincrono, con 160 nuove righe al secondo i tuoi totali in esecuzione saranno sempre obsoleti. Asincrono non significherebbe trigger o viste indicizzate


5

Il calcolo dei totali correnti è notoriamente lento, sia che lo si faccia con un cursore che con un join triangolare. È molto allettante denormalizzare, archiviare i totali in esecuzione in una colonna, specialmente se lo selezioni frequentemente. Tuttavia, come al solito quando denormalizzi, devi garantire l'integrità dei tuoi dati denormalizzati. Fortunatamente, è possibile garantire l'integrità dei totali in esecuzione con vincoli - purché tutti i vincoli siano attendibili, tutti i totali in esecuzione siano corretti.

Inoltre, in questo modo puoi facilmente assicurarti che il saldo corrente (totali correnti) non sia mai negativo - l'applicazione con altri metodi può anche essere molto lenta. Il seguente script dimostra la tecnica.

    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

Copiato dal mio blog

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.