EXPLAIN¶
Команда EXPLAIN
добавляется перед DQL- и
DML-запросами для того, чтобы показать, как будет выглядеть
план исполнения запроса, при этом не выполняя сам запрос. План строится
на узле, к которому подключился пользователь, и позволяет наглядно
оценить структуру и последовательность действий при выполнении запроса.
EXPLAIN
является инструментом для анализа и оптимизации запросов.
Синтаксис¶
Примеры¶
Для начала рассмотрим план простого запроса на получение данных одного столбца таблицы:
EXPLAIN SELECT "score" FROM "scoring";
Вывод в консоль:
---
- - projection ("scoring"."score"::decimal -> "score")
- ' scan "scoring"'
...
Обязательными элементами плана запроса являются scan
и projection
.
Первый узел отвечает за сканирование (получение данных) таблицы, второй
— за выборку нужных столбцов. Построение проекции (projection
) всегда
происходит после сканирования. В рамках построения проекции планировщик
создает псевдоним для столбца: "scoring"."score" -> "score"
.
Если в запросе есть условие (where
), то в план добавляется узел
selection
:
EXPLAIN SELECT "score" FROM "scoring" WHERE "score" > 70;
Вывод в консоль:
---
- - projection ("scoring"."score"::decimal -> "score")
- ' selection ROW("scoring"."score"::decimal) > ROW(70::unsigned)'
- ' scan "scoring"'
...
Если projection
выбирает столбцы (атрибуты таблицы), то selection
фильтрует данные по строкам (ROW
).
Фраза selection ROW("scoring"."score") > ROW(70)'
является результатом
трансформации фильтра where "score" > 70
в where ("score") > (70)
,
т.е. превращения значения в строку из одного столбца.
Запрос с несколькими проекциями¶
Пример построения проекции из более сложного запроса:
EXPLAIN SELECT "id","name" FROM "characters"
EXCEPT
SELECT "id","name" FROM "assets"
WHERE "stock" > 1000;
Вывод в консоль:
---
- - except
- ' projection ("characters"."id"::integer -> "id", "characters"."name"::string -> "name")'
- ' scan "characters"'
- ' projection ("assets"."id"::integer -> "id", "assets"."name"::string -> "name")'
- ' selection ROW("assets"."stock"::integer) > ROW(1000::unsigned)'
- ' scan "assets"'
...
В таком плане запроса присутствует два блока projection
, перед
которыми стоит логическое условие (EXCEPT
). В каждом блоке есть свое
сканирование таблицы и, опционально, дополнительный фильтр по строкам
(selection
).
Варианты перемещения данных¶
В плане запроса может быть указан параметр motion
, который отражает
вариант перемещения данных между узлами хранения. Существуют следующие
четыре варианта:
- Локальная вставка. Представляет собой локальную материализацию
данных с подсчетом
значений
bucket_id
для каждого кортежа (соответственно, кортежи будут сгруппированы по этим бакетам). Перемещения данных на другие узлы хранения через узел-маршрутизатор не происходит. На текущем узле хранения будет локально создана виртуальная таблица из результатов читающего запроса или из переданныхVALUES
, а потом данные из нее будут вставлены локально в целевую таблицу. Планировщик отобразит значениеmotion [policy: local segment]
. - Локальная материализация. Данный вариант аналогичен предыдущему с
той разницей, что при материализации данных не происходит вычисление
bucket_id
. При таком запросе планировщик отобразит значениеmotion [policy: local]
. - Частичное перемещение. При выполнении запроса на каждый узел
кластера будет отправлена только востребованная часть данных (таблица
перераспределяется по новому ключу). При таком запросе планировщик
отобразит значение
motion [policy: segment]
. - Полное перемещение. На каждый узел кластера будет отправлена вся
таблица. Планировщик отобразит значение
motion [policy: full]
.
Перемещение данных происходит в тех случаях, когда в запросе требуется
обработать данные из нескольких таблиц или несколько раз из одной
таблицы (JOIN
, EXCEPT
, подзапросы), а также при выполнении
агрегатных функций (SUM
, COUNT
...). Перемещение
данных происходит по следующей схеме:
- на узле-маршрутизаторе (
router
) собираются запрошенные данные со всех узлов хранения (storage
); - в случае частичного перемещения (
motion [policy: segment]
), собранные данные объединяются в виртуальную таблицу с новым ключом шардирования; - узел-маршрутизатор отправляет на узлы хранения только нужные им строки из этой виртуальной таблицы.
Таким образом, перемещение обеспечивает корректность выполнения локальных запросов за счет копирования недостающих данных на каждый узел хранения в кластере.
Вариант перемещения данных (motion policy
) зависит от того, какие
данные доступны на локальных узлах хранения. При простом чтении из одной
таблицы перемещения нет никогда. При работе с несколькими таблицами
перемещения также может не быть, если в каждой части запроса адресуются
те столбцы, по которым таблица распределена (указан ключ шардирования).
При этом, использование агрегатных функций и/или соединения при работе с
одной или несколькими таблицами может потребовать частичного или полного
перемещения данных.
Примеры разных вариантов motion policy
приведены ниже.
Локальная вставка характерна для INSERT
с передачей строки
значений:
EXPLAIN INSERT INTO "assets" VALUES (1, 'Woody', 2561);
Вывод в консоль:
---
- - 'insert "assets" on conflict: fail'
- ' motion [policy: local segment([ref("COLUMN_1")])]'
- ' values'
- ' value row (data=ROW(1::unsigned, ''Woody''::string, 2561::unsigned))'
...
Локальная материализация относится к тем случаям, когда требуется
положить в память прочитанные данные из локального запроса для их
дальнейшей обработки. Перемещения данных нет и вычисление bucket_id
не
требуется (см.
подробнее).
Примером может служить удаление данных из таблицы:
EXPLAIN DELETE FROM "characters" WHERE "id" = 1;
Вывод в консоль:
---
- - delete "characters"
- ' motion [policy: local]'
- ' projection ("characters"."id"::integer -> pk_col_0)'
- ' selection ROW("characters"."id"::integer) = ROW(1::unsigned)'
- ' scan "characters"'
...
Локальная материализация происходит и при обновлении данных в тех
случаях, если не затрагивается колонка, по которой таблица шардирована.
Например, если при создании таблицы было указано шардирование по колонке
id
(distributed by ("id")
), то обновление данных в других колонках
не приведет к их перемещению через узел-маршрутизатор. Поскольку при
UPDATE
не происходит пересчет bucket_id
, то планировщик использует
политику local
:
EXPLAIN UPDATE "characters" SET "year" = 2010;
Вывод в консоль:
---
- - update "characters"
- '"year" = COL_0'
- ' motion [policy: local]'
- ' projection (2010::unsigned -> COL_0, "characters"."id"::integer -> COL_1)'
- ' scan "characters"'
- 'execution options:'
- sql_vdbe_max_steps = 45000
- vtable_max_rows = 5000
...
Частичное перемещение происходит, когда требуется отправить на узлы хранения недостающую часть таблицы.
Пример INSERT
со вставкой из читающего запроса другой таблицы, у
которой отличается ключ шардирования:
EXPLAIN INSERT INTO "assets" SELECT * FROM "assets3" WHERE "id3" = 1;
Вывод в консоль:
---
- - 'insert "assets" on conflict: fail'
- ' motion [policy: segment([ref("id3")])]'
- ' projection ("assets3"."id3"::integer -> "id3", "assets3"."name3"::string ->
"name3", "assets3"."stock3"::integer -> "stock3")'
- ' selection ROW("assets3"."id3"::integer) = ROW(1::unsigned)'
- ' scan "assets3"'
...
Пример JOIN
двух таблиц с разными ключами шардирования:
EXPLAIN SELECT "id","name" FROM "assets"
JOIN
(SELECT "id3","name3" FROM "assets3") AS "new_assets"
ON "assets"."id" = "new_assets"."id3";
Вывод в консоль:
---
- - projection ("assets"."id"::integer -> "id", "assets"."name"::string -> "name")
- ' join on ROW("assets"."id"::integer) = ROW("new_assets"."id3"::integer)'
- ' scan "assets"'
- ' projection ("assets"."id"::integer -> "id", "assets"."name"::string
-> "name", "assets"."stock"::integer -> "stock")'
- ' scan "assets"'
- ' motion [policy: segment([ref("id3")])]'
- ' scan "new_assets"'
- ' projection ("assets3"."id3"::integer -> "id3", "assets3"."name3"::string -> "name3")'
- ' scan "assets3"'
...
Пример UPDATE
с обновлением колонки, по которой шардирована таблица
(например, distributed by ("id", "name")
):
EXPLAIN UPDATE "characters" SET "name" = 'Etch', "year" = 2010 WHERE "id" = 2;
Вывод в консоль:
---
- - update "characters"
- '"id" = COL_0'
- '"name" = COL_1'
- '"year" = COL_2'
- ' motion [policy: segment([])]'
- ' projection ("characters"."id"::integer -> COL_0, ''Etch''::string ->
COL_1, 2010::unsigned -> COL_2, "characters"."id"::integer -> COL_3, "characters"."name"::string
-> COL_4)'
- ' selection ROW("characters"."id"::integer) = ROW(2::unsigned)'
- ' scan "characters"'
- 'execution options:'
- sql_vdbe_max_steps = 45000
- vtable_max_rows = 5000
...
Полное перемещение происходит, когда требуется скопировать всю внутреннюю таблицу (в правой части запроса) на все узлы, содержащие внешнюю таблицу (в левой части).
Пример JOIN
с соединениям не по колонкам шардирования для обеих
таблиц:
EXPLAIN SELECT "id","name","stock","year" FROM "characters"
JOIN
(SELECT "id" AS "number","stock" FROM "assets") AS stock
ON "characters"."id" = stock."number";
Вывод в консоль:
---
- - projection (
- "characters"."id" -> "id",
- "characters"."name" -> "name",
- "STOCK"."stock" -> "stock",
- "characters"."year" -> "year")',
- ' join on ROW("characters"."id") = ROW("STOCK"."number")',
- ' scan "characters"',
- ' projection (
- "characters"."id" -> "id",
- "characters"."name" -> "name",
- "characters"."year" -> "year")',
- ' scan "characters"',
- ' motion [policy: full]',
- ' scan "STOCK"',
- ' projection (
- "assets"."id" -> "number",
- "assets"."stock" -> "stock")',
- ' scan "assets"'
...
Пример выполнения агрегатной функции.
EXPLAIN SELECT COUNT("id") FROM "characters";
Вывод в консоль:
---
- - projection (sum(("8278664dae744882bfeec573f427fd0d_count_11"::integer))::decimal
-> "COL_1")
- ' motion [policy: full]'
- ' scan'
- ' projection (count(("characters"."id"::integer))::integer -> "8278664dae744882bfeec573f427fd0d_count_11")'
- ' scan "characters"'
...