Этот документ представляет базовую структуру и интерпретацию планов запросов, а также предоставляет примеры и пояснения как для однокузовных, так и для параллельных (распределённых) планов запросов.
План запроса (также известный как план выполнения или план выполнения запроса) — это ключевой компонент жизненного цикла запроса базы данных. Он генерируется оптимизатором запросов и предоставляет подробное описание того, как SQL-заявка будет выполнена в базе данных.
В централизованных базах данных (таких как PostgreSQL и другие системы с одним узлом) все данные находятся на одном узле, и весь процесс выполнения запроса происходит локально. Для удовлетворения требований запроса достаточно однокузовного плана.
Однако в распределённых базах данных (таких как Greenplum и YMatrix) данные распределены между несколькими узлами. При создании таблиц пользователь должен определить стратегию распределения и ключ распределения. Данные затем распределяются между несколькими экземплярами MXSegment на основе этого ключа, причём каждый MXSegment содержит лишь часть полного набора данных.
В результате традиционные однокузовные планы запросов оказываются недостаточными. Необходимо применять новый тип параллельного плана запроса (или распределённого плана запроса). Такой план использует распределение данных для обеспечения эффективных параллельных вычислений и гарантирует, что обработка происходит как можно ближе к данным — в идеале непосредственно на экземплярах MXSegment, минимизируя вычисления на MXMaster. Этот подход максимизирует производительность запросов.
В YMatrix после загрузки данных в таблицу необходимо сначала выполнить команду ANALYZE для сбора статистики. Также рекомендуется периодически выполнять полные операции ANALYZE. Точная статистика необходима оптимизатору для принятия обоснованных решений о выборе оптимального плана запроса, что повышает производительность.
После корректного сбора статистики используйте команду EXPALIN, чтобы просмотреть логические планы запросов, сгенерированные оптимизатором для заданного SQL-запроса. Оптимизатор обычно оценивает несколько потенциальных планов и выбирает наиболее эффективный. Анализ и понимание этих планов помогают разработчикам и администраторам баз данных (DBA) оптимизировать производительность запросов и выявлять потенциальные узкие места.
План запроса имеет древовидную структуру, состоящую из нескольких узлов плана, где каждый узел представляет конкретную операцию, такую как сканирование таблицы, соединение или агрегация, и передаёт свои результаты родительскому узлу для дальнейшей обработки.
Каждый узел содержит детальную информацию о выполнении, включая порядок выполнения, метод доступа, уровень параллелизма и оценку стоимости. Дерево плана запроса начинается с листовых узлов и заканчивается корневым узлом. Обход дерева снизу вверх позволяет проследить полный путь выполнения запроса.
В распределённых планах запросов распространённые типы узлов плана и соответствующие им операторы приведены ниже:
| Тип узла плана | Описание операции | Скалярный оператор | Векторизованный оператор |
|---|---|---|---|
| Узлы сканирования | Отвечают за сканирование таблиц или индексов | SeqScan: Последовательное сканирование всех строк таблицы IndexScan: Обход индекса для извлечения строк из таблицы BitmapHeapScan & BitmapIndexScan: Часто используются вместе. Сканирование битовой карты индекса считывает подходящие записи индекса и строит битовую карту (точность зависит от размера индекса). Затем сканирование битовой карты таблицы использует эту карту для доступа к таблице. Примечание: Сканирование индекса извлекает одну запись индекса за раз и немедленно проверяет соответствующую строку таблицы, чередуя доступ к индексу и таблице — подходит для точечных запросов. Сканирование битовой карты разделяет процесс: сначала собирает все совпадающие записи индекса в памяти и сортирует их, затем обращается к таблице — идеально подходит для широких диапазонных запросов. DynamicSeqScan: Использует функцию выбора партиций для определения нужных партиций ParallelSeq: При включении параллельного запроса количество параллельных рабочих процессов устанавливается равным числу ядер CPU для максимальной эффективности |
MxVScan: Векторизованное последовательное сканирование для таблиц MARS2/MARS3/AOCO |
| Узлы соединения | Объединяют две или более таблиц | HashJoin: Создаёт хеш-таблицу из меньшей таблицы, используя столбец соединения в качестве ключа хеширования. Затем сканирует большую таблицу, вычисляет хеш-ключи и проверяет их в хеш-таблице на совпадения. HashCond показывает столбцы, используемые для соединения. NestedLoopJoin: Перебирает строки одного набора данных и для каждой строки сканирует другой набор. Требует трансляции одной таблицы, если только обе таблицы не имеют одинаковый ключ распределения, позволяя выполнять локальные соединения без трансляции. MergeJoin: Сортирует оба набора данных и объединяет их |
MxVHashJoin: Векторизованное хеш-соединение для таблиц MARS2/MARS3/AOCO |
| Другие узлы плана | Materialize: Кэширует результаты подзапроса; сохраняет выходные кортежи при первом выполнении для повторного использования верхними узлами Sort: Сортирует кортежи, возвращаемые дочерними узлами Group: Группирует отсортированные кортежи из нижних узлов Agg: Выполняет агрегатные функции Unique: Удаляет дубликаты строк Hash: Вычисляет хеш-значения; вспомогательный узел для HashJoin Limit: Обрабатывает условия LIMIT/OFFSETWindowAgg: Обрабатывает оконные функции LockRows: Блокирует выбранные строки, опционально отфильтрованные по FOR SHARE/FOR UPDATESetOP: Объединяет несколько наборов данных с помощью объединения ( UNION), пересечения (INTERSECT) или разности (EXCEPT/MINUS)SubqueryScan: Выполняет подзапросы Append: Конкатенирует несколько наборов результатов Result: Обрабатывает выражения, вычисляемые один раз, или простые операторы VALUES с только INSERT ... VALUESGroupAgg: Сначала сортирует данные по групповым столбцам, чтобы одинаковые группы были соседними, затем агрегирует при сканировании HashAgg: Группирует и агрегирует с использованием хеш-таблицы, сортировка не требуется Broadcast Motion: Каждый MXSegment отправляет свои строки всем остальным, в результате чего на каждом сегменте создаётся полная локальная копия Redistribute Motion: Каждый MXSegment пересчитывает распределение данных на основе ключа переопределения и отправляет строки на соответствующий целевой сегмент Gather Motion: Результаты всех MXSegment собираются в единый поток. Для большинства запросов это последний шаг |
MxVMotion: Векторизованное Gather Motion для таблиц MARS2/MARS3/AOCO MxVSort: Векторизованная сортировка для таблиц MARS2/MARS3/AOCO MxVResult: Векторизованная операция Result MxVSubqueryScan: Векторизованное сканирование подзапроса MxVLimit: Векторизованная операция Limit MxVHashAgg: Векторизованная операция HashAgg MxVGroupAgg: Векторизованная операция GroupAgg MxVAppend: Векторизованная операция Append |
Эти узлы могут комбинироваться по мере необходимости для формирования полного плана запроса.
Из-за его вложенной древовидной структуры план запроса необходимо читать снизу вверх.
Ниже приведены примеры однокузовного и параллельного планов запроса.
Сначала создадим две таблицы. (Примеры таблиц созданы в PostgreSQL 9.2)
=# CREATE TABLE sale (
cid int,
pid int,
pnum int,
qty float,
date timestamp
);
=# CREATE TABLE customer (
cid int,
cname text,
cwechat text
);
Вставим тестовые данные.
=# INSERT INTO sale (cid, pid, pnum, qty, date) VALUES
(1, 1, 1, 10.0, '2023-07-20 08:37:06'),
(2, 3, 6, 35.0, '2023-07-15 18:22:00'),
(3, 2, 1, 3.0, '2023-07-17 11:37:00'),
(4, 1, 2, 10.0, '2023-07-20 10:37:09'),
(5, 3, 2, 35.0, '2023-07-17 08:12:06'),
(6, 1, 1, 10.0, '2023-07-02 12:10:23'),
(7, 3, 1, 35.0, '2023-07-07 08:07:00'),
(8, 5, 3, 99.0, '2023-07-20 10:37:06'),
(9, 3, 1, 35.0, '2023-07-20 15:30:00'),
(10, 3, 1, 35.0, '2023-07-20 09:00:00');
=# INSERT INTO customer (cid, cname, cwechat) VALUES
(1, 'kepler', 'kepler1997'),
(2, 'amiee', 'amiee1995'),
(3, 'lila', 'lila2002'),
(4, 'cici', 'cici1982'),
(5, 'damien', 'damien1983'),
(6, 'ariana', 'ariana1990'),
(7, 'kanye', 'kanye1960'),
(8, 'taylor', 'taylor1996'),
(9, 'michael', 'mike2005'),
(10, 'ray', 'ray1957');
Теперь вычислим общую сумму продаж по каждому клиенту, объединив таблицы sale и customer по общему ключу cid, и проанализируем план запроса.
Сначала соберём статистику перед генерацией плана.
=# ANALYZE sale;
=# ANALYZE customer;
Сгенерируем план запроса.
=# EXPLAIN SELECT c.cname, sum(s.pnum * s.qty) AS amount FROM sale s, customer c WHERE s.cid = c.cid GROUP BY c.cname;
QUERY PLAN
------------------------------------------------------------------------------
HashAggregate (cost=2.56..2.66 rows=10 width=14)
Group Key: c.cname
-> Hash Join (cost=1.23..2.46 rows=10 width=18)
Hash Cond: (s.cid = c.cid)
-> Seq Scan on sale s (cost=0.00..1.10 rows=10 width=16)
-> Hash (cost=1.10..1.10 rows=10 width=10)
-> Seq Scan on customer c (cost=0.00..1.10 rows=10 width=10)
(7 rows)
Этот план запроса содержит два основных узла: Hash Join и HashAggregate. Каждый узел включает три оценки стоимости: стоимость, количество строк (rows) и ширину строки (width). Эти параметры помогают оценить время выполнения, размер результирующего набора и сложность обработки и передачи данных.
Диаграмма дерева плана запроса представлена ниже:
_1691547703.png)
Чтобы интерпретировать план запроса, необходимо понимать его вложенность. Каждый узел представляет подоперацию, и данные передаются сверху вниз от дочернего узла к родительскому. Оптимизатор начинает с последовательного сканирования таблицы customer, стоимость которого составляет 1.10. Затем он создаёт хеш-таблицу в памяти. Одновременно он сканирует таблицу sale, вычисляет хеш-значения и сравнивает их с хеш-таблицей, используя поле cid в качестве ключа соединения. На этом этапе начальная и общая стоимость составляют соответственно 1.23 и 2.46. Наконец, данные группируются и агрегируются по полю cname с использованием хеш-таблицы, при этом общая стоимость запроса составляет 2.66.
Подробное объяснение компонентов:
Как и ранее, создадим две таблицы. (Примеры таблиц созданы в YMatrix 5.1 с использованием движка хранения MARS3)
Перед созданием таблиц MARS3 установите расширение matrixts для функциональности временных рядов.
=# CREATE EXTENSION matrixts;
=# CREATE TABLE sale (
cid int,
pid int,
pnum int,
qty float,
date timestamp
) USING MARS3
DISTRIBUTED BY (cid,date)
ORDER BY (cid,date);
=# CREATE TABLE customer (
cid int,
cname text,
cwechat text
) USING MARS3
DISTRIBUTED BY (cid)
ORDER BY (cid);
Вставьте те же тестовые данные и соберите статистику.
Теперь сгенерируем план запроса с использованием того же SQL-запроса. Поскольку векторизация включена по умолчанию в YMatrix, появляется множество векторизованных операторов, повышающих производительность.
=# EXPLAIN SELECT c.cname, sum(s.pnum * s.qty) AS amount FROM sale s, customer c WHERE s.cid = c.cid GROUP BY c.cname;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
Gather Motion 4:1 (slice1; segments: 4) (cost=72.26..72.44 rows=10 width=14)
-> GroupAggregate (cost=72.26..72.32 rows=2 width=14)
Group Key: c.cname
-> Sort (cost=72.26..72.27 rows=2 width=18)
Sort Key: c.cname
-> Redistribute Motion 4:4 (slice2; segments: 4) (cost=0.00..72.25 rows=2 width=18)
Hash Key: c.cname
-> Nested Loop (cost=0.00..72.20 rows=2 width=18)
Join Filter: (s.cid = c.cid)
-> Custom Scan (MxVMotion) Redistribute Motion 4:4 (slice3; segments: 4) (cost=0.00..36.07 rows=2 width=16)
Hash Key: s.cid
-> Custom Scan (MxVScan) on sale s (cost=0.00..36.02 rows=2 width=16)
-> Materialize (cost=0.00..36.04 rows=2 width=10)
-> Custom Scan (MxVScan) on customer c (cost=0.00..36.02 rows=2 width=10)
Optimizer: Postgres query optimizer
(15 rows)
Этот план содержит шесть основных узлов: Materialize, Redistribute Motion, Nested Loop, Sort, GroupAggregate и Gather Motion. Каждый из них включает оценки стоимости, количества строк и ширины.
Диаграмма дерева плана запроса:
_1691547677.png)
Оптимизатор начинает с операции материализации. Сканирование и материализация таблицы customer занимают примерно 36. Поскольку таблица customer распределена по ключу cid, а таблица sale — по ключу (cid,date), таблица sale должна быть переопределена по ключу cid, чтобы связанные строки находились на одном и том же MXSegment для локального соединения. После переопределения применяется вложенный цикл соединения с фильтром s.cid = c.cid. На этом этапе общая стоимость достигает ~72, что указывает на значительное потребление ресурсов. Поскольку запрос требует группировки по cname, происходит второе переопределение, за которым следует сортировка по cname и агрегирование по группам. Наконец, результаты отправляются на узел Gather Motion. Узел Gather Motion вычисляет итоговую стоимость (72.32) и возвращает результаты на MXMaster.
Детали компонентов:
CREATE TABLE x AS SELECT... записывает напрямую в новую таблицу, а не отправляет на MXMaster.Расположение срезов для Segment 1 и Segment 2:
_1691548044.png)