Skip to content
Snippets Groups Projects
Commit c612f303 authored by Denis Smirnov's avatar Denis Smirnov
Browse files

doc: add readme for IR

parent 26d44b49
No related branches found
No related tags found
1 merge request!1414sbroad import
# Промежуточное представление
**Промежуточное представление** (*intermediate representation*, *IR*) представлено деревом логического плана, сериализованным в структуру Plan:
```rust
pub struct Plan {
nodes: Vec<Node>,
relations: Option<HashMap<String, Table>>,
slices: Option<Vec<Vec<usize>>>,
top: Option<usize>,
}
```
Чтобы не связываться с borrow checker и `RefCell`/`Rc`, все узлы дерева храним в единой арене (массив nodes). Указателями на узлы дерева являются индексы в массиве, поэтому поддерживается только операция добавления новых узлов в арену (старые удалять нельзя).
Так же отдельно храним вершину дерева (`top`), которая помечена как опциональная. Это сделано для удобства создания дерева, которое мы сериализуем снизу-вверх - вершину мы добавим в конце и на начальных этапах она будет пустой. Итоговый план без вершины считается невалидным.
Так же отдельно хранится опциональная хеш-таблица с отношениями (relations), куда входят как физические, так и виртуальные таблицы (для промежуточных результатов).
1. Отношения помечены опциональными, т.к. существуют SQL запросы, не использующие таблицы (`select 1`).
1. Отношения хранятся в хеш-таблице, чтобы гарантировать уникальность по имени и упростить их поиск при построении узлов плана из AST. Для виртуальных таблиц можно генерировать уникальное имя на основе их позиции в массиве.
Т.к. строится кластерный план запроса по сегментированным данным, то периодически в дереве придется вставлять команды перераспределения данных между узлами. На текущий момент перераспределение осуществляется в стиле `MapReduce` через координатор (возможно, когда-нибудь мы будем пересылать данные между сегментами напрямую, но не сейчас). Эти команды перемещения в дереве называются `Motion` узлами и приводят к созданию на координаторе виртуальных таблиц и материализации в них результатов с сегментов данных. Каждое поддерево любого `Motion` называется слайсом и является единицей исполнения на узлах с данными. `Motion` узлы в свою очередь образуют дерево, которое позволяет параллельно исполнять слайсы одного уровня (обходим дерево снизу-вверх). Поэтому, массив массивов слайсов - это подготовленный в правильном порядке список `Motion` узлов на исполнение (каждый массив можно исполнять параллельно). Опциональным он помечен опять же для удобства построения плана - на многих этапах трансформаций дерева слайсы не будут заполнены и появятся дальше. Итоговый план без слайсов считается невалидным.
## Узлы плана
Дерево плана состоит из узлов двух разных типов - выражения и реляционные операторы (узлы реляционной алгебры):
```rust
pub enum Node {
Expression(Expression),
Relational(Relational),
}
```
Каждый из них может свободно ссылаться друг на друга, для этого они хранятся в единой арене плана. Разделение узлов на два класса связано с разной логикой работы с этими узлами и нежеланием порождать одно гигантское перечисление. Мы используем `enum` для хранения различных классов узлов, чтобы не связываться с динамической диспетчеризацией в `rust` и не нести ее издержек по производительности.
### Выражения
Выражения описывают, откуда мы получили каждую колонку в кортеже и как выглядит сам кортеж.
```rust
pub enum Expression {
// 42 as a
Alias {
name: String,
child: usize,
},
// a > 42
// b in (select c from ...)
Bool {
left: usize,
op: operator::Bool,
right: usize,
},
// 42
Constant {
value: Value,
},
// &0 (left)
Reference {
branch: Branch,
/// expression position in the input row
position: usize,
},
// (a, b, 1)
Row {
list: Vec<usize>,
},
}
```
Кортеж представляет из себя дерево выражений, с вершиной типа `Row`, которая содержит в себе список имен колонок (тип `Alias`). Это важное соглашение, прописанное в коде - мы всегда знаем, как называется каждая колонка в каждом реляционном узле, что упростит нам сериализацию из AST. Листьями являются:
1. Константы (`Constant`) - поддерживают логический, числовой, строковый типы данных и `null`.
1. Указатели (`Reference`) - указывают на позицию входного кортежа в дочернем узле реляционного оператора. Мы специально используем именно позицию в кортеже, а не ссылку на выражение в массиве узлов плана, так как при трансформациях мы вполне можем выкинуть из дерева дочерний реляционный оператор. И при таком хранении нам не придется менять указатели в родительских выражениях. Так как мы можем создавать кортежи в реляционных операторах с несколькими ветвями (`UnionAll`, `InnerJoin`), и в случае `UnionAll` мы были бы вынуждены держать массив позиций в ссылке, что неудобно. Вместо этого мы так указываем ветвь (`Left`, `Right`, `Both`) откуда пришла наша колонка. По-умолчанию (для реляционных операторов с одним потомком) мы ставим левую ветвь.
Логических оператор - это частный случай бинарного оператора, который возвращает всегда логический тип. На первом этапе мы не добавляем другие бинарные (`+`, `-` и т.д.) или унарные (`<<`, `~`, ...) операторы. Он представляет из себя ветвление дерева выражений, где одна из веток вполне может ссылаться на реляционный оператор вложенного запроса (это должно проверяться в коде).
Например, кортеж `(a, b, a > b)` из входного кортежа `(a, b)` превратится в следующее дерево (псевдо-код):
```
Row <- id 15
- Alias ("a") <- id 11
- Reference { Left, 0} <- id 10
- Alias ("b") <- id 13
- Reference { Left, 0} <- id 12
- Alias ("col2") <- id 15 - сами генерируем имя колонки по ее позиции в массиве (начиная с нуля))
- Bool { 11, Gt, 13} <- id 14
```
### Реляционные операторы
Реляционные операторы описывают, какой вид преобразования мы делаем с входящим кортежем и какое распределение у данных в этом кортеже (**TODO:** возможно, это все же свойство выражения, а не реляционного оператора - обдумать еще раз вложенные запросы).
```rust
pub enum Relational {
InnerJoin {
distribution: Distribution,
condition: usize,
left: usize,
output: usize,
right: usize,
},
Motion {
child: usize,
distribution: Distribution,
output: usize,
},
Projection {
child: usize,
distribution: Distribution,
output: usize,
},
ScanRelation {
distribution: Distribution,
output: usize,
relation: String,
},
ScanSubQuery {
child: usize,
distribution: Distribution,
output: usize,
},
Selection {
child: usize,
distribution: Distribution,
filter: usize,
output: usize,
},
UnionAll {
distribution: Distribution,
left: usize,
right: usize,
output: usize,
},
}
```
У каждого реляционного оператора есть обязательные поля:
1. `output` - указатель на кортеж, т.е. на вершину `Row` в поддереве выражений.
1. `distribution` - политика распределения по кластеру данных, из которых составлен кортеж (подробнее ниже).
#### Сканирование
Листовой узел в дереве реляционных операторов, содержит указатель на уникальное имя отношения, из которого он получает данные. В роли отношения может выступать как сегментированная по кластеру таблица, так и промежуточные результаты из виртуальной таблицы (куда их положил исполнитель запроса). Сканирование получает то же самое распределение и набор колонок в кортеже, что его отношение.
#### Проекция
Реляционный оператор, который преобразует колонки в каждом входящем кортеже: удаляет, создает новые, меняет местами и т.д. Например, в `select 1, a from t`, где `t` - сегментированная по `b` таблица с колонками `a` и `b`, узел проекции получит на вход кортеж `(a, b)` из сканирования с распределением `b`, а на выходе выдаст кортеж `1, a` со случайным распределение (т.к. мы удалили колонку ключа распределения).
## Отношения
Отношения являются источниками кортежей и описывают, по какому правилу эти кортежи распределены по узлам кластера.
```rust
pub enum Table {
Segment {
columns: Vec<Column>,
key: Vec<usize>,
name: String,
},
Virtual {
columns: Vec<Column>,
data: Vec<Vec<Value>>,
name: String,
},
VirtualSegment {
columns: Vec<Column>,
data: HashMap<String, Vec<Vec<Value>>>,
key: Vec<usize>,
name: String,
},
}
```
Принципиально бывают двух видов:
1. Уже существовали до начала исполнения запроса - сегментированные по кластеру таблицы (`Segment`). Информацию о них мы получаем из схемы кластера, они являются основой для проверки узлов AST дерева и создания реляционных операторов с именами колонок в выражениях.
1. Созданы в процессе исполнения запроса (`Virtual*`), т.к. для выполнения последующих слайсов плана нам необходимо перераспределить результаты. Т.к. слайсы - это поддеревья `Motion` узлов, то в процессе исполнения мы будем хранить связку `Motion - Virtual*`, а на этапе линковки заменять `Motion` листья на сканирования поверх созданных таблиц с результатами.
Виртуальные таблицы бывают двух видов:
1. Собранные данные лежат вперемешку (`Virtual`). Они будут **полностью** сериализованы и отправлены на **каждый** узел кластера, участвующий в слайсе.
1. Собранные данные сегментированы (`VirtualSegment`) по определенному ключу распределения. Соответственно, отправлять следует на каждый узел кластера в запросе только соответствующий ему по ключу сегмент данных.
У каждого отношения есть обязательные атрибуты:
1. Уникальное имя (уникальность обеспечивается хеш таблицей в плане).
1. Колонки, которые описывают типы данных и имена в кортеже отношения. Для виртуальных таблиц колонки генерируются на основании кортежа в `Motion` узле.
```rust
pub struct Column {
pub name: String,
pub type_name: Type,
}
```
## Распределение
Распределение описывает, как расположены данные в отношениях внутри кластера.
```rust
pub enum Distribution {
Random,
Replicated,
Segment { key: Vec<usize> },
Single,
}
```
Например, данные из сегментов таблиц распределены по некоторому ключу в кластере (`Segment`), а константы в кортеже можно считать распределенными по всем узлам кластера, на которые отправим запрос (`Replicated`). Т.к. кортежи составляются из колонок таблиц и констант, то кортежи несут в себе информацию о расположении данных в кластере до тех пор, пока мы не затираем (удаляем или преобразуем) в них колонки ключа распределения (становится `Random`). Если же мы вставляем данные через координатор, то на какой-то момент они существуют в виртуальной таблице только на нем, и нигде более в кластере - такое распределение называется `Single`. Каждый узел плана сам выводит распределение из входящих в него кортежей: в родительский узел входят кортежи с определенным распределением из дочерних узлов, далее с колонками этих кортежей производятся различные преобразования. Зная, что было сделано с каждым входящим кортежем, можно сказать, какое распределение будет у исходящего кортежа.
Для узлов, у которых есть левое и правое поддерево (реляционные операторы `UnionAll`, `InnerJoin`; логическое выражение с вложенным запросом) возможен конфликт распределений дочерних узлов. Для его решения вставляется `Motion` узел, собирающий данные на координаторе и порождающий новый слайс с соответствующей виртуальной таблицей.
## Значения
Значения содержатся в константных выражениях и их главная задача - хранить введенные пользователем данные.
```rust
pub enum Value {
Boolean(bool),
Null,
Number(d128),
String(String),
}
```
Например, в запросе `select 1, 'hello', a from t;` `1` и `hello` - это значения констант. Так как они добавлены в тело запроса, то куда бы мы это запрос ни отправили, значения констант там и окажутся (распределение у констант `Replicated`). На текущий момент поддерживаются строки, числа, логический тип и `null`. Основная проблема с числами - нам нужно, чтобы любые записи числа (`1e0`, `1` и `1.0`) были сериализованы в одинаковые биты, чтобы потом в трансформациях их можно было сравнить и получить равенство. Для этого мы полагаемся на небезопасную обертку над С библиотекой `libdecnumber` (**TODO:** следить, не появятся ли нативные реализации, удовлетворяющие стандарту.)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment