Потеря производительности из-за бокового соединения в запросе Postgres

Я имею дело с запросом PostgreSQL (v14) такого типа.

SELECT
    ...,
    EXISTS (
        SELECT 1
        FROM table2 t2
        WHERE t2.fk = t1.id
          AND LOWER(t2.table_name) = 't1'
    ) AS t2_record_exists
FROM table1 t1;

и надеялся извлечь логику из функции, которая будет использоваться в боковом соединении, чтобы вычислить это поле более читабельным способом (так как оно будет использоваться в представлении).

Это результирующий запрос

SELECT
    ...,
    t2_record_exists.t2_record_exists
FROM table1 t1
    LEFT JOIN LATERAL does_t2_record_exist(t1.id, 't1') t2_record_exists(t2_record_exists) ON TRUE;

Это использует эту функцию

CREATE OR REPLACE FUNCTION does_t2_record_exist(object_id int8, _table_name text)
    RETURNS bool
    LANGUAGE plpgsql
AS $function$
BEGIN
    RETURN EXISTS (
        SELECT 1
        FROM table2 t2
        WHERE t2.fk = object_id
            AND LOWER(t2.table_name) = _table_name
    );
END $function$;

Второй запрос испытывает серьезную потерю производительности, поскольку он выполняется примерно за 6000 мс, а первый — за 300 мс.

Я не знаю, почему это произошло, поскольку я наивно предполагал, что одна и та же операция (подзапрос EXISTS) будет выполняться одинаковое количество раз (один раз для каждой строки).

Что здесь не так? Как можно заранее предвидеть такие проблемы с производительностью?

Обновлено: Вот планы запросов (полученные с помощью EXPLAIN ANALYZE) для моего конкретного случая.

Запрос 1 (с подзапросом):

Index Only Scan using cos_table1 on table1 t1 (cost=0.43..3723311.61 rows=1481535 width=9) (actual time=56.299..247.674 rows=1477585 loops=1)
  Heap Fetches: 46760
  SubPlan 2
    ->  Seq Scan on table2 t2  (cost=0.00..15.27 rows=2 width=8) (actual time=11.300..11.454 rows=372 loops=1)
          Filter: (lower(table_name) = 't1'::text)
          Rows Removed by Filter: 113
Planning Time: 0.085 ms
JIT:
  Functions: 13
  Options: Inlining true, Optimization true, Expressions true, Deforming true
  Timing: Generation 0.769 ms, Inlining 17.544 ms, Optimization 24.557 ms, Emission 13.652 ms, Total 56.524 ms
Execution Time: 276.285 ms

Запрос 2 (с LATERAL JOIN):

Nested Loop Left Join  (cost=0.68..56512.74 rows=1481535 width=9) (actual time=0.039..5978.865 rows=1477585 loops=1)
  ->  Index Only Scan using cos_table1 on table1 t1 (cost=0.43..26881.79 rows=1481535 width=8) (actual time=0.011..179.682 rows=1477585 loops=1)
        Heap Fetches: 46760
  ->  Function Scan on does_t2_record_exist t2_record_exist (cost=0.25..0.26 rows=1 width=1) (actual time=0.004..0.004 rows=1 loops=1477585)
Planning Time: 0.065 ms
Execution Time: 6024.267 ms

Инструкции по вопросам производительности Postgres можно найти здесь: stackoverflow.com/tags/postgresql- Performance/info

Erwin Brandstetter 28.06.2024 15:43

Если оно все равно будет заключено в представление, почему оно должно быть более читабельным? (Если это действительно так, я бы сказал, что это менее читабельно, хотя и короче).

jjanes 28.06.2024 17:28

@jjanes Можно утверждать, что наличие одной строки для каждого поля дает более красивый запрос представления, где сложные для вычисления поля не слишком отвлекают. Я также не сообщил, что этот подзапрос следует добавлять в несколько представлений, чтобы избежать повторений. Я уверен, что есть хорошие контраргументы, но тема вопроса как раз в понимании несоответствия производительности

Luca Blanchi 28.06.2024 18:10
ReactJs | Supabase | Добавление данных в базу данных
ReactJs | Supabase | Добавление данных в базу данных
Это и есть ваш редактор таблиц в supabase.👇
Понимание Python и переход к SQL
Понимание Python и переход к SQL
Перед нами лабораторная работа по BloodOath:
0
3
94
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Когда операция EXISTS находится непосредственно в запросе, PostgreSQL может преобразовать подзапрос в семантически эквивалентный JOIN; однако, когда операция EXISTS скрыта за вызовом функции, механизм запросов больше не имеет информации, необходимой для выполнения такой оптимизации. Выполнение EXPLAIN (ANALYZE, BUFFERS, VERBOSE) для каждого запроса должно прояснить причину разницы в производительности: включение EXISTS в функцию приводит к тому, что подзапрос выполняется для каждой строки вместо использования более эффективного JOIN.

Ответ принят как подходящий

Вы не показали фактические планы запросов (вывод EXPLAIN (ANALYZE, BUFFERS, SETTINGS)), но в вашей функции есть несколько ловушек производительности. Перепишите как:

CREATE OR REPLACE FUNCTION does_t2_record_exist(_object_id int8, _table_name text)
  RETURNS bool
  LANGUAGE sql STABLE PARALLEL SAFE AS  -- !
$func$
SELECT EXISTS (
   SELECT FROM table2 t2
   WHERE  t2.fk = _object_id
   AND    lower(t2.table_name) = _table_name
   );
$func$;

И ваш запрос тогда просто:

SELECT ...
     , does_t2_record_exist(t1.id, 't1') AS t2_record_exists
FROM   table1 t1

Соединение LATERAL не требуется, как только вы поместили коррелированный подзапрос в функцию.

Выражение lower(t2.table_name) не является жаргонным . Обязательно наличие многостолбцового B-дерева (по умолчанию) и индекса выражения на table2(fk, lower(tablename)). (Или только в одном из обоих индексных полей, если оно само по себе достаточно избирательно.)

Для простой функции вам не нужен PL/pgSQL. Переключение на LANGUAGE sql возможно позволит встроить функцию.

Добавьте метки функций PARALLEL SAFE и STABLE (потому что они оба действительно применимы!), чтобы обеспечить различные оптимизации производительности.

Тем не менее, во всяком случае, функция может быть медленнее, чем помещение выражения EXISTS с коррелированным подзапросом непосредственно в список SELECT. Но очень немного, если все делать правильно.

Тем не менее, соединение LATERAL или ваша функция, выполняющая коррелированный подзапрос, должна выполняться один раз для каждой соответствующей строки запроса. Если в результате будет запрошено большинство строк, которые пришлось бы обрабатывать простому подзапросу, соединение (не LATERAL) по-прежнему будет самым быстрым вариантом. (Или исходный запрос, который может привести к тому же плану запроса.) Например:

SELECT ...
     , t2.fk IS NOT NULL AS t2_record_exists
FROM   table1 t1
LEFT   JOIN (
   SELECT DISTINCT t2.fk  -- do we even need DISTINCT?
   FROM   table2 t2
   WHERE  lower(t2.table_name) = 't1'
   ) t2 ON t2.fk = t1.id

Видеть:

Добавленные планы запросов показывают...

В вашей таблице table2 всего 372 строки, 113 из которых фильтруются. Это крошечный. Вам не понадобится индекс! Postgres делает это с помощью последовательного сканирования исходного запроса одним махом.
Заключение функциональности в «черный ящик» функции PL/pgSQL заставило Postgres выполнять ее loops=1477585 раз. Плохо.

Моя переписанная версия может это исправить. Или вернитесь к исходному запросу. (Или мой добавленный альтернативный вариант.)

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

(боковые соединения также могут выступать в качестве барьеров оптимизации, но не в данном конкретном случае)

Если представление обычно используется с очень ограничительным предложением WHERE, которое будет помещено в таблицу table1, это может оказаться преимуществом, поскольку массовая операция может быть неэффективной, когда потребуется всего несколько строк.

Вы можете использовать какую-то систему макросов для упрощения повторяющегося кода. PostgreSQL не предоставляет такого, это будет зависеть от вашего репозитория кода, инфраструктуры или того, что вы используете для управления своим кодом.

Другие вопросы по теме