#sql-server #datetime #recursion #grouping
#sql-сервер #дата и время #рекурсия #группировка
Вопрос:
У меня есть набор данных, аналогичный приведенному ниже образцу:
RecordId RecordDateTime AttributeA AttributeB Amount Value
--------- ------------------- ----------- ----------- ------ -------
1 2020-08-01 00:00:00 AttA01 AttB01 10.00 6510
2 2020-08-01 00:00:00 AttA01 AttB01 11.00 3620
3 2020-08-01 00:07:00 AttA01 AttB01 17.00 87430
4 2020-08-01 00:15:00 AttA01 AttB01 15.00 4140
5 2020-08-01 00:20:00 AttA02 AttB01 30.00 2350
6 2020-08-01 00:30:00 AttA01 AttB01 20.00 14560
7 2020-08-01 00:46:00 AttA01 AttB01 15.00 1370
8 2020-08-01 02:15:00 AttA01 AttB01 30.00 1380
9 2020-08-01 02:30:00 AttA01 AttB01 5.00 4490
10 2020-08-01 02:30:00 AttA01 AttB01 6.00 13100
Мне нужно сгруппировать все записи, где attributeA и attributeB совпадают и где время записи следующей записи находится в пределах 15 минут от времени записи текущей записи.
Результаты должны показывать самую раннюю дату группы, attributeA и attributeB, сумму Сумм и самую раннюю запись в значении группы в качестве начального значения и значение последней записи в качестве конечного значения.
Примерные результаты:
RecordDateTime AttributeA AttributeB Amount StartValue EndValue
------------------- ----------- ----------- ------ ---------- ----------
2020-08-01 00:00:00 AttA01 AttB01 73.00 3620 14560
2020-08-01 00:20:00 AttA02 AttB01 30.00 2350 2350
2020-08-01 00:46:00 AttA01 AttB01 15.00 1370 1370
2020-08-01 02:15:00 AttA01 AttB01 41.00 1380 13100
Я борюсь с рекурсивной группировкой, основанной на разнице во времени в 15 минут. Я попробовал несколько вещей, включая рекурсивный CTE, но на самом деле не добился никакого прогресса, который стоит показывать. Есть ли простое решение для группировки?
Предоставление более подробной информации по запросу.
Редактировать: Для приложения всегда будут начальная и конечная даты, которые пользователь может динамически выбирать, которые могут охватывать несколько месяцев, начиная с любого дня или времени.
Предоставленные данные являются изготовленными данными. В реальной жизни атрибутом a будет имя пользователя, а атрибутом B — местоположение, а записи — транзакции.
Любая серия транзакций, выполняемых одним и тем же пользователем в одном и том же местоположении с задержкой от 0 до 15 минут (>= 0 и <= 15) между каждой транзакцией, должна быть сгруппирована, суммирована и рассматриваться как одна транзакция. Другими словами, если транзакции находятся в пределах 15 минут от предыдущей транзакции, они должны быть сгруппированы с предыдущими транзакциями и любой другой транзакцией до этого, с которой сгруппирована предыдущая транзакция.
На данный момент меня не волнуют никакие транзакции, которые могли произойти до или после выбранной начальной и конечной даты и времени, даже если они могли бы составлять часть серии в выбранном диапазоне дат.
Ответ №1:
Это своего рода поворот в решении проблемы островов.
Учитывая этот пример данных:
CREATE TABLE #x
(
RecordId int,
RecordDateTime datetime,
AttributeA char(6),
AttributeB char(6),
Amount DECIMAL(12,2),
[Value] int
);
INSERT #x(RecordId, RecordDateTime, AttributeA, AttributeB, Amount, [Value])
VALUES
(1, '2020-08-01 00:00:00', 'AttA01', 'AttB01', 10.00, 6510),
(2, '2020-08-01 00:00:00', 'AttA01', 'AttB01', 11.00, 3620),
(3, '2020-08-01 00:07:00', 'AttA01', 'AttB01', 17.00, 87430),
(4, '2020-08-01 00:15:00', 'AttA01', 'AttB01', 15.00, 4140),
(5, '2020-08-01 00:20:00', 'AttA02', 'AttB01', 30.00, 2350),
(6, '2020-08-01 00:30:00', 'AttA01', 'AttB01', 20.00, 14560),
(7, '2020-08-01 00:46:00', 'AttA01', 'AttB01', 15.00, 1370),
(8, '2020-08-01 02:15:00', 'AttA01', 'AttB01', 30.00, 1380),
(9, '2020-08-01 02:30:00', 'AttA01', 'AttB01', 5.00, 4490),
(10, '2020-08-01 02:30:00', 'AttA01', 'AttB01', 6.00, 13100);
Я разделил это на три CTE:
x
определяет, какие строки являются «привязанными» строками, путем нахождения любой строки с отметкой времени, с которой прошло более 15 минут, чем с предыдущей строки для той же комбинации A / B (или с начала 2000 года в случае первого экземпляра любой комбинации A / B). Он присваивает столбец с именемis_anchor
, чтобы легко идентифицировать эти строки.y
просто применяет группу путем суммирования всехis_anchor
значений из всех предыдущих строк для заданной комбинации A / B, упорядоченной по времени. Эффект здесь заключается в том, что всем строкам, следующим за привязкой, которые не являются привязками, присваивается одна и та же группа, потому что все они добавляют0
к сумме.z
затем для каждой группы суммируютсяAmount
значения и находят первое и последнееValue
значения. Изz
мы выбираем только строки, гдеis_anchor = 1
.
Результирующий запрос:
;WITH x(a,b,d,am,v,is_anchor) AS
(
SELECT AttributeA, AttributeB, RecordDateTime, Amount, [Value],
CASE WHEN DATEDIFF(MINUTE, COALESCE(LAG(RecordDateTime, 1)
OVER
(
PARTITION BY AttributeA, AttributeB ORDER BY RecordDateTime
), '20000101'), RecordDateTime) > 15 THEN 1 ELSE 0 END
FROM #x
),
y AS
(
SELECT *,
grp = SUM(is_anchor) OVER
(PARTITION BY a,b ORDER BY d ROWS UNBOUNDED PRECEDING)
FROM x
),
z AS
(
SELECT d, a, b, is_anchor,
Amount = SUM(am) OVER (PARTITION BY a,b,grp),
StartValue = FIRST_VALUE(v) OVER
(PARTITION BY a,b,grp ORDER BY d, v),
EndValue = FIRST_VALUE(v) OVER
(PARTITION BY a,b,grp ORDER BY d DESC, v DESC)
FROM y
)
SELECT RecordDateTime = d, AttributeA = a, AttributeB = b,
Amount, StartValue, EndValue
FROM z
WHERE is_anchor = 1
ORDER BY d, a, b;
Результаты:
RecordDateTime AttributeA AttributeB Amount StartValue EndValue
======================= ========== ========== ====== ========== ========
2020-08-01 00:00:00.000 AttA01 AttB01 73.00 3620 14560
2020-08-01 00:20:00.000 AttA02 AttB01 30.00 2350 2350
2020-08-01 00:46:00.000 AttA01 AttB01 15.00 1370 1370
2020-08-01 02:15:00.000 AttA01 AttB01 41.00 1380 13100
Комментарии:
1. Это решение работает отлично и отчасти изменило мой подход к проблемам SQL в целом. Просто чтобы уточнить правила для начальных и конечных значений, когда для начального значения есть привязка к дате, нам нужно выбрать наименьшее значение, а когда для конечного значения есть привязка к дате, нам нужно выбрать наибольшее значение.
2. @Corvex111 обновил
FIRST_VALUE()
логику и добавилEndValue
то, что я как-то пропустил в первый раз.