status: "accepted"

decision-makers: @darthunix, @EmirVildanov

--------------------------------

# Поддержка распределенных оконных функций в SQL

## Описание проблемы

Заканчивается добавление оконных функций в локальный байткод тарантула. Задача
этого ADR - описать, как строить IR для распределенного запроса и разрешать
конфликты.

Перед дальнейшим чтением рекомендуется ознакомиться с предметной областью:

- <https://postgrespro.ru/docs/postgrespro/17/tutorial-window>
- <https://postgrespro.ru/docs/postgrespro/17/sql-expressions#SYNTAX-WINDOW-FUNCTIONS>
- <https://www.sqlite.org/windowfunctions.html>

## Реализация

Природа оконных функций такова, что в каждом окне вычисления выполняются
независимо для каждого раздела `partition by`. Как следствие, такая логика
хорошо подходит для шардирования по ключу и перераспределения данных в
кластере.

### Ограничения

- Оконные функции разрешены только в проекциях.
- В рамках окна поддерживаются ROWS и RANGE.
- В рамках окна не поддерживаются исключения из рамки.
- Из оконных функций поддерживается только `row_number`.
- Из оконных агрегатов поддерживаются `sum`, `count`, `avg`, `max`, `min`.

### Изменения в IR

```
create table t (a int primary key, b int) distributed by (a);

explain select row_number() filter (where a > 0) over w from t
window w as (
  partition by a
  order by b desc
  rows between current row and unbounded following
);

-> projection
   output:
   - row
     - alias ("row_number")
       - over
         - row_number
         - ref $0
         - filter:
           - gt
             - ref a
             - const 0
   windows:
   -> window: $0
       - name: "w"
       - partition:
         - row
           - ref a
       - order:
         - type: desc
         - row
           - ref b
       - frame:
         - type: rows
         - left: current row
         - right: unbounded following

   -> scan t
```

1. В проекции помимо поля `children` нужно добавить отдельное поле `windows`.
   В нем будет храниться список реляционных операторов типа `window`,
   выражения из которого будут ссылаться на `output` первого ребенка
   проекции.
1. Появится новый реляционный оператор `window`, который будет содержать:
   - имя - используется только для именованных окон вида `window w as (...)`;
   - сегментирование - если отсутствует, то все строки рассматриваются как
     единый раздел;
   - сортировка - порядок сортировки внутри раздела (может отсутствовать);
   - рамка окна - определяет рамки окна относительно обрабатываемой строки.
1. Появится новое выражение `over`, которое будет содержать:
   - оконную или агрегационную функцию;
   - ссылку на окно;
   - фильтр для агрегационной функции (опционально).

### Разрешение конфликтов распределений

1. Производится разрешение конфликтов между различными окнами в проекции.
   На выходе получаем итоговое распределение данных, устраивающее все окна.

   ```mermaid
   flowchart LR
       Resolver{resolver}
       InnerWindow("segment (key1)") --> Resolver
       OuterWindow("segment (key1)") --> Resolver
       Resolver --> Output("segment (key1)")
   ```

   ```mermaid
   flowchart LR
       Resolver{resolver}
       InnerWindow("single/segment (key1)") --> Resolver
       OuterWindow("single/segment (key2)") --> Resolver
       Resolver --> Output("single")
   ```

1. Полученное распределение для окон сравнивается с распределением дочернего
   реляционного оператора.

   ```mermaid
   flowchart LR
       Resolver{resolver}
       Window("window") --> |"segment (key1)"| Resolver
       Child("child") --> |"segment (key1)/single/global"| Resolver
       Resolver --> Output("no motion")
   ```

   ```mermaid
   flowchart LR
       Resolver{resolver}
       Window("window") --> |"segment (key1)"| Resolver
       Child("child") --> |"segment (key2)/any"| Resolver
       Resolver --> Output("motion: segment (key1)")
   ```

   ```mermaid
   flowchart LR
       Resolver{resolver}
       Window("window") --> |"single"| Resolver
       Child("child") --> |"single/global"| Resolver
       Resolver --> Output("no motion")
   ```

   ```mermaid
   flowchart LR
       Resolver{resolver}
       Window("window") --> |"single"| Resolver
       Child("child") --> |"segment/any"| Resolver
       Resolver --> Output("motion: full")
   ```

1. Подзапросы в проекции никак не влияют на разрешение конфликтов в окнах.

#### Пример без перемещений

```
create table t (a int primary key, b int) distributed by (a);

explain select row_number() over (partition by a), b from t;

-> projection
   output:

   - row                          -> any
     - alias ("row_number")
       - over
         - row_number
         - ref $0
   - alias ("b")
     - ref b
     windows:
     -> window: $0                  -> segment (a)
         - partition:
           - row                     -> segment (a)
             - ref a

     ??? => [segment (a)] vs segment (a) => no motion

     -> scan t                      -> segment (a)

```

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

1. У сканирования распределение по колонке "а".
1. У единственного окна $0 распределение тоже по колонке "а".

Следовательно, проверяем наличие конфликта между распределением окна и
распределением дочернего оператора сканирования. В данном примере конфликта
нет и дополнительный Motion оператор в плане не нужен.

У выходного кортежа проекции будет распределение `any`, так как ключ
распределения был обрезан самой проекцией.

#### Пример частичного перемещения

```

create table t (a int primary key, b int) distributed by (a);

explain select sum(a) over w, b from t window w as (partition by b);

-> projection
   output:

   - row                          -> segment (b)
     - alias ("sum")"
       - over
         - sum
           - ref b
         - ref $1
   - alias ("b")
     - ref b
      windows:
      -> window: $0                  -> segment (b)
     - name: "w"
     - partition:
       - row                     -> segment (b)
         - ref b
   
      ??? => [segment (b)] vs segment (a) => motion segment (b)
   
      -> scan t                      -> segment (a)

```

В примере используется другая нотация оконных функций. Она позволяет удобно
переиспользовать одно окно в нескольких местах проекции. Рассмотрим разрешение
конфликтов с дочерним узлом сканирования:

1. У сканирования распределение по колонке "а".
1. У единственного окна $0 распределение тоже по колонке "b".

Существует конфликт между распределением, ожидаемым окном (по колонке "b")
и таблицей (по колонке "а"). Решением является перераспределить таблицу
по колонке "b" через motion с политикой segment.

У выходного кортежа проекции будет распределение `segment (b)`, так как ключ
по колонке "b" после перераспределения не был обрезан проекцией.

#### Пример полного перемещения

```

create table t (a int primary key, b int) distributed by (a);

explain select row_number() over (partition by b), sum(b) over (partition by a) from t;

-> projection
   output:

   - row                          -> single
     - alias ("row_number")
       - over
         - row_number
         - ref $0
   - alias ("sum")"
     - over
       - sum
         - ref b
       - ref $1
   
      windows:
      -> window: $0                  -> segment (b)
         - partition:
           - row                     -> segment (b)
             - ref b
      -> window: $1                  -> segment (a)
         - partition:
           - row                     -> segment (a)
             - ref a
   
      ??? => [segment (b), segment (a)] vs segment (a)
          => [single] vs segment (a) => motion full
   
      -> scan t                      -> segment (a)

```

Рассмотрим разрешение конфликтов в проекции с несколькими окнами.

1. У сканирования распределение по колонке "а".
1. У окна $0 распределение тоже по колонке "b".
1. У окна $1 распределение тоже по колонке "а".

Вначале решаем конфликт между распределением окон. Так как они шардированы
по разным колонкам, то нас устроить только полная копия на роутере. Значит,
итоговое распределение окон - single.

Дальше считаем распределение между окнами и сканированием. Чтобы получить
полную копию на роутере из шардированной по "а" таблицы, нужно сделать полное
перемещение через motion full.

У выходного кортежа проекции будет распределение single.

#### Пример полного перемещения без группировки в окне

```

create table t (a int primary key, b int) distributed by (a);

explain select row_number() over (), a from t;

-> projection
   output:

   - row                          -> single
     - alias ("row_number")
       - over
         - row_number
         - ref $0
      windows:
      -> window: $0                  -> single
   
      ??? => [global] vs segment (a) => motion full
   
      -> scan t                      -> segment (a)

```

Рассмотрим разрешение конфликтов в проекции, когда в окне нет группировки
`partition by`.

1. У сканирования распределение по колонке "a".
1. У окна $0 отсутствует группировка, поэтому распределение single. Его не нужно
   путать с global, так как нам нужно обеспечить исполнение ровно один раз, как
   в group by.

Между окном, ожидающим распределение single, и сканированием с распределением по
колонке "а" есть конфликт. Его разрешение - полное перемещение таблицы на роутер
через motion full.

У выходного кортежа проекции будет распределение single.

#### Пример частичного перемещения с подзапросом

```

create table t (a int primary key, b int) distributed by (a);

explain select
  sum(a) over (partition by b),
  (select b from t where a = 1) as c
from t;

-> projection
   output:

   - row                             -> any
     - alias ("sum")"
       - over
         - sum
           - ref b
         - ref $1
   - alias ("c")
     - ref $1
      windows:
      -> window: $0                  -> segment (b)
     - partition:
       - row                         -> segment (b)
         - ref b
   
      ??? => [segment (b)] vs segment (a) => motion segment (b)
   
      -> scan t                      -> segment (a)

subquery: $1                      -> global

??? => motion full

-> projection (b)
   -> selection (a = 1)
      -> scan t

```

Рассмотрим разрешение конфликтов в проекции с подзапросом и оконной функцией.

1. У сканирования распределение по колонке "a".
1. У окна $0 распределение по колонке "b".
1. В проекции есть подзапрос.

Между окном с распределением "b" и сканированием по колонке "а" конфликт
разрешается motion segment (b).

## Тестирование

В рамках МР в тарантул по добавлению локальных оконных функций было портировано
тестовое покрытие из sqlite (см. файлы `windowX.test.lua` в
<https://git.picodata.io/core/tarantool/-/tree/sd/window/test/sql-tap>). Для этого
использовался парсер грамматики тестов sqlite:
<https://git.picodata.io/darthunix/translator>.

В идеале мы бы хотели продублировать все это огромное тестовое покрытие внутри
picodata. Возможно, нам потребуется еще раз распарсить тесты и их ожидаемые
результаты, но уже транслировать их в pytest.
