Очень интересная проблема, которую можно очень легко решить с помощью SQL, — это найти последовательный ряд событий во временном ряду. Но что такое последовательный ряд событий во временном ряду?
Возьмите переполнение стека, например. У переполнения стека есть крутая система репутации, которая использует значки для поощрения определенного поведения. Как социальный сайт, они поощряют пользователей посещать платформу каждый день. Таким образом, два отличных значка награждаются:
Неформально очевидно, что это значит. Вам нужно будет войти в день 1. Затем снова в день 2. Затем снова (возможно, несколько раз, это не имеет значения) в день 3. Забыли войти в день 4? По электронной почте Ой. Мы начнем считать снова.
Как это сделать в SQL?
В этом блоге каждая проблема найдет свое решение в SQL . Так же и это. И чтобы решить эту проблему, мы собираемся использовать потрясающий Stack Exchange Data Explorer , который предоставляет много общедоступной информации об использовании Stack Exchange.
Обратите внимание, что мы не будем запрашивать последующие дни посещений, так как эта информация не является общедоступной. Вместо этого давайте запросить последовательные дни сообщений, которые сделал пользователь.
Базой данных является SQL Server, поэтому мы можем выполнить следующую инструкцию:
|
1
2
3
4
|
SELECT DISTINCT CAST(CreationDate AS DATE) AS dateFROM PostsWHERE OwnerUserId = ##UserId##ORDER BY 1 |
… который, для моего собственного UserId генерирует что-то вроде:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
date ----------2010-11-262010-11-272010-11-292010-11-302010-12-012010-12-022010-12-032010-12-052010-12-062010-12-072010-12-082010-12-092010-12-132010-12-14...(769 rows) |
( запустите заявление самостоятельно, здесь )
Как мы видим из данных, в первые дни были пробелы:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
date --------------------------------------2010-11-262010-11-27 <---- Gap here after 2 days2010-11-292010-11-302010-12-012010-12-022010-12-03 <---- Gap here after 5 days2010-12-052010-12-062010-12-072010-12-082010-12-09 <---- Gap here after 5 days2010-12-132010-12-14... |
Визуально очень легко увидеть, сколько дней подряд были посты без пробелов. Но как это сделать с SQL?
Чтобы упростить задачу, давайте «храним» отдельные запросы в общих табличных выражениях. Приведенным выше запросом мы назовем dates :
|
01
02
03
04
05
06
07
08
09
10
|
WITH -- This table contains all the distinct date -- instances in the data set dates(date) AS ( SELECT DISTINCT CAST(CreationDate AS DATE) FROM Posts WHERE OwnerUserId = ##UserId## )... |
Теперь цель результирующего запроса состоит в том, чтобы поместить все последовательные даты в одну группу, чтобы мы могли агрегировать по этой группе. Следующий запрос — это то, что мы хотим написать:
|
1
2
3
4
5
6
7
|
SELECT COUNT(*) AS consecutiveDates, MIN(week) AS minDate, MAX(week) AS maxDateFROM groupsGROUP BY grpORDER BY 1 DESC, 2 DESC |
Мы хотели бы объединить каждую группу « grp » и посчитать количество дат в группе, а также найти самую низкую и самую высокую дату в каждой группе.
Генерация групп для последовательных дат
Давайте снова посмотрим на данные, и чтобы проиллюстрировать идею, мы добавим последовательные номера строк, независимо от пробелов в датах:
|
01
02
03
04
05
06
07
08
09
10
11
12
|
row number date --------------------------------1 2010-11-262 2010-11-273 2010-11-29 <-- gap before this row4 2010-11-305 2010-12-016 2010-12-027 2010-12-038 2010-12-05 <-- gap before this row |
Как вы можете видеть, независимо от того, есть ли разрыв между датами (две даты не являются последовательными), их номера строк все равно будут последовательными. Мы можем сделать это с ROW_NUMBER() оконной функции ROW_NUMBER() , очень легко:
|
1
2
3
4
|
SELECT ROW_NUMBER() OVER (ORDER BY date) AS [row number], dateFROM dates |
Теперь давайте проверим следующий интересный запрос:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
WITH -- This table contains all the distinct date -- instances in the data set dates(date) AS ( SELECT DISTINCT CAST(CreationDate AS DATE) FROM Posts WHERE OwnerUserId = ##UserId## ), -- Generate "groups" of dates by subtracting the -- date's row number (no gaps) from the date itself -- (with potential gaps). Whenever there is a gap, -- there will be a new group groups AS ( SELECT ROW_NUMBER() OVER (ORDER BY date) AS rn, dateadd(day, -ROW_NUMBER() OVER (ORDER BY date), date) AS grp, date FROM dates )SELECT *FROM groupsORDER BY rn |
Вышеприведенный запрос дает:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
rn grp date --- ---------- ----------1 2010-11-25 2010-11-262 2010-11-25 2010-11-273 2010-11-26 2010-11-294 2010-11-26 2010-11-305 2010-11-26 2010-12-016 2010-11-26 2010-12-027 2010-11-26 2010-12-038 2010-11-27 2010-12-059 2010-11-27 2010-12-0610 2010-11-27 2010-12-0711 2010-11-27 2010-12-0812 2010-11-27 2010-12-0913 2010-11-30 2010-12-1314 2010-11-30 2010-12-14 |
( запустите заявление самостоятельно, здесь )
Все, что мы сделали, это grp номер строки из даты, чтобы получить новую дату « grp ». Фактическая дата, полученная таким способом, не имеет значения. Это просто вспомогательное значение.
Однако мы можем гарантировать, что для последовательных дат значение grp будет одинаковым, поскольку для всех последовательных дат следующие два уравнения дают истину:
|
1
2
|
date2 - date1 = 1 // difference in days between datesrn2 - rn1 = 1 // difference in row numbers |
Тем не менее, для непоследовательных дат, хотя разница в числах строк все еще равна 1, разница в днях больше не равна 1. Теперь группы можно легко увидеть:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
rn grp date --- ---------- ----------1 2010-11-25 2010-11-262 2010-11-25 2010-11-273 2010-11-26 2010-11-294 2010-11-26 2010-11-305 2010-11-26 2010-12-016 2010-11-26 2010-12-027 2010-11-26 2010-12-038 2010-11-27 2010-12-059 2010-11-27 2010-12-0610 2010-11-27 2010-12-0711 2010-11-27 2010-12-0812 2010-11-27 2010-12-0913 2010-11-30 2010-12-1314 2010-11-30 2010-12-14 |
Таким образом, полный запрос теперь можно увидеть здесь:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
WITH -- This table contains all the distinct date -- instances in the data set dates(date) AS ( SELECT DISTINCT CAST(CreationDate AS DATE) FROM Posts WHERE OwnerUserId = ##UserId## ), -- Generate "groups" of dates by subtracting the -- date's row number (no gaps) from the date itself -- (with potential gaps). Whenever there is a gap, -- there will be a new group groups AS ( SELECT ROW_NUMBER() OVER (ORDER BY date) AS rn, dateadd(day, -ROW_NUMBER() OVER (ORDER BY date), date) AS grp, date FROM dates )SELECT COUNT(*) AS consecutiveDates, MIN(week) AS minDate, MAX(week) AS maxDateFROM groupsGROUP BY grpORDER BY 1 DESC, 2 DESC |
И это дает:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
consecutiveDates minDate maxDate ---------------- ------------- ------------- 14 2012-08-13 2012-08-2614 2012-02-03 2012-02-1610 2013-10-24 2013-11-0210 2011-05-11 2011-05-209 2011-06-30 2011-07-087 2012-01-17 2012-01-237 2011-06-14 2011-06-206 2012-04-10 2012-04-156 2012-04-02 2012-04-076 2012-03-26 2012-03-316 2011-10-27 2011-11-016 2011-07-17 2011-07-226 2011-05-23 2011-05-28... |
( запустите заявление самостоятельно, здесь )
Бонусный запрос 1: найдите недели подряд
Тот факт, что мы выбрали детальность дней в приведенном выше запросе, является случайным выбором. Мы просто взяли временную метку из нашего временного ряда и «свернули» ее до желаемой степени детализации, используя функцию CAST :
|
1
|
SELECT DISTINCT CAST(CreationDate AS DATE) |
Если мы хотим знать последовательные недели, мы просто изменим эту функцию на другое выражение, например
|
1
2
|
SELECT DISTINCT datepart(year, CreationDate) * 100 + datepart(week, CreationDate) |
Это новое выражение берет год и неделю и генерирует значения, например 201503 для недели 03 в 2015 году. Остальная часть утверждения остается точно такой же:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
WITH weeks(week) AS ( SELECT DISTINCT datepart(year, CreationDate) * 100 + datepart(week, CreationDate) FROM Posts WHERE OwnerUserId = ##UserId## ), groups AS ( SELECT ROW_NUMBER() OVER (ORDER BY week) AS rn, dateadd(day, -ROW_NUMBER() OVER (ORDER BY week), week) AS grp, week FROM weeks )SELECT COUNT(*) AS consecutiveWeeks, MIN(week) AS minWeek, MAX(week) AS maxWeekFROM groupsGROUP BY grpORDER BY 1 DESC, 2 DESC |
И мы получим следующий результат:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
consecutiveWeeks minWeek maxWeek ---------------- ------- ------- 45 201401 201445 29 201225 201253 25 201114 201138 23 201201 201223 20 201333 201352 16 201529 201544 15 201305 201319 12 201514 201525 12 201142 201153 9 201502 201510 7 201447 201453 7 201321 201327 6 201048 201053 4 201106 201109 3 201329 201331 3 201102 201104 2 201301 201302 2 201111 201112 1 201512 201512 |
( запустите заявление самостоятельно, здесь )
Неудивительно, что последовательные недели охватывают гораздо более длинные диапазоны, поскольку я обычно широко использую Stack Overflow.
Бонусный запрос 2: Упростите запрос с помощью DENSE_RANK ()
В предыдущей статье мы показали, что SQL Trick: ROW_NUMBER() должен SELECT то, что DENSE_RANK() должен SELECT DISTINCT .
Если мы вернемся к нашему примеру с последовательными днями, мы можем переписать запрос, чтобы найти отличные даты И группы за один раз, используя DENSE_RANK() :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
WITH groups(date, grp) AS ( SELECT DISTINCT CAST(CreationDate AS DATE), dateadd(day, -DENSE_RANK() OVER (ORDER BY CAST(CreationDate AS DATE)), CAST(CreationDate AS DATE)) AS grp FROM Posts WHERE OwnerUserId = ##UserId## )SELECT COUNT(*) AS consecutiveDates, MIN(date) AS minDate, MAX(date) AS maxDateFROM groupsGROUP BY grpORDER BY 1 DESC, 2 DESC |
( запустите заявление самостоятельно, здесь )
Если вышеупомянутое не имеет смысла, я рекомендую прочитать нашу предыдущую статью здесь, которая объясняет это:
дальнейшее чтение
Выше приведен один очень полезный пример использования оконных функций ( ROW_NUMBER() ) в SQL. Узнайте больше о функциях окна в любой из следующих статей:
- Используйте этот аккуратный трюк с функцией окна для расчета разницы во времени во временном ряду
- Вероятно, самая крутая особенность SQL: оконные функции
- Не упустите возможности SQL Power с FIRST_VALUE (), LAST_VALUE (), LEAD () и LAG ()
- Разница между ROW_NUMBER (), RANK () и DENSE_RANK ()
| Ссылка: | Как найти самую длинную последовательность событий в SQL от нашего партнера по JCG Лукаса Эдера в блоге JAVA, SQL и AND JOOQ . |
