SQL Server - Vários totais em execução

8

Eu tenho uma tabela base com transações e preciso criar uma tabela com totais em execução. Eu preciso que eles sejam por conta e também tenham alguns totais em execução para cada conta (dependendo do tipo de transação) e, dentro disso, alguns totais em execução por subconta.

Minha tabela base possui estes campos (mais ou menos):

AccountID  |  SubAccountID   |  TransactionType  |  TransactionAmount

Considerando que eu tenho cerca de 4 tipos de totais em execução por Account / TransactionType e mais 2 totais em execução por Account / SubAccount / TransactionType, e eu tenho cerca de 2 milhões de contas com cerca de 10 subcontas cada e estou recebendo cerca de 10 mil transações a cada minuto (com carga máxima), como você faria isso?

Também é necessário que isso seja executado de forma assíncrona por meio de um trabalho SQL, criando as agregações sem fazer parte das próprias transações.

Estou bem preso usando um cursor aqui - o que leva muito tempo. Eu realmente aprecio qualquer conselho / artigo que esteja fazendo mais ou menos o mesmo.

AvnerSo
fonte
11
A abordagem padrão da contabilidade é manter os totais em execução já em uma tabela. Eu armazeno em todas as transações não apenas o valor antigo, mas também o novo valor da conta. Você não está preso usando um cursor aqui, pois isso pode ser feito em uma instrução SELECT sql.
TomTom
3
Você está no SQL Server 2000 ou há outras restrições que impedem o uso das funções da janela (ROW_NUMBER, RANK, etc)?
Bryan
11
Nosso sistema contábil teve problemas quando os totais em execução foram armazenados em uma tabela física separada. O software do nosso fornecedor pode atualizar transações reais sem atualizar a tabela de saldo real, resultando em um saldo operacional fora de sintonia. Um sistema bem projetado pode evitar isso, mas tenha cuidado e considere a importância da precisão se você seguir a abordagem da tabela separada.
Ben Brocka
Por que isso é um requisito e o que está tentando ser realizado? Dependendo da necessidade, você provavelmente pode consultar a tabela de transações mediante solicitação dos dados especificados ('atuais') e mover / agregar linhas no final do dia (data warehousing, que tenho certeza que o SQL Server fornece utilitários).
Clockwork-Muse
Estou limitado ao SQL Server 2005. Não preciso ter o último total sempre preciso, mas preciso manter todos os totais em execução para cada ação realizada - uma tabela "Histórico". TomTom - não vou manter isso com a tabela original - preciso de alguns totais em execução de diferentes tipos de transação e eles não pertencem à tabela original. Eu não acho que isso possa ser feito apenas com um SELECT - é um cursor ou um loop while. Eu adoraria aprender o contrário. X-Zero - Esse é um tipo de procedimento de data warehousing. Eu só preciso fazer isso a cada minuto e não uma vez por dia.
precisa saber é o seguinte

Respostas:

7

Assíncrono implica que os totais em execução não precisam ser completamente precisos o tempo todo, ou seus padrões de alteração de dados são tais que uma construção total em execução única será válida e precisa até o próximo carregamento. De qualquer forma, tenho certeza que você já pensou nessa parte, então não vou insistir nisso.

Suas principais opções para um método suportado de alto desempenho são uma função / procedimento do SQLCLR ou um UPDATEmétodo de iteração baseado em conjunto de Hugo Kornelis. O método SQLCLR (implementado em um procedimento, mas razoavelmente fácil de traduzir) pode ser encontrado aqui .

Não consegui encontrar o método de Hugo online, mas ele está detalhado no excelente MVP Deep Dives (Volume 1). Um código de exemplo para ilustrar o método de Hugo (copiado de uma das minhas postagens em outro site para o qual você talvez não tenha um login) é mostrado abaixo:

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

No SQL Server 2012, você pode usar as extensões das funções de janelas, por exemplo SUM OVER (ORDER BY).

Paul White 9
fonte
5

Não sei por que você deseja assíncrono, mas algumas exibições indexadas parecem apenas o ticket aqui. Se você deseja um SUM simples para algum grupo: defina o total em execução.

Se você realmente deseja assíncrono, com 160 novas linhas por segundo, seus totais em execução estarão sempre desatualizados. Assíncrono não significaria gatilhos ou exibições indexadas

gbn
fonte
5

O cálculo dos totais em execução é notoriamente lento, seja com um cursor ou com uma junção triangular. É muito tentador desnormalizar, armazenar totais em execução em uma coluna, especialmente se você a selecionar com frequência. No entanto, como sempre, quando você desnormaliza, precisa garantir a integridade de seus dados desnormalizados. Felizmente, você pode garantir a integridade dos totais em execução com restrições - desde que todas as suas restrições sejam confiáveis, todos os totais em execução estejam corretos.

Dessa forma, você pode facilmente garantir que o saldo atual (totais em execução) nunca seja negativo - a imposição de outros métodos também pode ser muito lenta. O script a seguir demonstra a técnica.

    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

Copiado do meu blog

AK
fonte