7.2. Обработка запросов

Shardman использует стандартный конвейер выполнения запросов PostgreSQL. Доступ к другим узлам в кластере осуществляется через модифицированное расширение postgres_fdw.

Планировщик запросов Shardman использует абстрактное синтаксическое дерево запроса (АСТ) и создаёт план запроса, который используется исполнителем. Оценивая методы выполнения запросов, планировщик оперирует так называемыми путями, которые указывают, как следует обращаться к отношениям. При обработке дерева соединения запроса планировщик рассматривает различные комбинации того, как отношения могут быть соединены. Каждый раз он проверяет соединение двух отношений, одно из которых само может быть отношением соединения. После выбора порядка и стратегий соединения отношений планировщик рассматривает операции группировки, упорядочения и ограничения. Когда выбран самый дешёвый путь, он преобразуется в план запроса. План состоит из дерева узлов, у каждого из которых есть методы для получения одной следующей строки результата (или NULL, если результатов больше нет).

7.2.1. Техника выталкивания

7.2.1.1. Соединения

Эффективность выполнения запросов в распределённой СУБД определяется тем, сколько операций можно выполнить на узлах, содержащих фактические данные. В Shardman много усилий посвящено тому, чтобы выталкивать операции соединения. Когда планировщик находит отношение, доступное через обёртку сторонних данных (FDW), он создаёт ForeignPath для доступа к нему. Позже, когда он контролирует соединение двух отношений, и оба они доступны через ForeignPath с одного и того же стороннего сервера, он может рассматривать возможность передачи этого соединения на сервер и создания так называемого ForeignJoinPath. Планировщик не может этого сделать, если тип соединения не поддерживается, если фильтры, прикреплённые к отношению, должны применяться локально или если результат сканирования отношения содержит поля, которые невозможно оценить на удалённом сервере. Примером неподдерживаемого в настоящее время типа соединения является антисоединение. Локальные фильтры, прикреплённые к отношению, следует применять локально, если удалённое выполнение может привести к другому результату или если модуль postgres_fdw не может создать SQL-выражения для применения некоторых фильтров. Примером полей, которые нельзя оценить на удалённом сервере, являются атрибуты внутреннего отношения полусоединения, недоступные через внешнее отношение. Если параметр конфигурации foreign_join_fast_path включён (это значение по умолчанию), планировщик Shardman прекращает поиск других стратегий соединения двух отношения, как только он находит возможным для них стороннее соединение. Если параметр конфигурации postgres_fdw.enforce_foreign_join включён (что также является значением по умолчанию), стоимость стороннего соединения оценивается таким образом, чтобы всегда быть меньше стоимости локального соединения.

Когда несколько сегментированных таблиц соединяются по ключу сегментирования, возможно посекционное соединение. Это означает, что вместо соединения исходных таблиц можно соединить их соответствующие секции. В настоящее время посекционное соединение применяется только в том случае, если условия соединения включают все ключи секций, которые должны иметь один и тот же тип данных и иметь точно совпадающие наборы дочерних секций. Посекционное соединение имеет решающее значение для эффективного выполнения запросов, поскольку оно позволяет выталкивать соединения секций таблицы. Очевидно, что для соединения нескольких секций эти секции должны находиться на одном узле. Обычно это происходит, когда сегментированные таблицы создаются с одним и тем же параметром num_parts. Однако, чтобы в процессе перебалансировки соответствующие секции были перемещены на одни и те же узлы, сегментированные таблицы должны быть при создании помечены как совмещённые (см. Раздел 7.1.1). Посекционное соединение включается параметром enable_partitionwise_join, который в Shardman по умолчанию включён.

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

7.2.1.2. Агрегирования

После планирования соединений планировщик рассматривает пути для операций после соединения, таких как агрегирование, ограничение, сортировка и группировка. Не все такие операции достигают логики выталкивания FDW. Например, в настоящее время секционирование эффективно предотвращает выталкивание предложений LIMIT. Существует две эффективные стратегии выполнения агрегатных функций на удалённых узлах. Первый — это агрегирование по секциям, когда предложение GROUP BY включает ключ секционирования, агрегатная функция может быть вытолкнуто вместе с предложением GROUP BY (это поведение контролируется параметром enable_partitionwise_aggregate, который по умолчанию включён в Shardman). В качестве альтернативы планировщик может решить выполнять частичное агрегирование для каждой секции сегментированной таблицы, а затем соединить результаты. В Shardman такое частичное агрегирование может быть пропущено, если оно соответствует основному агрегированию. Например, неполная функция sum() всегда может быть вытолкнута, а avg() — нет. Также планировщик отказывается выталкивать частичные агрегирования наружу, если они содержат дополнительные предложения, такие как ORDER BY или DISTINCT, или если оператор содержит предложение HAVING.

7.2.1.3. Подзапросы

Как правило, подзапросы нельзя передавать на другие узлы кластера. Однако Shardman использует два подхода для снятия этого ограничения.

Первый — это распаковка подзапросов. В PostgreSQL несвязанные подзапросы могут быть преобразованы в полусоединения. В следующем примере подзапрос ANY к несекционированным таблицам преобразуется в Hash Semi Join:

EXPLAIN (COSTS OFF) SELECT * FROM pgbench_branches WHERE bid = ANY (SELECT bid FROM pgbench_tellers);
                        QUERY PLAN                         
-----------------------------------------------------------
 Hash Semi Join
   Hash Cond: (pgbench_branches.bid = pgbench_tellers.bid)
   ->  Seq Scan on pgbench_branches
   ->  Hash
         ->  Seq Scan on pgbench_tellers

Когда параметр optimize_correlated_subqueries включён (по умолчанию), планировщик Shardman также пытается преобразовывать связанные подзапросы (т. е. подзапросы, которые ссылаются на отношения верхнего уровня) в полусоединения. Эта оптимизация работает для операторов IN и =. Преобразование имеет некоторые ограничения. Например, оно не выполняется, если подзапрос содержит агрегатные функции или ссылается на отношения верхнего уровня вне предложения WHERE. Эта оптимизация позволяет преобразовывать более сложные подзапросы в полусоединения, как в следующем примере:

EXPLAIN (COSTS OFF) SELECT * FROM pgbench_branches WHERE bid = ANY (SELECT bid FROM pgbench_tellers WHERE tbalance = bbalance);
                                                       QUERY PLAN                                                       
------------------------------------------------------------------------------------------------------------------------
 Hash Semi Join
   Hash Cond: ((pgbench_branches.bid = pgbench_tellers.bid) AND (pgbench_branches.bbalance = pgbench_tellers.tbalance))
   ->  Seq Scan on pgbench_branches
   ->  Hash
         ->  Seq Scan on pgbench_tellers
(5 rows)

После применения распаковки подзапросов полусоединение можно вынести для выполнения на удалённый узел.

Второй подход заключается в выталкивании всего подзапроса. Это возможно, когда оптимизатор уже выяснил, что подзапрос ссылается только на секции того же стороннего сервера, что и запрос верхнего уровня, и соответствующие сторонние сканирования не имеют локальных условий. Оптимизация контролируется параметром postgres_fdw.subplan_pushdown (по умолчанию отключено). Когда postgres_fdw принимает решение отправить подзапрос, он должен восстановить этот подзапрос. Подзапрос, содержащий узлы плана, для которых не реализовано восстановление, не будет вытолкнут. Пример выталкивания подзапроса выглядит следующим образом:

EXPLAIN (VERBOSE ON, COSTS OFF)
SELECT * FROM pgbench_accounts a WHERE a.bid=90 AND abalance =
    (SELECT min(tbalance) FROM pgbench_tellers t WHERE t.bid=90 and a.bid=t.bid);
                                     QUERY PLAN                                                                                                                   
--------------------------------------------------------------------------------------
 Foreign Scan on public.pgbench_accounts_5_fdw a
   Output: a.aid, a.bid, a.abalance, a.filler
   Remote SQL: SELECT aid, bid, abalance, filler FROM public.pgbench_accounts_5 r2 WHERE ((r2.bid = 90)) AND ((r2.abalance = ((SELECT min(sp0_2.tbalance) FROM public.pgbench_tellers_5 sp0_2 WHERE ((sp0_2.bid = 90)) AND ((r2.bid = 90))))))
   Transport: Silk
   SubPlan 1
     ->  Finalize Aggregate
           Output: min(t.tbalance)
           ->  Foreign Scan
                 Output: (PARTIAL min(t.tbalance))
                 Relations: Aggregate on (public.pgbench_tellers_5_fdw t)
                 Remote SQL: SELECT min(tbalance) FROM public.pgbench_tellers_5 WHERE ((bid = 90)) AND (($1::integer = 90))
                 Transport: Silk

Обратите внимание, что в приведённом выше плане нет ссылок на SubPlan 1.

7.2.2. Асинхронное выполнение

При обращении к сегментированной таблице планировщик Shardman создаёт планы Append для сканирования всех секций таблицы и объединения результатов. Когда некоторые из секций являются сторонними таблицами, планировщик может решить использовать асинхронное выполнение. Это означает, что когда у узла Append впервые после инициализации запрашиваются кортежи, он посылает асинхронным дочерним узлам команду начать считывание результата. Для узлов postgres_fdw async ForeignScan означает, что объявляется удалённый курсор и на удалённый сервер отправляется запрос на выборку. Если используется транспорт Silk, то запрос отправляется для выполнения на удалённый сервер в виде сообщения MT_SPI.

После отправки запроса на удалённые серверы Append возвращается к выборке данных из синхронных дочерних узлов — локальных узлов сканирования или синхронных узлов ForeignScan. Данные из таких узлов извлекаются блокирующим образом. Когда Append завершает получение данных от синхронных узлов, он проверяет, есть ли данные на асинхронных узлах. Если данных нет, он ожидает результатов от асинхронных узлов.

Shardman может выполнять несколько типов планов асинхронно. Это асинхронные ForeignScans, проекции и тривиальные сканирования подзапросов (select * from subquery) по асинхронным планам.

Асинхронное выполнение включено по умолчанию на уровне стороннего сервера. Оно контролируется параметром async_capable расширения postgres_fdw. На данный момент только планы Append поддерживают асинхронное выполнение, а MergeAppend — нет.

Изучая планы запросов, обратите внимание на наличие в плане неасинхронных узлов ForeignScan. Асинхронное выполнение может значительно увеличивать время выполнения запроса.

Примеры:

EXPLAIN (COSTS OFF) SELECT * FROM pgbench_accounts;
                               QUERY PLAN                                
-------------------------------------------------------------------------
 Append
   ->  Seq Scan on pgbench_accounts_0 pgbench_accounts_1
   ->  Async Foreign Scan on pgbench_accounts_1_fdw pgbench_accounts_2
   ->  Async Foreign Scan on pgbench_accounts_2_fdw pgbench_accounts_3
   ->  Seq Scan on pgbench_accounts_3 pgbench_accounts_4
   ->  Async Foreign Scan on pgbench_accounts_4_fdw pgbench_accounts_5
   ->  Async Foreign Scan on pgbench_accounts_5_fdw pgbench_accounts_6
   ->  Seq Scan on pgbench_accounts_6 pgbench_accounts_7
   ->  Async Foreign Scan on pgbench_accounts_7_fdw pgbench_accounts_8
   ->  Async Foreign Scan on pgbench_accounts_8_fdw pgbench_accounts_9
   ->  Seq Scan on pgbench_accounts_9 pgbench_accounts_10
   ->  Async Foreign Scan on pgbench_accounts_10_fdw pgbench_accounts_11
   ->  Async Foreign Scan on pgbench_accounts_11_fdw pgbench_accounts_12
   ->  Seq Scan on pgbench_accounts_12 pgbench_accounts_13
   ->  Async Foreign Scan on pgbench_accounts_13_fdw pgbench_accounts_14
   ->  Async Foreign Scan on pgbench_accounts_14_fdw pgbench_accounts_15
   ->  Seq Scan on pgbench_accounts_15 pgbench_accounts_16
   ->  Async Foreign Scan on pgbench_accounts_16_fdw pgbench_accounts_17
   ->  Async Foreign Scan on pgbench_accounts_17_fdw pgbench_accounts_18
   ->  Seq Scan on pgbench_accounts_18 pgbench_accounts_19
   ->  Async Foreign Scan on pgbench_accounts_19_fdw pgbench_accounts_20

Здесь показан типичный асинхронный план. Существуют асинхронные сторонние сканирования и локальные последовательные сканирования, которые выполняются синхронно.

EXPLAIN (COSTS OFF) SELECT * FROM pgbench_accounts ORDER BY aid;
                            QUERY PLAN                             
-------------------------------------------------------------------
 Merge Append
   Sort Key: pgbench_accounts.aid
   ->  Sort
         Sort Key: pgbench_accounts_1.aid
         ->  Seq Scan on pgbench_accounts_0 pgbench_accounts_1
   ->  Foreign Scan on pgbench_accounts_1_fdw pgbench_accounts_2
   ->  Foreign Scan on pgbench_accounts_2_fdw pgbench_accounts_3
   ->  Sort
         Sort Key: pgbench_accounts_4.aid
         ->  Seq Scan on pgbench_accounts_3 pgbench_accounts_4
   ->  Foreign Scan on pgbench_accounts_4_fdw pgbench_accounts_5
   ->  Foreign Scan on pgbench_accounts_5_fdw pgbench_accounts_6
   ->  Sort
         Sort Key: pgbench_accounts_7.aid
         ->  Seq Scan on pgbench_accounts_6 pgbench_accounts_7
   ->  Foreign Scan on pgbench_accounts_7_fdw pgbench_accounts_8
   ->  Foreign Scan on pgbench_accounts_8_fdw pgbench_accounts_9
   ->  Sort
         Sort Key: pgbench_accounts_10.aid
         ->  Seq Scan on pgbench_accounts_9 pgbench_accounts_10
   ->  Foreign Scan on pgbench_accounts_10_fdw pgbench_accounts_11
   ->  Foreign Scan on pgbench_accounts_11_fdw pgbench_accounts_12
   ->  Sort
         Sort Key: pgbench_accounts_13.aid
         ->  Seq Scan on pgbench_accounts_12 pgbench_accounts_13
   ->  Foreign Scan on pgbench_accounts_13_fdw pgbench_accounts_14
   ->  Foreign Scan on pgbench_accounts_14_fdw pgbench_accounts_15
   ->  Sort
         Sort Key: pgbench_accounts_16.aid
         ->  Seq Scan on pgbench_accounts_15 pgbench_accounts_16
   ->  Foreign Scan on pgbench_accounts_16_fdw pgbench_accounts_17
   ->  Foreign Scan on pgbench_accounts_17_fdw pgbench_accounts_18
   ->  Sort
         Sort Key: pgbench_accounts_19.aid
         ->  Seq Scan on pgbench_accounts_18 pgbench_accounts_19
   ->  Foreign Scan on pgbench_accounts_19_fdw pgbench_accounts_20

Здесь используется merge append, поэтому выполнение не может быть асинхронным.

7.2.3. Альтернатива выборке всех строк

Во многих случаях операции над данными невозможно выполнить удалённо (например, если в фильтрах используется какая-то функция переменной среды, когда несколько сегментированных таблиц объединяются по атрибуту, не являющемуся ключом сегментирования, и выталкивание определённого типа соединения не поддерживается) или когда планировщик считает локальное выполнение более дешёвым. В таких случаях различные операции (выборка, объединение или агрегирование) не выталкиваются вниз, а выполняются локально. Это может привести к неэффективному выполнению запросов из-за большого трафика между кластерами и высокой стоимости обработки на координаторе. Когда такое случается, нужно проверить, есть ли у оптимизатора свежая статистика, подумать о том, чтобы переписать запрос, чтобы извлечь выгоду из различных форм выталкивания, или, по крайней мере, проверить, что предложенный план запроса является подходящим. Чтобы включить в СУБД анализ данных для всего кластера, можно использовать функцию shardman.global_analyze.