WITH
предоставляет способ написания вспомогательных операторов для использования в более крупном запросе. Эти операторы, которые часто называют общими табличными выражениями или CTE , можно рассматривать как определяющие временные таблицы, которые существуют только для одного запроса. Каждое вспомогательное выражение в WITH
предложении может быть SELECT
, INSERT
, UPDATE
или DELETE
; а само WITH
предложение присоединено к первичному утверждению, которое также может быть SELECT
, INSERT
, UPDATE
или DELETE
.
SELECT
вWITH
Основное значение SELECT
in WITH
заключается в том, чтобы разбивать сложные запросы на более простые части. Пример:
WITH regional_sales AS (
SELECT region, SUM(amount) AS total_sales
FROM orders
GROUP BY region
), top_regions AS (
SELECT region
FROM regional_sales
WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)
)
SELECT region,
product,
SUM(quantity) AS product_units,
SUM(amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product;
который отображает итоги продаж по каждому продукту только в самых популярных регионах продаж. Предложение WITH
определяет два вспомогательных оператора с именами regional_sales
и top_regions
, где выходные данные regional_sales
используются в top_regions
, а выходные данные top_regions
используются в основном SELECT
запросе. Этот пример можно было бы написать без WITH
, но нам потребовались бы два уровня вложенных подпрограмм SELECT
. Это немного легче следовать этому пути.
Рекурсивные запросы
Необязательный RECURSIVE
модификатор превращается WITH
из простого синтаксического удобства в функцию, которая выполняет то, что иначе невозможно в стандартном SQL. Используя RECURSIVE
, WITH
запрос может ссылаться на свой собственный вывод. Очень простым примером является этот запрос для суммирования целых чисел от 1 до 100:
WITH RECURSIVE t(n) AS (
VALUES (1)
UNION ALL
SELECT n+1 FROM t WHERE n < 100
)
SELECT sum(n) FROM t;
Общая форма рекурсивного WITH
запроса всегда представляет собой нерекурсивный терм , затем UNION
(или UNION ALL
), затем рекурсивный терм , где только рекурсивный терм может содержать ссылку на собственные выходные данные запроса. Такой запрос выполняется следующим образом:
Рекурсивная оценка запроса
1) Оцените нерекурсивный термин. Для UNION
(но не UNION ALL
) отбрасывать повторяющиеся строки. Включить все оставшиеся строки в результат рекурсивного запроса, а также поместить их во временную рабочую таблицу .
2) Пока рабочий стол не пуст, повторите эти шаги:
- Вычислите рекурсивный терм, заменив рекурсивную ссылку на себя текущим содержимым рабочей таблицы. Для
UNION
(но неUNION ALL
) отбросить повторяющиеся строки и строки, которые дублируют любую предыдущую строку результата. Включить все оставшиеся строки в результат рекурсивного запроса, а также поместить их во временную промежуточную таблицу . - Замените содержимое рабочей таблицы содержимым промежуточной таблицы, затем очистите промежуточную таблицу.
В приведенном выше примере рабочая таблица имеет только одну строку на каждом шаге и принимает значения от 1 до 100 на последовательных шагах. На 100-м шаге нет вывода из-за WHERE
предложения, поэтому запрос завершается.
Рекурсивные запросы обычно используются для работы с иерархическими или древовидными данными. Полезным примером является этот запрос, чтобы найти все прямые и косвенные части продукта, учитывая только таблицу, которая показывает непосредственные включения:
WITH RECURSIVE included_parts(sub_part, part, quantity) AS (
SELECT sub_part, part, quantity FROM parts WHERE part = 'our_product'
UNION ALL
SELECT p.sub_part, p.part, p.quantity
FROM included_parts pr, parts p
WHERE p.part = pr.sub_part
)
SELECT sub_part, SUM(quantity) as total_quantity
FROM included_parts
GROUP BY sub_part
Порядок поиска
При вычислении обхода дерева с помощью рекурсивного запроса может потребоваться упорядочить результаты либо в глубину, либо в ширину. Это можно сделать, вычислив столбец упорядочивания вместе с другими столбцами данных и используя его для сортировки результатов в конце. Обратите внимание, что на самом деле это не влияет на то, в каком порядке оценка запроса посещает строки; это, как всегда, зависит от реализации SQL. Этот подход просто обеспечивает удобный способ впоследствии упорядочить результаты.
Чтобы создать порядок в глубину, мы вычисляем для каждой строки результата массив строк, которые мы уже посетили. Например, рассмотрим следующий запрос, выполняющий поиск в таблице tree
с использованием link
поля:
WITH RECURSIVE search_tree(id, link, data) AS (
SELECT t.id, t.link, t.data
FROM tree t
UNION ALL
SELECT t.id, t.link, t.data
FROM tree t, search_tree st
WHERE t.id = st.link
)
SELECT * FROM search_tree;
Чтобы добавить информацию об упорядочении в глубину, вы можете написать следующее:
WITH RECURSIVE search_tree(id, link, data, path) AS (
SELECT t.id, t.link, t.data, ARRAY[t.id]
FROM tree t
UNION ALL
SELECT t.id, t.link, t.data, path || t.id
FROM tree t, search_tree st
WHERE t.id = st.link
)
SELECT * FROM search_tree ORDER BY path;
В общем случае, когда для идентификации строки необходимо использовать более одного поля, используйте массив строк. Например, если нам нужно отслеживать поля f1
и f2
:
WITH RECURSIVE search_tree(id, link, data, path) AS (
SELECT t.id, t.link, t.data, ARRAY[ROW(t.f1, t.f2)]
FROM tree t
UNION ALL
SELECT t.id, t.link, t.data, path || ROW(t.f1, t.f2)
FROM tree t, search_tree st
WHERE t.id = st.link
)
SELECT * FROM search_tree ORDER BY path;
Чтобы создать порядок в ширину, вы можете добавить столбец, который отслеживает глубину поиска, например:
WITH RECURSIVE search_tree(id, link, data, depth) AS (
SELECT t.id, t.link, t.data, 0
FROM tree t
UNION ALL
SELECT t.id, t.link, t.data, depth + 1
FROM tree t, search_tree st
WHERE t.id = st.link
)
SELECT * FROM search_tree ORDER BY depth;
Чтобы получить стабильную сортировку, добавьте столбцы данных в качестве вторичных столбцов сортировки.
Имеется встроенный синтаксис для вычисления столбца сортировки в глубину или в ширину. Например:
WITH RECURSIVE search_tree(id, link, data) AS (
SELECT t.id, t.link, t.data
FROM tree t
UNION ALL
SELECT t.id, t.link, t.data
FROM tree t, search_tree st
WHERE t.id = st.link
) SEARCH DEPTH FIRST BY id SET ordercol
SELECT * FROM search_tree ORDER BY ordercol;
WITH RECURSIVE search_tree(id, link, data) AS (
SELECT t.id, t.link, t.data
FROM tree t
UNION ALL
SELECT t.id, t.link, t.data
FROM tree t, search_tree st
WHERE t.id = st.link
) SEARCH BREADTH FIRST BY id SET ordercol
SELECT * FROM search_tree ORDER BY ordercol;
Этот синтаксис внутренне расширен до чего-то похожего на вышеупомянутые рукописные формы. Предложение SEARCH
указывает, требуется ли поиск в глубину или в ширину, список столбцов для отслеживания для сортировки и имя столбца, который будет содержать данные результатов, которые можно использовать для сортировки. Этот столбец будет неявно добавлен к выходным строкам CTE.
Обнаружение Цикла
При работе с рекурсивными запросами важно быть уверенным, что рекурсивная часть запроса в конечном итоге не вернет кортежи, иначе запрос будет бесконечно зацикливаться. Иногда с помощью UNION
вместо этого UNION ALL
можно добиться этого, отбрасывая строки, которые дублируют предыдущие выходные строки. Однако часто в цикле не используются полностью повторяющиеся строки вывода: может потребоваться проверка только одного или нескольких полей, чтобы увидеть, была ли достигнута та же самая точка ранее. Стандартным методом обработки таких ситуаций является вычисление массива уже посещенных значений. Например, снова рассмотрим следующий запрос, выполняющий поиск в таблице graph
с использованием link
поля:
WITH RECURSIVE search_graph(id, link, data, depth) AS (
SELECT g.id, g.link, g.data, 0
FROM graph g
UNION ALL
SELECT g.id, g.link, g.data, sg.depth + 1
FROM graph g, search_graph sg
WHERE g.id = sg.link
)
SELECT * FROM search_graph;
Этот запрос зациклится, если link
отношения содержат циклы. Поскольку нам требуется вывод « deep » , простое изменение UNION ALL
на UNION
не устранит зацикливание. Вместо этого нам нужно распознать, достигли ли мы снова той же строки, следуя определенному пути ссылок. Мы добавляем два столбца is_cycle
и path
в запрос, подверженный циклам:
WITH RECURSIVE search_graph(id, link, data, depth, is_cycle, path) AS (
SELECT g.id, g.link, g.data, 0,
false,
ARRAY[g.id]
FROM graph g
UNION ALL
SELECT g.id, g.link, g.data, sg.depth + 1,
g.id = ANY(path),
path || g.id
FROM graph g, search_graph sg
WHERE g.id = sg.link AND NOT is_cycle
)
SELECT * FROM search_graph;
Помимо предотвращения циклов, значение массива часто полезно само по себе, поскольку представляет « путь » , по которому можно добраться до любой конкретной строки.
В общем случае, когда для распознавания цикла необходимо проверить более одного поля, используйте массив строк. Например, если нам нужно сравнить поля f1
и f2
:
WITH RECURSIVE search_graph(id, link, data, depth, is_cycle, path) AS (
SELECT g.id, g.link, g.data, 0,
false,
ARRAY[ROW(g.f1, g.f2)]
FROM graph g
UNION ALL
SELECT g.id, g.link, g.data, sg.depth + 1,
ROW(g.f1, g.f2) = ANY(path),
path || ROW(g.f1, g.f2)
FROM graph g, search_graph sg
WHERE g.id = sg.link AND NOT is_cycle
)
SELECT * FROM search_graph;
Имеется встроенный синтаксис для упрощения обнаружения циклов. Приведенный выше запрос также можно записать так:
WITH RECURSIVE search_graph(id, link, data, depth) AS (
SELECT g.id, g.link, g.data, 1
FROM graph g
UNION ALL
SELECT g.id, g.link, g.data, sg.depth + 1
FROM graph g, search_graph sg
WHERE g.id = sg.link
) CYCLE id SET is_cycle USING path
SELECT * FROM search_graph;
и он будет внутренне переписан в вышеуказанную форму. Предложение CYCLE
указывает сначала список столбцов, которые необходимо отслеживать для обнаружения циклов, затем имя столбца, которое покажет, был ли обнаружен цикл, и, наконец, имя другого столбца, который будет отслеживать путь. Столбцы цикла и пути будут неявно добавлены к выходным строкам CTE.
Полезный трюк для тестирования запросов, когда вы не уверены, могут ли они зацикливаться, — это поместить a LIMIT
в родительский запрос. Например, этот запрос будет бесконечно зацикливаться без LIMIT
:
WITH RECURSIVE t(n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM t
)
SELECT n FROM t LIMIT 100;
Это работает, потому что реализация PostgreSQL оценивает только столько строк WITH
запроса, сколько фактически получено родительским запросом. Использование этого трюка в продакшене не рекомендуется, потому что другие системы могут работать по-другому. Кроме того, обычно это не сработает, если вы заставите внешний запрос сортировать результаты рекурсивного запроса или присоединять их к какой-либо другой таблице, потому что в таких случаях внешний запрос обычно будет пытаться получить все WITH
выходные данные запроса.
Материализация общего табличного выражения
Полезным свойством WITH
запросов является то, что они обычно оцениваются только один раз при выполнении родительского запроса, даже если родительский запрос или одноуровневые WITH
запросы ссылаются на них более одного раза. Таким образом, дорогостоящие вычисления, которые необходимы в нескольких местах, могут быть помещены в WITH
запрос, чтобы избежать избыточной работы. Другое возможное применение — предотвращение нежелательных множественных вычислений функций с побочными эффектами. Однако другая сторона этой медали заключается в том, что оптимизатор не может перенести ограничения из родительского запроса в WITH
запрос с множественными ссылками, поскольку это может повлиять на все виды использования WITH
выходных данных запроса, хотя должно влиять только на одно. Множественные ссылкиWITH
запрос будет оцениваться как написанный, без подавления строк, которые впоследствии могут быть отброшены родительским запросом. (Но, как упоминалось выше, вычисление может быть остановлено раньше, если ссылка(и) на запрос требуют только ограниченное количество строк.)
Однако если WITH
запрос является нерекурсивным и не имеет побочных эффектов (т. е. не SELECT
содержит изменчивых функций), то его можно объединить с родительским запросом, что позволяет проводить совместную оптимизацию двух уровней запросов. По умолчанию это происходит, если родительский запрос ссылается на WITH
запрос только один раз, но не в том случае, если он ссылается на WITH
запрос более одного раза. Вы можете переопределить это решение, указав MATERIALIZED
принудительное отдельное вычисление WITH
запроса или указав NOT MATERIALIZED
принудительное объединение его с родительским запросом. При последнем выборе существует риск дублирования вычислений WITH
запроса, но он все же может дать чистую экономию, если для каждого использования WITH
запроса требуется лишь небольшая часть WITH
полного вывода запроса.
Простым примером этих правил является
WITH w AS (
SELECT * FROM big_table
)
SELECT * FROM w WHERE key = 123;
Этот WITH
запрос будет свернут, создав тот же план выполнения, что и
SELECT * FROM big_table WHERE key = 123;
В частности, если есть индекс key
, он, вероятно, будет использоваться для выборки только строк, имеющих key = 123
. С другой стороны, в
WITH w AS (
SELECT * FROM big_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.key = w2.ref
WHERE w2.key = 123;
запрос WITH
будет материализован, создавая временную копию big_table
, которая затем соединяется сама с собой — без какого-либо индекса. Этот запрос будет выполняться намного эффективнее, если его записать в виде
WITH w AS NOT MATERIALIZED (
SELECT * FROM big_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.key = w2.ref
WHERE w2.key = 123;
чтобы ограничения родительского запроса можно было применить непосредственно к сканированию файлов big_table
.
Пример, который NOT MATERIALIZED
может быть нежелательным:
WITH w AS (
SELECT key, very_expensive_function(val) as f FROM some_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.f = w2.f;
Здесь материализация WITH
запроса гарантирует, что very_expensive_function
он оценивается только один раз для каждой строки таблицы, а не дважды.
В приведенных выше примерах показано WITH
использование только с SELECT
, но таким же образом его можно присоединить к INSERT
, UPDATE
, или DELETE
. В каждом случае он эффективно предоставляет временные таблицы, на которые можно ссылаться в основной команде.
Заявления об изменении данных вWITH
Вы можете использовать операторы изменения данных ( INSERT
, UPDATE
, или DELETE
) в WITH
. Это позволяет выполнять несколько разных операций в одном запросе. Пример:
WITH moved_rows AS (
DELETE FROM products
WHERE
"date" >= '2010-10-01' AND
"date" < '2010-11-01'
RETURNING *
)
INSERT INTO products_log
SELECT * FROM moved_rows;
Этот запрос эффективно перемещает строки из products
в products_log
. DELETE
in удаляет WITH
указанные строки из products
, возвращая их содержимое с помощью своего RETURNING
предложения; а затем основной запрос считывает этот вывод и вставляет его в products_log
.
Отличительной чертой приведенного выше примера является то, что WITH
предложение присоединено к элементу INSERT
, а не к подразделу SELECT
внутри элемента INSERT
. Это необходимо, поскольку операторы, изменяющие данные, разрешены только в WITH
предложениях, присоединенных к оператору верхнего уровня. Однако применяются обычные WITH
правила видимости, поэтому можно ссылаться на WITH
вывод оператора из под- SELECT
.
Операторы, изменяющие данные, WITH
обычно содержат RETURNING
предложения, как показано в примере выше. Именно выходные данные RETURNING
предложения, а не целевая таблица оператора, изменяющего данные, формируют временную таблицу, на которую может ссылаться остальная часть запроса. Если в операторе, изменяющем данные, WITH
отсутствует RETURNING
предложение, то он не формирует временную таблицу и на него нельзя ссылаться в остальной части запроса. Тем не менее такой оператор будет выполнен. Не особенно полезный пример:
WITH t AS (
DELETE FROM foo
)
DELETE FROM bar;
В этом примере будут удалены все строки из таблиц foo
и bar
. Количество затронутых строк, сообщаемое клиенту, будет включать только строки, удаленные из bar
.
Рекурсивные ссылки на самих себя в операторах, изменяющих данные, не допускаются. В некоторых случаях можно обойти это ограничение, обратившись к выводу рекурсивного метода WITH
, например:
WITH RECURSIVE included_parts(sub_part, part) AS (
SELECT sub_part, part FROM parts WHERE part = 'our_product'
UNION ALL
SELECT p.sub_part, p.part
FROM included_parts pr, parts p
WHERE p.part = pr.sub_part
)
DELETE FROM parts
WHERE part IN (SELECT part FROM included_parts);
Этот запрос удалит все прямые и косвенные части продукта.
Операторы, изменяющие WITH
данные, выполняются ровно один раз и всегда до конца, независимо от того, считывает ли первичный запрос все (или вообще какие-либо) их выходные данные. Обратите внимание, что это отличается от правила для SELECT
in WITH
: как указано в предыдущем разделе, выполнение a SELECT
выполняется только до тех пор, пока первичный запрос требует его вывода.
Подоператоры WITH
выполняются одновременно друг с другом и с основным запросом. Таким образом, при использовании операторов, изменяющих данные в WITH
, порядок, в котором на самом деле происходят указанные обновления, непредсказуем. Все операторы выполняются с одним и тем же моментальным снимком, поэтому они не могут « видеть » влияние друг друга на целевые таблицы. Это смягчает последствия непредсказуемости фактического порядка обновления строк и означает, что RETURNING
данные — единственный способ сообщить об изменениях между различными WITH
подоператорами и основным запросом. Примером этого является то, что в
WITH t AS (
UPDATE products SET price = price * 1.05
RETURNING *
)
SELECT * FROM products;
внешний SELECT
вернет исходные цены до действия UPDATE
, в то время как в
WITH t AS (
UPDATE products SET price = price * 1.05
RETURNING *
)
SELECT * FROM t;
внешний SELECT
вернет обновленные данные.
Попытка обновить одну и ту же строку дважды в одном операторе не поддерживается. Происходит только одна из модификаций, но достоверно предсказать, какая именно, непросто (а иногда и невозможно). Это также относится к удалению строки, которая уже была обновлена в том же операторе: выполняется только обновление. Поэтому обычно следует избегать попыток дважды изменить одну строку в одном операторе. В частности, избегайте написания WITH
подоператоров, которые могут повлиять на те же строки, которые были изменены основным оператором или родственным подоператором. Последствия такого заявления не будут предсказуемы.
В настоящее время любая таблица, используемая в качестве цели оператора, изменяющего данные, WITH
не должна иметь ни условного правила, ни ALSO
правила, ни INSTEAD
правила, которое расширяется до нескольких операторов.
0 комментариев