Агрегировать только смежные записи с T-SQL - PullRequest
4 голосов
/ 25 октября 2008

У меня есть (упрощенно для примера) таблица со следующими данными

Row Start       Finish       ID  Amount
--- ---------   ----------   --  ------
  1 2008-10-01  2008-10-02   01      10
  2 2008-10-02  2008-10-03   02      20
  3 2008-10-03  2008-10-04   01      38
  4 2008-10-04  2008-10-05   01      23
  5 2008-10-05  2008-10-06   03      14
  6 2008-10-06  2008-10-07   02       3
  7 2008-10-07  2008-10-08   02       8
  8 2008-10-08  2008-11-08   03      19

Даты представляют период во времени, идентификатор - это состояние, в котором находилась система в течение этого периода, а сумма - это значение, связанное с этим состоянием.

Что я хочу сделать, это объединить суммы для смежных строк с одинаковым идентификатором , но сохранить ту же общую последовательность, чтобы можно было объединять смежные серии. Таким образом, я хочу получить такие данные, как:

Row Start       Finish       ID  Amount
--- ---------   ----------   --  ------
  1 2008-10-01  2008-10-02   01      10
  2 2008-10-02  2008-10-03   02      20
  3 2008-10-03  2008-10-05   01      61
  4 2008-10-05  2008-10-06   03      14
  5 2008-10-06  2008-10-08   02      11
  6 2008-10-08  2008-11-08   03      19

Мне нужно решение T-SQL, которое можно поместить в SP, однако я не понимаю, как это сделать с помощью простых запросов. Я подозреваю, что это может потребовать некоторой итерации, но я не хочу идти по этому пути.

Причина, по которой я хочу выполнить это агрегирование, заключается в том, что следующим шагом в процессе является выполнение SUM () и Count (), сгруппированных по уникальным идентификаторам, которые встречаются в последовательности, так что мои окончательные данные будут выглядеть примерно так :

ID  Counts Total
--  ------ -----
01       2    71
02       2    31
03       2    33

Однако, если я сделаю простой

SELECT COUNT(ID), SUM(Amount) FROM data GROUP BY ID

На исходном столе я получаю что-то вроде

ID  Counts Total
--  ------ -----
01       3    71
02       3    31
03       2    33

Чего я не хочу.

Ответы [ 4 ]

4 голосов
/ 25 октября 2008

Если вы прочитаете книгу «Разработка ориентированных на время приложений баз данных в SQL» от RT Snodgrass (pdf которого доступен на его веб-сайте в разделе публикаций) и дойдете до рисунка 6.25 на p165-166, вы найдете нетривиальный SQL, который можно использовать в текущем примере для группировки различных строк с одинаковым значением идентификатора и непрерывными временными интервалами.

Разработка запроса, приведенная ниже, близка к правильной, но в конце обнаружена проблема, источником которой является первый оператор SELECT. Я еще не выяснил, почему дается неправильный ответ. [Если кто-то может проверить SQL на своей СУБД и сказать мне, правильно ли работает первый запрос, это было бы очень полезно!]

Это выглядит примерно так:

-- Derived from Figure 6.25 from Snodgrass "Developing Time-Oriented
-- Database Applications in SQL"
CREATE TABLE Data
(
    Start   DATE,
    Finish  DATE,
    ID      CHAR(2),
    Amount  INT
);

INSERT INTO Data VALUES('2008-10-01', '2008-10-02', '01', 10);
INSERT INTO Data VALUES('2008-10-02', '2008-10-03', '02', 20);
INSERT INTO Data VALUES('2008-10-03', '2008-10-04', '01', 38);
INSERT INTO Data VALUES('2008-10-04', '2008-10-05', '01', 23);
INSERT INTO Data VALUES('2008-10-05', '2008-10-06', '03', 14);
INSERT INTO Data VALUES('2008-10-06', '2008-10-07', '02',  3);
INSERT INTO Data VALUES('2008-10-07', '2008-10-08', '02',  8);
INSERT INTO Data VALUES('2008-10-08', '2008-11-08', '03', 19);

SELECT DISTINCT F.ID, F.Start, L.Finish
    FROM Data AS F, Data AS L
    WHERE F.Start < L.Finish
      AND F.ID = L.ID
      -- There are no gaps between F.Finish and L.Start
      AND NOT EXISTS (SELECT *
                        FROM Data AS M
                        WHERE M.ID = F.ID
                        AND F.Finish < M.Start
                        AND M.Start < L.Start
                        AND NOT EXISTS (SELECT *
                                            FROM Data AS T1
                                            WHERE T1.ID = F.ID
                                              AND T1.Start <  M.Start
                                              AND M.Start  <= T1.Finish))
      -- Cannot be extended further
      AND NOT EXISTS (SELECT *
                          FROM Data AS T2
                          WHERE T2.ID = F.ID
                            AND ((T2.Start <  F.Start  AND F.Start  <= T2.Finish)
                              OR (T2.Start <= L.Finish AND L.Finish <  T2.Finish)));

Вывод этого запроса:

01  2008-10-01      2008-10-02
01  2008-10-03      2008-10-05
02  2008-10-02      2008-10-03
02  2008-10-06      2008-10-08
03  2008-10-05      2008-10-06
03  2008-10-05      2008-11-08
03  2008-10-08      2008-11-08

Отредактировано : Существует проблема с предпоследней строкой - ее не должно быть. И мне не ясно (пока), откуда это.

Теперь нам нужно обработать это сложное выражение как выражение запроса в предложении FROM другого оператора SELECT, которое будет суммировать значения суммы для данного идентификатора по записям, которые перекрываются с максимальными диапазонами, показанными выше.

SELECT M.ID, M.Start, M.Finish, SUM(D.Amount)
    FROM Data AS D,
         (SELECT DISTINCT F.ID, F.Start, L.Finish
              FROM Data AS F, Data AS L
              WHERE F.Start < L.Finish
                AND F.ID = L.ID
                -- There are no gaps between F.Finish and L.Start
                AND NOT EXISTS (SELECT *
                                    FROM Data AS M
                                    WHERE M.ID = F.ID
                                    AND F.Finish < M.Start
                                    AND M.Start < L.Start
                                    AND NOT EXISTS (SELECT *
                                                        FROM Data AS T1
                                                        WHERE T1.ID = F.ID
                                                          AND T1.Start <  M.Start
                                                          AND M.Start  <= T1.Finish))
                  -- Cannot be extended further
                AND NOT EXISTS (SELECT *
                                    FROM Data AS T2
                                    WHERE T2.ID = F.ID
                                      AND ((T2.Start <  F.Start  AND F.Start  <= T2.Finish)
                                        OR (T2.Start <= L.Finish AND L.Finish <  T2.Finish)))) AS M
    WHERE D.ID = M.ID
      AND M.Start  <= D.Start
      AND M.Finish >= D.Finish
    GROUP BY M.ID, M.Start, M.Finish
    ORDER BY M.ID, M.Start;

Это дает:

ID  Start        Finish       Amount
01  2008-10-01   2008-10-02   10
01  2008-10-03   2008-10-05   61
02  2008-10-02   2008-10-03   20
02  2008-10-06   2008-10-08   11
03  2008-10-05   2008-10-06   14
03  2008-10-05   2008-11-08   33              -- Here be trouble!
03  2008-10-08   2008-11-08   19

Отредактировано : Это почти правильный набор данных, для которого необходимо выполнить агрегирование COUNT и SUM, запрошенное исходным вопросом, поэтому окончательный ответ:

SELECT I.ID, COUNT(*) AS Number, SUM(I.Amount) AS Amount
    FROM (SELECT M.ID, M.Start, M.Finish, SUM(D.Amount) AS Amount
            FROM Data AS D,
                 (SELECT DISTINCT F.ID, F.Start, L.Finish
                      FROM  Data AS F, Data AS L
                      WHERE F.Start < L.Finish
                        AND F.ID = L.ID
                        -- There are no gaps between F.Finish and L.Start
                        AND NOT EXISTS
                            (SELECT *
                                FROM  Data AS M
                                WHERE M.ID = F.ID
                                  AND F.Finish < M.Start
                                  AND M.Start < L.Start
                                  AND NOT EXISTS
                                      (SELECT *
                                          FROM Data AS T1
                                          WHERE T1.ID = F.ID
                                            AND T1.Start <  M.Start
                                            AND M.Start  <= T1.Finish))
                          -- Cannot be extended further
                        AND NOT EXISTS
                            (SELECT *
                                FROM  Data AS T2
                                WHERE T2.ID = F.ID
                                  AND ((T2.Start <  F.Start  AND F.Start  <= T2.Finish) OR
                                       (T2.Start <= L.Finish AND L.Finish <  T2.Finish)))
                 ) AS M
            WHERE D.ID = M.ID
              AND M.Start  <= D.Start
              AND M.Finish >= D.Finish
            GROUP BY M.ID, M.Start, M.Finish
          ) AS I
        GROUP BY I.ID
        ORDER BY I.ID;

id     number  amount
01      2      71
02      2      31
03      3      66

Обзор : Ой! Драт ... запись для 3 имеет в два раза больше, чем должна иметь. Предыдущие «отредактированные» части указывают, где что-то пошло не так. Похоже, что либо первый запрос немного ошибочен (возможно, он предназначен для другого вопроса), либо оптимизатор, с которым я работаю, работает неправильно. Тем не менее, должен быть ответ, тесно связанный с этим, который даст правильные значения.

Для справки: протестировано на IBM Informix Dynamic Server 11.50 на Solaris 10. Тем не менее, должно работать нормально на любой другой СУБД SQL, соответствующей умеренным стандартам.

1 голос
/ 25 октября 2008

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

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

Я бы добавил битовое поле со значением по умолчанию 0 в исходную таблицу, чтобы отслеживать, какие записи были обработаны. Предполагая, что никто не использует select * в таблице, добавление столбца со значением по умолчанию не повлияет на остальную часть вашего приложения.

Добавьте комментарий к этому сообщению, если вам нужна помощь в написании решения.

1 голос
/ 25 октября 2008

Вероятно, необходимо создать курсор и просмотреть результаты, отслеживая, с каким идентификатором вы работаете, и накапливая данные по пути. Когда идентификатор изменяется, вы можете вставить накопленные данные во временную таблицу и вернуть таблицу в конце процедуры (выбрать все из нее). Табличная функция может быть лучше, так как вы можете просто вставить ее в таблицу возврата по ходу дела.

0 голосов
/ 27 октября 2008

Ну, я решил пойти по итерационному маршруту, используя смесь соединений и курсоров. Присоединяя таблицу данных к себе, я могу создать список ссылок только тех записей, которые являются последовательными.

INSERT INTO #CONSEC
  SELECT a.ID, a.Start, b.Finish, b.Amount 
  FROM Data a JOIN Data b 
  ON (a.Finish = b.Start) AND (a.ID = b.ID)

Затем я могу размотать список, перебрав его курсором, и выполнив обновления обратно в таблицу данных, чтобы настроить (и удалить теперь посторонние записи из таблицы данных)

DECLARE CCursor  CURSOR FOR
  SELECT ID, Start, Finish, Amount FROM #CONSEC ORDER BY Start DESC

@Total = 0
OPEN CCursor
FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
WHILE @FETCH_STATUS = 0
BEGIN
  @Total = @Total + @Amount
  @Start_Last = @Start
  @Finish_Last = @Finish
  @ID_Last = @ID

  DELETE FROM Data WHERE Start = @Finish
  FETCH NEXT FROM CCursor INTO @ID, @START, @FINISH, @AMOUNT
  IF (@ID_Last<> @ID) OR (@Finish<>@Start_Last)
    BEGIN
      UPDATE Data
        SET Amount = Amount + @Total
        WHERE Start = @Start_Last
      @Total = 0
    END  
END

CLOSE CCursor
DEALLOCATE CCursor

Это все работает и имеет приемлемую производительность для типичных данных, которые я использую.

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

...