Аналитические функции на примере Oracle, функции LAG. Прочитав этот материал вы поймете, как работают аналитические функции в Oracle. Я рассмотрю только одну функцию, но принцип действия у них один.
Постановка задачи
Будем рассматривать на простом примере: Есть таблица APP_STAT, которая содержит данные о совершаемой операции. Каждая строка содержит время, описание операции и имя пользователя, который эту операцию выполнил. Своеобразный log. Нам нужно узнать, сколько пользователь "думает" между кликами и вычислить среднее время, которое каждый пользователь тратит на "обдумывание".
Создадим таблицу:
create table"APP_STAT" ( "T" date not null, "PAGE" varchar2(50 char) not null, "USER_NAME" varchar2(50 char) not null );
Соответственно, T - время, в которое выполнялась операция; PAGE - название страницы/операции, которую совершил пользователь; USER_NAME - имя пользователя.
Заполним ее данными:
alter session set NLS_DATE_FORMAT='DD-MM-YY HH24:MI:SS'; insert into APP_STAT (T, PAGE, USER_NAME) select '22-06-08 10:30:00','login','bart' from dual union select '22-06-08 10:31:00','search 1','bart' from dual union select '22-06-08 10:31:20','search 1','bart' from dual union select '22-06-08 10:31:20','login','homer' from dual union select '22-06-08 10:31:30','search 1','bart' from dual union select '22-06-08 10:32:00','new doc','bart' from dual union select '22-06-08 10:32:10','list doc','homer' from dual union select '22-06-08 10:32:20','view doc','homer' from dual union select '22-06-08 10:33:40','list doc','homer' from dual union select '22-06-08 10:34:00','view doc','homer' from dual union select '22-06-08 10:36:20','save doc','bart' from dual union select '22-06-08 10:36:30','delete doc','homer' from dual union select '22-06-08 10:36:30','list doc','bart' from dual union select '22-06-08 10:37:00','logout','homer' from dual union select '22-06-08 10:37:00','view doc','bart' from dual union select '22-06-08 10:37:10','edit doc','bart' from dual union select '22-06-08 10:38:30','save doc','bart' from dual union select '22-06-08 10:38:45','logout','bart' from dual;
Посмотрим, что мы сделали:
select * from APP_STAT order by T asc
T
PAGE
USER_NAME
22-06-08 10:30:00
login
bart
22-06-08 10:31:00
search 1
bart
22-06-08 10:31:20
login
homer
22-06-08 10:31:20
search 1
bart
22-06-08 10:31:30
search 1
bart
22-06-08 10:32:00
new doc
bart
22-06-08 10:32:10
list doc
homer
22-06-08 10:32:20
view doc
homer
22-06-08 10:33:40
list doc
homer
22-06-08 10:34:00
view doc
homer
22-06-08 10:36:20
save doc
bart
22-06-08 10:36:30
delete doc
homer
22-06-08 10:36:30
list doc
bart
22-06-08 10:37:00
logout
homer
22-06-08 10:37:00
view doc
bart
22-06-08 10:37:10
edit doc
bart
22-06-08 10:38:30
save doc
bart
22-06-08 10:38:45
logout
bart
Решение
Итак, необходимо узнать, как долго в среднем пользователь задерживается на страницах. Не хотелось бы грузить клиентскую часть, тем более, цель этой статьи как раз таки научить избегать этого. Будем использовать аналитическую функцию LAG, для лучшего понимания рассмотрим все мысли по шагам.
Шаг 0
Выберем лог посещений для одного пользователя 'bart'
sql [1]
select
to_char(T, 'HH24:MI:SS') T, PAGE, USER_NAME from APP_STAT where USER_NAME = 'bart' order by T
Как видим, для того, чтобы узнать количество времени, которое пользователь проводит, например, на странице 'login', нужно из времени, которое указано во второй строке вычесть время, которое указано в первой: 22/06/2008 10:31:00 [минус] 22/06/2008 10:30:00 [равно] 60 секунд
Маленькое замечание для тех, кто не помнит: в Oracle результат (date - date) - это количество дней между датами. Число это может быть дробным, так что легко узнать количество минут или секунд, умножив на соответственное число.
Первой моей мыслью обычно было загрузить эти данные на клиента и обработать результат на Java (C#, C++, [подставьте сюда свой любимый язык программирования]), но теперь я знаю, что существует такая аналитическая функция, как LAG
T
PAGE
USER_NAME
10:30:00
login
bart
10:31:00
search 1
bart
10:31:20
search 1
bart
10:31:30
search 1
bart
10:32:00
new doc
bart
10:36:20
save doc
bart
10:36:30
list doc
bart
10:37:00
view doc
bart
10:37:10
edit doc
bart
10:38:30
save doc
bart
10:38:45
logout
bart
Шаг 1
Выберем данные для пользователя 'bart' так, чтобы увидеть разницу во времени между текущей операцией в строке лога и предыдущей
sql [2]
select
to_char(T, 'HH24:MI:SS') T, (T - lag(T, 1, null) over (order by T)) * (24 * 60 * 60) DIFF, PAGE, USER_NAME from APP_STAT where USER_NAME = 'bart' order by T
Как видите, в колонке DIFF выводится разница между временем в колонке T для текущей строки и предыдущей. Не вдаваясь в подробности, пойдем дальше, объяснения будут позже.
T
DIFF
PAGE
USER_NAME
10:30:00
login
bart
10:31:00
60
search 1
bart
10:31:20
20
search 1
bart
10:31:30
10
search 1
bart
10:32:00
30
new doc
bart
10:36:20
260
save doc
bart
10:36:30
10
list doc
bart
10:37:00
30
view doc
bart
10:37:10
10
edit doc
bart
10:38:30
80
save doc
bart
10:38:45
15
logout
bart
Шаг 2
Мы выбрали с помощью sql [2] список операций для пользователя и время, которое прошло между двумя подряд идущими операциями. Но нас интересуют все пользователи (мы хотим вычислить, сколько времени пользователь тратит между кликами). Во втором шаге мы выберем все данные, не фильтруя записи по пользователям. Тут есть определенная проблема, так как у нас много пользователей и когда 'bart' думает, 'homer' уже успел что-то сделать. Т.е. последовательность действий и, соответственно, вычисление разности, нужно вести в контексте одного пользователя. Если мы используем sql [2] без фильтра, фактически мы получим время, в течении которого приходили клики, не важно от кого. Нас это не устраивает, потому пишем так:
sql [3]
select
to_char(T, 'HH24:MI:SS') T, (T - lag(T, 1, null) over (partition by USER_NAMEorder by T)) * (24 * 60 * 60) DIFF, PAGE, USER_NAME from APP_STAT order by T
Теперь у нас есть результирующий набор с временем в секундах, которое пользователь думает между кликами. Вычислить среднее время - очень просто
T
DIFF
PAGE
USER_NAME
10:30:00
login
bart
10:31:00
60
search 1
bart
10:31:20
login
homer
10:31:20
20
search 1
bart
10:31:30
10
search 1
bart
10:32:00
30
new doc
bart
10:32:10
50
list doc
homer
10:32:20
10
view doc
homer
10:33:40
80
list doc
homer
10:34:00
20
view doc
homer
10:36:20
260
save doc
bart
10:36:30
10
list doc
bart
10:36:30
150
delete doc
homer
10:37:00
30
view doc
bart
10:37:00
30
logout
homer
10:37:10
10
edit doc
bart
10:38:30
80
save doc
bart
10:38:45
15
logout
bart
Шаг 3
Просто вычисляем среднее значение:
sql [4]
select avg(DIFF) from ( select (T - lag(T, 1, null) over (partition by USER_NAMEorder by T)) * (24 * 60 * 60) DIFF from APP_STAT )
Обратите внимание: аналитические функции нельзя использовать в агрегациях, так что мы использовали подзапрос.
Получили: 54.0625
Теперь пришла пора раскрыть магию. Как же все это работает: Синтаксис функции LAG:
LAG (выражение, [смещение,] [значение-по-умолчанию]) over ([partition by раздел] выражение-сортировки)
Что же она возвращает? Все очень просто: значение [выражения] для строки, которая находится выше текущей на [смещение] строчек, как если бы все строки сортировались по [выражение-сортировки]. При этом весь набор строк разбивается на независимые наборы так, что значения в столбцах [раздел] для одного набора - одинаковые. Каждый набор обрабатывается отдельно.
sql [2]
select
to_char(T, 'HH24:MI:SS') T, (T - lag(T, 1, null) over (order by T)) * (24 * 60 * 60) DIFF, PAGE, USER_NAME from APP_STAT where USER_NAME = 'bart' order by T
В sql [2] мы использовали простой вариант без использования раздела (partition). На самом деле раздел есть, но он является полным результирующим набором (т.е. все строки из таблицы APP_STAT, где пользователь == 'bart').
Кроме того, мы использовали сортировку. Для чего она? Как Вы знаете, база данных вольна вернуть строки в любом порядке, а нам необходимо вычислять разницу между текущей операцией в логе и предыдущей. Этот порядок можно получить, отсортировав результат по времени (поле T). Для этого и нужна сортировка, чтобы база данных поняла, что такое "предыдущая строка".
Итак, в sql [2] функция LAG возвращает:
Значение поля T из предыдущей строки (смещение 1) или null (значение-по-умолчанию), если предыдущей строки не существует. При этом сортировка выполняется по полю T.
sql [3]
select
to_char(T, 'HH24:MI:SS') T, (T - lag(T, 1, null) over (partition by USER_NAMEorder by T)) * (24 * 60 * 60) DIFF, PAGE, USER_NAME from APP_STAT order by T
В sql [3] мы использовали дополнительное выражение раздела (partition). Мотивация разделения на раздел с точки зрения логики тут такова: мы хотим понять, сколько думал отдельный пользователь, а не как часто на сервер приходил запрос. Для этого разности во времени нужно считать в контексте каждого отдельного пользователя. Как получить это? Ответ напрашивается сам с собой: нужно рассматривать выборку строк по каждому отдельному пользователю. Для этого и был придуман параметр [partition by раздел]. Мы знаем, что нам нужно разделить полученный набор так, чтобы каждая группа строк содержала упоминание только одного пользователя, затем мы хотим применить ту же операцию, что и в sql [2], то есть подсчитать разность во времени. Визуально это выглядит так:
Шаг 0
Это все данные из таблицы лога
select
to_char(T, 'HH24:MI:SS') T, PAGE, USER_NAME USR from APP_STAT order by T asc
T
PAGE
USER
10:30:00
login
bart
10:31:00
search 1
bart
10:31:20
login
homer
10:31:20
search 1
bart
10:31:30
search 1
bart
10:32:00
new doc
bart
10:32:10
list doc
homer
10:32:20
view doc
homer
10:33:40
list doc
homer
10:34:00
view doc
homer
10:36:20
save doc
bart
10:36:30
delete doc
homer
10:36:30
list doc
bart
10:37:00
logout
homer
10:37:00
view doc
bart
10:37:10
edit doc
bart
10:38:30
save doc
bart
10:38:45
logout
bart
Шаг 1
Разделим данные для каждого пользователя
select to_char(T, 'HH24:MI:SS') T, PAGE, USER_NAME USR from APP_STAT where USER_NAME = 'bart' order by T asc
select
to_char(T, 'HH24:MI:SS') T, PAGE, USER_NAME USR from APP_STAT where USER_NAME = 'homer' order by T asc
bart:
T
PAGE
USR
10:30:00
login
bart
10:31:00
search 1
bart
10:31:20
search 1
bart
10:31:30
search 1
bart
10:32:00
new doc
bart
10:36:20
save doc
bart
10:36:30
list doc
bart
10:37:00
view doc
bart
10:37:10
edit doc
bart
10:38:30
save doc
bart
10:38:45
logout
bart
homer:
T
PAGE
USR
10:31:20
login
homer
10:32:10
list doc
homer
10:32:20
view doc
homer
10:33:40
list doc
homer
10:34:00
view doc
homer
10:36:30
delete doc
homer
10:37:00
logout
homer
Шаг 2
Подсчитаем время, которое пользователь тратил на обдумывание "между кликами"
Вы уже знаете, что это можно сделать с помощью функции LAG:
select
to_char(T, 'HH24:MI:SS') T, (T - lag(T, 1, null) over (order by T)) * (24 * 60 * 60) DIFF, PAGE, USER_NAME from APP_STAT where USER_NAME = 'bart' order by T
select
to_char(T, 'HH24:MI:SS') T, (T - lag(T, 1, null) over (order by T)) * (24 * 60 * 60) DIFF, PAGE, USER_NAME from APP_STAT where USER_NAME = 'homer' order by T
bart:
T
DIFF
PAGE
USER_NAME
10:30:00
login
bart
10:31:00
60
search 1
bart
10:31:20
20
search 1
bart
10:31:30
10
search 1
bart
10:32:00
30
new doc
bart
10:36:20
260
save doc
bart
10:36:30
10
list doc
bart
10:37:00
30
view doc
bart
10:37:10
10
edit doc
bart
10:38:30
80
save doc
bart
10:38:45
15
logout
bart
homer:
T
DIFF
PAGE
USER_NAME
10:31:20
login
homer
10:32:10
50
list doc
homer
10:32:20
10
view doc
homer
10:33:40
80
list doc
homer
10:34:00
20
view doc
homer
10:36:30
150
delete doc
homer
10:37:00
30
logout
homer
Шаг 3
Ну, а теперь сольем результаты в одну таблицу
Как это сделать с помощью LAG в одном запросе - ниже.
T
DIFF
PAGE
USER_NAME
10:30:00
login
bart
10:31:00
60
search 1
bart
10:31:20
login
homer
10:31:20
20
search 1
bart
10:31:30
10
search 1
bart
10:32:00
30
new doc
bart
10:32:10
50
list doc
homer
10:32:20
10
view doc
homer
10:33:40
80
list doc
homer
10:34:00
20
view doc
homer
10:36:20
260
save doc
bart
10:36:30
10
list doc
bart
10:36:30
150
delete doc
homer
10:37:00
30
view doc
bart
10:37:00
30
logout
homer
10:37:10
10
edit doc
bart
10:38:30
80
save doc
bart
10:38:45
15
logout
bart
Эти все операции производятся с помощью одного SQL:
select T, (T - lag(T, 1, null) over (partition by USER_NAMEorder by T)) * (24 * 60 * 60) DIFF, PAGE, USER_NAME from APP_STAT order by T
Выражение 'partition by USER_NAME' как раз и выполнило разделение таблицы из Шага 0 так, что мы получили две таблицы из Шага 1. Далее данные обрабатывались отдельно как в Шаге 2, строки склеились и результат вернулся отсортированным по полю T так, как записано в основном запросе. В результате вы получим такую-же таблицу, как и в результате Шага 3.
Обратите внимание, что финальная сортировка не влияет на вычисление функции LAG, так как в качестве параметра ей также передаются правила, по которому нужно отсортировывать каждый набор. Другими словами, финальная сортировка выполняется после вычисления в аналитических функциях. Например, если мы хотим посмотреть, как вел себя каждый пользователь, мы можем поменять сортировку в основном запросе:
select T, (T - lag(T, 1, null) over (partition by USER_NAMEorder by T)) * (24 * 60 * 60) DIFF, PAGE, USER_NAME from APP_STAT order by USER_NAME
Получим правильный результат:
T
DIFF
PAGE
USER_NAME
10:30:00
login
bart
10:31:00
60
search 1
bart
10:31:20
20
search 1
bart
10:31:30
10
search 1
bart
10:32:00
30
new doc
bart
10:36:20
260
save doc
bart
10:36:30
10
list doc
bart
10:37:00
30
view doc
bart
10:37:10
10
edit doc
bart
10:38:30
80
save doc
bart
10:38:45
15
logout
bart
10:31:20
login
homer
10:32:10
50
list doc
homer
10:32:20
10
view doc
homer
10:33:40
80
list doc
homer
10:34:00
20
view doc
homer
10:36:30
150
delete doc
homer
10:37:00
30
logout
homer
Заключение
В заключении отмечу, что аналитических функций - немало и они предоставляют различные возможности. В этой статье не описывались так называемые "окна" (window), которые наряду с "разделом" используются для определения группы строк, над которым нужно производить вычисления.