SQL Server - несколько промежуточных итогов

8

У меня есть базовая таблица с транзакциями, и мне нужно создать таблицу с промежуточными итогами. Мне нужно, чтобы они были для каждой учетной записи, а также имели несколько промежуточных итогов для каждой учетной записи (в зависимости от типа транзакции), а внутри них - некоторые промежуточные итоги для каждой субсчета.

Моя базовая таблица имеет эти поля (более или менее):

AccountID  |  SubAccountID   |  TransactionType  |  TransactionAmount

Учитывая, что у меня есть около 4 типов промежуточных сумм на Account / TransactionType и еще 2 промежуточных суммы на Account / SubAccount / TransactionType, у меня есть около 2 миллионов учетных записей с примерно 10 вспомогательными учетными записями в каждой, и я получаю около 10 000 транзакций каждую минуту (при максимальной нагрузке), как бы вы это сделали?

Также необходимо, чтобы это выполнялось асинхронно через задание SQL, создавая агрегаты, не являясь частью самих транзакций.

Я застрял, используя курсор здесь - это занимает слишком много времени. Я очень ценю любые советы / статьи, которые делают более или менее то же самое.

AvnerSo
источник
1
Стандартный подход бухгалтерского учета состоит в том, чтобы сохранить промежуточные итоги уже в таблице. Я храню с каждой транзакцией не только старое значение, но и новое значение счета. Вы не застряли с помощью курсора здесь, так как это можно сделать одним оператором SQL SELECT.
TomTom
3
Вы используете SQL Server 2000 или существуют другие ограничения, не позволяющие вам использовать оконные функции (ROW_NUMBER, RANK и т. Д.)?
Брайан
1
В нашей системе бухгалтерского учета возникли проблемы, когда промежуточные итоги хранились в отдельной физической таблице. Программное обеспечение нашего поставщика может обновлять фактические транзакции без обновления таблицы фактического баланса, в результате чего операционный баланс выходит из строя. Хорошо спроектированная система может этого избежать, но будьте осторожны и подумайте, насколько важна точность, если вы подходите к отдельной таблице.
Бен Брока
Почему это требование, и что пытается быть достигнуто? В зависимости от необходимости, вы, возможно, могли бы запросить таблицу транзакций по запросу («текущих») указанных данных и переместить / агрегировать строки в конце дня (хранилище данных, для которого, я уверен, SQL Server предоставляет утилиты).
Заводная муза
Я ограничен SQL Server 2005. Мне не нужно, чтобы последний итог всегда был точным, но мне нужно сохранять все промежуточные итоги для каждого выполненного действия - таблицу «Журнал». TomTom - я не буду хранить это с исходной таблицей - мне нужно несколько промежуточных итогов разных типов транзакций, и они не принадлежат исходной таблице. Я не думаю, что это можно сделать только с помощью SELECT - это либо курсор, либо цикл while. Я хотел бы узнать иначе. X-Zero - это своего рода процедура хранения данных. Мне просто нужно делать это каждую минуту, а не раз в день.
AvnerSo

Ответы:

7

Асинхронный подразумевает, что промежуточные итоги не обязательно должны быть всегда точными, или ваши шаблоны изменения данных таковы, что одноразовая сборка промежуточных итогов будет действительной и точной до следующей загрузки. Во всяком случае, я уверен, что вы продумали эту часть до конца, поэтому я не буду разбираться с этим.

Основными параметрами высокопроизводительного, поддерживаемого метода являются функция / процедура SQLCLR или UPDATEметод итераций Хуго Корнелиса на основе множеств. Метод SQLCLR (реализованный в процедуре, но достаточно легко переводимый) можно найти здесь .

Я не смог найти метод Хьюго онлайн, но он подробно описан в отличных глубоких погружениях MVP (том 1). Пример кода, иллюстрирующий метод Хьюго (скопированный из одного из моих постов на другом сайте, для которого у вас может не быть логина), показан ниже:

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

В SQL Server 2012 вы можете использовать расширения оконной функции, например SUM OVER (ORDER BY).

Пол Уайт 9
источник
5

Я не уверен, почему вы хотите асинхронный, но пара индексированных представлений звучит как билет здесь. Если вы хотите простой SUM для какой-либо группы, а именно: определите промежуточный итог.

Если вам действительно нужна асинхронность, при 160 новых строках в секунду ваши текущие итоги всегда будут устаревшими. Асинхронный означает отсутствие триггеров или индексированных представлений.

ГБН
источник
5

Вычисление промежуточных итогов печально известно медленно, независимо от того, делаете ли вы это с помощью курсора или треугольного соединения. Очень заманчиво денормализовать, хранить промежуточные итоги в столбце, особенно если вы часто его выбираете. Однако, как обычно, когда вы денормализуете, вы должны гарантировать целостность ваших денормализованных данных. К счастью, вы можете гарантировать целостность промежуточных итогов с ограничениями - если все ваши ограничения являются доверенными, все промежуточные итоги верны.

Таким образом, вы также можете легко гарантировать, что текущий баланс (промежуточные итоги) никогда не будет отрицательным - применение других методов также может быть очень медленным. Следующий скрипт демонстрирует технику.

    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

Скопировано из моего блога

Аляска
источник