Вопрос

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

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

Это не то, чего я хочу.

Нет правильного решения

Другие советы

Если вы прочитали книгу «Разработка временных приложений баз данных на SQL» автора Р. Т. Снодграсс (pdf-файл которого доступен на его веб-сайте в разделе «Публикации») и дойдя до рис. 6.25 на стр. 165–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, умеренно совместимой со стандартами.

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

  

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top