В чем разница между LATERAL JOIN и подзапросом в PostgreSQL?

С тех пор, как в Postgres появилась возможность выполнять LATERAL соединения, я читал об этом, поскольку в настоящее время я делаю сложные дампы данных для своей команды с множеством неэффективных подзапросов, из-за которых общий запрос занимает четыре минуты или больше.

Я понимаю, что присоединения LATERAL могут мне помочь, но даже после прочтения таких статей, как эта из Heap Analytics, я все еще не совсем понимаю.

Каков вариант использования LATERAL соединения? В чем разница между LATERAL объединением и подзапросом?


person jdotjdot    schedule 16.02.2015    source источник
comment
blog.heapanalytics.com/ и объяснятьextended.com/2009/07/16/inner-join-vs-cross -apply (apply в SQL Server совпадает с lateral из стандарта SQL)   -  person a_horse_with_no_name    schedule 17.02.2015
comment
Ключевое слово LATERAL принадлежит следующей производной таблице (подзапросу), т.е. это не тип JOIN.   -  person jarlh    schedule 03.05.2021


Ответы (5)


Что такое LATERAL присоединение?

Эта функция была представлена ​​в PostgreSQL 9.3.
Цитирование руководство:

Подзапросам, появляющимся в FROM, может предшествовать ключевое слово LATERAL. Это позволяет им ссылаться на столбцы, предоставленные предшествующими FROM элементами. (Без LATERAL каждый подзапрос оценивается независимо и поэтому не может ссылаться на какой-либо другой элемент FROM.)

Табличным функциям, появляющимся в FROM, также может предшествовать ключевое слово LATERAL, но для функций ключевое слово является необязательным; аргументы функции в любом случае могут содержать ссылки на столбцы, предоставленные предшествующими FROM элементами.

Там приведены базовые примеры кода.

Больше похоже на коррелированный подзапрос

LATERAL join больше похож на коррелированный подзапрос, а не на простой подзапрос, в выражениях справа соединения LATERAL оцениваются один раз для каждой строки слева от него - точно так же, как коррелированный подзапрос - в то время как простой подзапрос (табличное выражение) оценивается только один раз. (У планировщика запросов есть способы оптимизировать производительность для любого из них.)
Связанный ответ с примерами кода для обеих сторон, решая ту же проблему:

Для возврата более чем одного столбца соединение LATERAL обычно проще, чище и быстрее.
Также помните, что эквивалентом коррелированного подзапроса является LEFT JOIN LATERAL ... ON true:

Что не может сделать подзапрос

Есть вещи, которые LATERAL может выполнять соединение, но (коррелированный) подзапрос не может (легко). Коррелированный подзапрос может возвращать только одно значение, а не несколько столбцов и не несколько строк - за исключением простых вызовов функций (которые умножают строки результатов, если они возвращают несколько строк). Но даже некоторые функции, возвращающие набор, разрешены только в предложении FROM. Как unnest() с несколькими параметрами в Postgres 9.4 или новее. Руководство:

Это разрешено только в предложении FROM;

Итак, это работает, но не может (легко) быть заменено подзапросом:

CREATE TABLE tbl (a1 int[], a2 int[]);
SELECT * FROM tbl, unnest(a1, a2) u(elem1, elem2);  -- implicit LATERAL

Запятая (,) в предложении FROM - это краткое обозначение для CROSS JOIN.
LATERAL автоматически предполагается для табличных функций.
Об особом случае UNNEST( array_expression [, ... ] ):

Функции, возвращающие множество, в списке SELECT

Вы также можете напрямую использовать функции возврата набора, такие как unnest() в списке SELECT. Раньше это проявляло удивительное поведение с более чем одной такой функцией в одном SELECT списке до Postgres 9.6. Но, наконец, он был очищен с помощью Postgres 10 и теперь является допустимой альтернативой (даже если это не стандартный SQL). Видеть:

Основываясь на приведенном выше примере:

SELECT *, unnest(a1) AS elem1, unnest(a2) AS elem2
FROM   tbl;

Сравнение:

dbfiddle для pg 9.6 здесь
dbfiddle для pg 10 здесь

Разъяснить дезинформацию

Руководство:

Для типов соединения INNER и OUTER необходимо указать условие соединения, а именно одно из NATURAL, ON join_condition или USING (join_column [, ...]). Значение см. Ниже.
Для CROSS JOIN ни один из этих пунктов не может появляться.

Итак, эти два запроса действительны (даже если они не особенно полезны):

SELECT *
FROM   tbl t
LEFT   JOIN LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t ON TRUE;

SELECT *
FROM   tbl t, LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t;

Пока этого нет:

SELECT *
FROM   tbl t
LEFT   JOIN LATERAL (SELECT * FROM b WHERE b.t_id = t.t_id) t;

Вот почему пример кода Andomar верен (CROSS JOIN не требует условия соединения) и Аттилы is не было.

person Erwin Brandstetter    schedule 17.02.2015
comment
Есть кое-что, что может сделать подзапрос, а LATERAL JOIN не может. Как оконные функции. Как здесь - person Evan Carroll; 08.02.2017
comment
@EvanCarroll: Я не нашел в ссылке коррелированных подзапросов. Но я добавил еще один ответ, чтобы продемонстрировать оконную функцию в подзапросе LATERAL: gis.stackexchange.com/a/230070/7244 - person Erwin Brandstetter; 27.02.2017
comment
Чище и быстрее? В некоторых случаях вроде бы величины быстрее. У меня был запрос, который менялся от нескольких дней до секунд после перехода на LATERAL. - person rovyko; 18.10.2018

Разница между соединением, отличным от lateral, и соединением lateral заключается в том, можете ли вы посмотреть на левую строку таблицы. Например:

select  *
from    table1 t1
cross join lateral
        (
        select  *
        from    t2
        where   t1.col1 = t2.col1 -- Only allowed because of lateral
        ) sub

Этот «внешний вид» означает, что подзапрос должен оцениваться более одного раза. В конце концов, t1.col1 может принимать множество значений.

Напротив, подзапрос после соединения, отличного от lateral, может быть оценен один раз:

select  *
from    table1 t1
cross join
        (
        select  *
        from    t2
        where   t2.col1 = 42 -- No reference to outer query
        ) sub

Как и требуется без lateral, внутренний запрос никоим образом не зависит от внешнего запроса. Запрос lateral является примером запроса correlated из-за его связи со строками вне самого запроса.

person Andomar    schedule 16.02.2015
comment
как select * from table1 left join t2 using (col1) сравнивается? Мне непонятно, когда соединения с использованием / по условию недостаточно, и было бы разумнее использовать латеральное. - person No_name; 05.07.2020

Во-первых, Боковое и поперечное применение - это одно и то же. Поэтому вы также можете прочитать о Cross Apply. Поскольку он был реализован в SQL Server на протяжении многих лет, вы найдете больше информации о нем, чем Lateral.

Во-вторых, по моему мнению, вы ничего не можете сделать, используя подзапрос вместо бокового. Но:

Рассмотрим следующий запрос.

Select A.*
, (Select B.Column1 from B where B.Fk1 = A.PK and Limit 1)
, (Select B.Column2 from B where B.Fk1 = A.PK and Limit 1)
FROM A 

Вы можете использовать боковой в этом состоянии.

Select A.*
, x.Column1
, x.Column2
FROM A LEFT JOIN LATERAL (
  Select B.Column1,B.Column2,B.Fk1 from B  Limit 1
) x ON X.Fk1 = A.PK

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

Есть больше вариантов использования для бокового или поперечного применения, но я нашел наиболее распространенный вариант.

person Atilla Ozgur    schedule 16.02.2015
comment
Точно, мне интересно, почему PostgreSQL использует lateral вместо apply. Может быть, Microsoft запатентовала синтаксис? - person Andomar; 17.02.2015
comment
@Andomar AFAIK lateral входит в стандарт SQL, а apply - нет. - person mu is too short; 17.02.2015
comment
LEFT JOIN требует условия соединения. Сделайте это ON TRUE, если вы не хотите как-то ограничивать. - person Erwin Brandstetter; 17.02.2015
comment
Эрвин прав, вы получите сообщение об ошибке, если не используете условие cross join или on - person Andomar; 17.02.2015
comment
@Andomar: Вдохновленный этой дезинформацией, я добавил еще один ответ, чтобы уточнить. - person Erwin Brandstetter; 17.02.2015
comment
Обновленный запрос снова неверен. Вы не можете присоединиться к X.Fk1, потому что это не результат подзапроса. Если вы включите Fk1 в подзапрос, он будет формально правильным, но все же будет демонстрировать экзотическое поведение. Он выбирает произвольную строку из B и присоединяет ее к A, только если Fk1 совпадает с A.PK. - person Erwin Brandstetter; 17.02.2015
comment
@ErwinBrandstetter Не могли бы вы проверить это еще раз? - person Atilla Ozgur; 18.02.2015
comment
and Limit 1? Это опечатка или допустимая конструкция, с которой я не знаком? - person isapir; 03.01.2018

Таблица базы данных

Имея следующую blog таблицу базы данных, в которой хранятся блоги, размещенные на нашей платформе:

  Таблица блога

И в настоящее время у нас есть два блога:

id created_on title url
1 2013-09-30 Vlad Mihalcea's Blog https://vladmihalcea.com
2 2017-01-22 Hypersistence https://hypersistence.io

Получение нашего отчета без использования SQL LATERAL JOIN

Нам нужно построить отчет, который извлекает из таблицы blog следующие данные:

  • идентификатор блога
  • возраст блога, в годах
  • дата следующей годовщины блога
  • количество дней, оставшихся до следующей годовщины.

Если вы используете PostgreSQL, вам необходимо выполнить следующий SQL-запрос:

SELECT
  b.id as blog_id,
  extract(
    YEAR FROM age(now(), b.created_on)
  ) AS age_in_years,
  date(
    created_on + (
      extract(YEAR FROM age(now(), b.created_on)) + 1
    ) * interval '1 year'
  ) AS next_anniversary,
  date(
    created_on + (
      extract(YEAR FROM age(now(), b.created_on)) + 1
    ) * interval '1 year'
  ) - date(now()) AS days_to_next_anniversary
FROM blog b
ORDER BY blog_id

Как видите, age_in_years нужно определять три раза, потому что он нужен вам при вычислении значений next_anniversary и days_to_next_anniversary.

И именно в этом нам может помочь LATERAL JOIN.

Получение отчета с помощью SQL LATERAL JOIN

Следующие системы реляционных баз данных поддерживают синтаксис LATERAL JOIN:

  • Oracle с 12c
  • PostgreSQL с 9.3
  • MySQL с 8.0.14

SQL Server может имитировать LATERAL JOIN, используя CROSS APPLY и OUTER APPLY.

LATERAL JOIN позволяет нам повторно использовать значение age_in_years и просто передавать его дальше при вычислении значений next_anniversary и days_to_next_anniversary.

Предыдущий запрос можно переписать для использования LATERAL JOIN следующим образом:

SELECT
  b.id as blog_id,
  age_in_years,
  date(
    created_on + (age_in_years + 1) * interval '1 year'
  ) AS next_anniversary,
  date(
    created_on + (age_in_years + 1) * interval '1 year'
  ) - date(now()) AS days_to_next_anniversary
FROM blog b
CROSS JOIN LATERAL (
  SELECT
    cast(
      extract(YEAR FROM age(now(), b.created_on)) AS int
    ) AS age_in_years
) AS t
ORDER BY blog_id

И значение age_in_years может быть вычислено и повторно использовано для вычислений next_anniversary и days_to_next_anniversary:

blog_id age_in_years next_anniversary days_to_next_anniversary
1 7 2021-09-30 295
2 3 2021-01-22 44

Намного лучше, правда?

age_in_years рассчитывается для каждой записи таблицы blog. Таким образом, он работает как коррелированный подзапрос, но записи подзапроса объединяются с основной таблицей, и по этой причине мы можем ссылаться на столбцы, созданные подзапросом.

person Vlad Mihalcea    schedule 22.01.2021

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

Например:

CREATE OR REPLACE FUNCTION delete_company(companyId varchar(255))
RETURNS void AS $$
    BEGIN
        DELETE FROM company_settings WHERE "company_id"=company_id;
        DELETE FROM users WHERE "company_id"=companyId;
        DELETE FROM companies WHERE id=companyId;
    END; 
$$ LANGUAGE plpgsql;

SELECT * FROM (
    SELECT id, name, created_at FROM companies WHERE created_at < '2018-01-01'
) c, LATERAL delete_company(c.id);

Это единственный способ, которым я знаю, как делать подобные вещи в PostgreSQL.

person Theodore R. Smith    schedule 05.10.2018