Объединить только смежные записи с помощью T-SQL
-
04-07-2019 - |
Вопрос
У меня есть (упрощенная для примера) таблица со следующими данными
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
Все это работает и имеет приемлемую производительность для типичных данных, которые я использую.
Я обнаружил одну маленькую проблему с приведенным выше кодом. Первоначально я обновлял таблицу данных в каждом цикле с помощью курсора. Но это не сработало. Похоже, что вы можете сделать только одно обновление для записи, и что несколько обновлений (чтобы продолжать добавлять данные) возвращаются к чтению исходного содержимого записи.