Аналоги аналитических функций

Переменные также можно использовать для замены аналитических функций. Далее несколько примеров. Для простоты будем считать, что все поля NOT NULL, а сортировка и партиционирование (PARTITION BY) происходят по одному полю. Использование NULL значений и более сложных сортировок сделает примеры более громоздкими, но суть не поменяет.

Для примеров создадим таблицу TestTable:

CREATE TABLE TestTable(
  group_id INT NOT NULL,
  order_id INT UNIQUE NOT NULL,
  value INT NOT NULL
);

где

group_id – идентификатор группы (аналог окна аналитической функции);

order_id – уникальное поле, по которому будет производиться сортировка;

value – некоторое числовое значение.

Заполним нашу таблицу тестовыми данными:

INSERT TestTable(order_id, group_id, value)
SELECT *
FROM(
  SELECT 1 order_id, 1 group_id, 1 value
  UNION ALL SELECT 2, 1, 2
  UNION ALL SELECT 3, 1, 2
  UNION ALL SELECT 4, 2, 1
  UNION ALL SELECT 5, 2, 2
  UNION ALL SELECT 6, 2, 3
  UNION ALL SELECT 7, 3, 1
  UNION ALL SELECT 8, 3, 2
  UNION ALL SELECT 9, 4, 1
  UNION ALL SELECT 11, 3, 2
)T;

Примеры замены некоторых аналитических функций.

1) ROW_NUMBER() OVER(ORDER BY order_id)

SELECT T.*, @I:=@I+1 RowNum
FROM TestTable T,(SELECT @I:=0)I
ORDER BY order_id;
group_idorder_idvalueRowNum
1111
1222
1323
2414
2525
2636
3717
3828
4919
311210

2) ROW_NUMBER() OVER(PARTITION BY group_id ORDER BY order_id)

SELECT group_id, order_id, value, RowNum
FROM(
  SELECT T.*,
    IF(@last_group_id = group_id, @I:=@I+1, @I:=1) RowNum,
    @last_group_id := group_id
  FROM TestTable T,(SELECT @last_group_id:=NULL, @I:=0)I
  ORDER BY group_id, order_id
)T;
group_idorder_idvalueRowNum
1111
1222
1323
2411
2522
2633
3711
3822
31123
4911

3) SUM(value) OVER(PARTITION BY group_id ORDER BY order_id)

SELECT group_id, order_id, value, RunningTotal
FROM(
  SELECT T.*,
    IF(@last_group_id = group_id, @I:=@I+value, @I:=value) RunningTotal,
    @last_group_id := group_id
  FROM TestTable T, (SELECT @last_group_id:=NULL, @I:=0)I
  ORDER BY group_id, order_id
)T;
group_idorder_idvalueRunningTotal
1111
1223
1325
2411
2523
2636
3711
3823
31125
4911

4) LAG(value) OVER(PARTITION BY group_id ORDER BY order_id)

SELECT group_id, order_id, value, LAG
FROM(
  SELECT T.*,
    IF(@last_group_id = group_id, @last_value, NULL) LAG,
    @last_group_id := group_id,
    @last_value := value
  FROM TestTable T,(SELECT @last_value:=NULL, @last_group_id:=NULL)I
  ORDER BY group_id, order_id
)T;
group_idorder_idvalueLAG
111NULL
1221
1322
241NULL
2521
2632
371NULL
3821
31122
491NULL

Для LEAD всё то же самое, только нужно сменить сортировку на ORDER BY group_id, order_id DESC

Для функций COUNT, MIN, MAX всё несколько сложнее, поскольку, пока мы не проанализируем все строчки в группе(окне), мы не сможем узнать значение функции. MS SQL, например, для этих целей «спулит» окно (временно помещает строки окна в скрытую буферную таблицу для повторного к ним обращения), в MySQL такой возможности нет. Но мы можем для каждого окна вычислить значение функции в последней строке при заданной сортировке (т.е. после анализа всего окна), а затем, отсортировав строки в окне в обратном порядке, проставить вычисленное значение по всему окну.

Таким образом, нам понадобится две сортировки. Чтобы итоговая сортировка осталась той же, что и в примерах выше, отсортируем сначала по полям group_id ASC, order_id DESC, затем по полям group_id ASC, order_id ASC.

5) COUNT(*) OVER(PARTITION BY group_id)

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

SELECT group_id, order_id, value, Cnt
FROM(
  SELECT group_id, order_id, value,
    IF(@last_group_id = group_id, @MaxRowNum, @MaxRowNum := RowNumDesc) Cnt,
    @last_group_id := group_id
  FROM(
    SELECT T.*,
      IF(@last_group_id = group_id, @I:=@I+1, @I:=1) RowNumDesc,
      @last_group_id := group_id
    FROM TestTable T,(SELECT @last_group_id:=NULL, @I:=0)I
    ORDER BY group_id, order_id DESC /*первая сортировка*/
  )T,(SELECT @last_group_id:=NULL, @MaxRowNum:=NULL)I
  ORDER BY group_id, order_id /*вторая сортировка*/
)T;
group_idorder_idvalueCnt
1113
1223
1323
2413
2523
2633
3713
3823
31123
4911

Функции MAX и MIN вычисляются по аналогии. Приведу только пример для MAX:

6) MAX(value) OVER(PARTITION BY group_id)

SELECT group_id, order_id, value, MaxVal
FROM(
  SELECT group_id, order_id, value,
    IF(@last_group_id = group_id, @MaxVal, @MaxVal := MaxVal) MaxVal,
    @last_group_id := group_id
  FROM(
    SELECT T.*,
      IF(@last_group_id = group_id, GREATEST(@MaxVal, value), @MaxVal:=value) MaxVal,
      @last_group_id := group_id
    FROM TestTable T,(SELECT @last_group_id:=NULL, @MaxVal:=NULL)I
    ORDER BY group_id, order_id DESC
  )T,(SELECT @last_group_id:=NULL, @MaxVal:=NULL)I
  ORDER BY group_id, order_id
)T;
group_idorder_idvalueMaxVal
1112
1222
1322
2413
2523
2633
3712
3822
31122
4911

7) COUNT(DISTINCT value) OVER(PARTITION BY group_id)

Интересная вещь, которая отсутствует в MS SQL Server, но её можно вычислить с подзапросом, взяв MAX от RANK. Так же поступим и здесь. В первой сортировке вычислим RANK() OVER(PARTITION BY group_id ORDER BY value DESC), затем во второй сортировке проставим максимальное значение всем строкам в каждом окне:

SELECT group_id, order_id, value, Cnt
FROM(
  SELECT group_id, order_id, value,
    IF(@last_group_id = group_id, @Rank, @Rank := Rank) Cnt,
    @last_group_id := group_id
  FROM(
    SELECT T.*,
      IF(@last_group_id = group_id,
        IF(@last_value = value, @Rank, @Rank:=@Rank+1)
      , @Rank:=1) Rank,
      @last_group_id := group_id,
      @last_value := value
    FROM TestTable T,(SELECT @last_value:=NULL, @last_group_id:=NULL, @Rank:=0)I
    ORDER BY group_id, value DESC, order_id DESC
  )T,(SELECT @last_group_id:=NULL, @Rank:=NULL)I
  ORDER BY group_id, value, order_id
)T;
group_idorder_idvalueCnt
1112
1222
1322
2413
2523
2633
3712
3822
31122
4911