SELECT в WITHWITH
WITH позволяет создавать вспомогательные инструкции для использования в основном запросе. Эти инструкции часто называют общими табличными выражениями или CTEможно рассматривать как определение временных таблиц, существующих только в рамках одного запроса. Каждая вспомогательная инструкция
в WITH предложении может быть SELECT,
INSERT, UPDATE, DELETE,
или MERGE; а само
WITH предложение присоединяется к основной инструкции, которая также может быть SELECT, INSERT, UPDATE,
DELETE, или MERGE.
SELECT в WITH #
Основное назначение SELECT в 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), а затем
рекурсивный терм, где только рекурсивный терм может содержать ссылку на собственный результат запроса. Такой запрос выполняется следующим образом:
Вычисление рекурсивного запроса
Вычислите нерекурсивный терм. Для UNION (но не для
UNION ALL), исключите дублирующиеся строки. Включите все оставшиеся
строки в результат рекурсивного запроса, а также поместите их во
временную рабочую таблицу.
Пока рабочая таблица не пуста, повторяйте следующие шаги:
Вычислите рекурсивный терм, подставляя текущее содержимое
рабочей таблицы вместо рекурсивной ссылки на саму себя.
Для UNION (но не UNION ALL), исключите
дублирующиеся строки и строки, которые дублируют любую из предыдущих строк результата.
Включите все остальные строки в результат рекурсивного запроса и
также поместите их во временную промежуточную таблицу.
Замените содержимое рабочей таблицы содержимым промежуточной таблицы, а затем очистите промежуточную таблицу.
Хотя RECURSIVE позволяет определять запросы рекурсивно, внутри такие запросы выполняются итеративно.
В приведенном выше примере рабочая таблица на каждом шаге содержит всего одну строку и принимает значения от 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 * pr.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 это, как и всегда, зависит от конкретной реализации. Данный подход лишь предоставляет удобный способ упорядочить полученные данные после завершения обработки.
Для формирования порядка обхода в глубину мы вычисляем для каждой результирующей строки массив строк, посещённых к данному моменту. Например, рассмотрим следующий запрос, выполняющий поиск в таблице дерево с использованием
ссылка поля:
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, путь) 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 путь;
В общем случае, когда для идентификации строки требуется несколько полей, используйте массив строк. Например, если необходимо отслеживать поля
f1 и f2:
WITH RECURSIVE search_tree(id, link, data, путь) 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, путь || ROW(t.f1, t.f2) FROM tree t, search_tree st WHERE t.id = st.link ) SELECT * FROM search_tree ORDER BY путь;
Опустите ROW() синтаксис в обычном случае, когда необходимо отслеживать только одно поле. Это позволяет использовать простой массив вместо массива составного типа, что повышает эффективность.
Чтобы обеспечить порядок обхода в ширину, можно добавить столбец, отслеживающий глубину поиска, например:
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;
Для получения стабильной сортировки добавьте столбцы данных в качестве вторичных критериев сортировки.
Алгоритм вычисления рекурсивного запроса выдает результат в порядке поиска в ширину (breadth-first search). Однако это является особенностью реализации, и полагаться на нее не следует. Порядок строк на каждом уровне не определен, поэтому в любом случае может потребоваться явная сортировка.
Для вычисления столбца сортировки при поиске в глубину или в ширину предусмотрен встроенный синтаксис. Например:
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 предложение SEARCH определяет, какой поиск требуется выполнить (в глубину или в ширину), список столбцов для отслеживания при сортировке и имя столбца, который будет содержать результирующие данные для сортировки. Этот столбец будет неявно добавлен к выходным строкам обобщенного табличного выражения (CTE).
При работе с рекурсивными запросами важно быть уверенным в том, что рекурсивная часть запроса в конечном итоге не вернет ни одного кортежа, иначе команда будет зацикливаться бесконечно. Иногда использование
UNION вместо UNION ALL позволяет добиться этого путем исключения строк, дублирующих предыдущие выходные строки. Однако зачастую цикл не ограничивается полностью дублирующимися выходными строками: для определения того, была ли достигнута та же точка ранее, может потребоваться проверка только одного или нескольких полей. Стандартный метод обработки таких ситуаций заключается в формировании массива уже посещенных значений. Например, рассмотрим еще раз следующий запрос, выполняющий поиск в таблице граф с использованием
ссылка поля:
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;
Данный запрос зациклится, если ссылка связи содержат
циклы. Поскольку требуется получить «depth» вывод, простая замена
UNION ALL на UNION не устранит зацикливание.
Вместо этого необходимо определить, встретилась ли та же строка снова
при прохождении по определённому пути ссылок. Мы добавим два столбца
is_cycle и путь в запрос, подверженный зацикливанию:
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;
Опустите ROW() синтаксис в обычном случае, когда для распознавания цикла достаточно проверить только одно поле. Это позволяет использовать обычный массив вместо массива составного типа, что повышает эффективность.
Для упрощения обнаружения циклов предусмотрен встроенный синтаксис. Приведенный выше запрос также можно написать следующим образом:
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 предложение сначала определяет список столбцов, которые необходимо отслеживать для обнаружения циклов, затем имя столбца, содержащего признак обнаружения цикла, и, наконец, имя еще одного столбца, в котором будет сохраняться путь. Столбцы cycle и path будут неявно добавлены в выходные строки обобщенного табличного выражения (CTE).
Столбец пути цикла вычисляется так же, как и столбец упорядочивания при поиске в глубину, описанный в предыдущем разделе. Запрос может содержать одновременно и
SEARCH и CYCLE предложение, но совместное использование спецификации поиска в глубину и спецификации обнаружения циклов приведет к избыточным вычислениям, поэтому эффективнее использовать только
CYCLE предложение и выполнять сортировку по столбцу path. Если же требуется упорядочивание при поиске в ширину, то указание обоих предложений
SEARCH и CYCLE может быть полезным.
Полезный прием для тестирования запросов, когда вы не уверены, не зациклятся ли они, заключается в добавлении LIMIT
в основной запрос. Например, выполнение этого запроса приведет к бесконечному циклу без LIMIT:
WITH RECURSIVE t(n) AS (
SELECT 1
UNION ALL
SELECT n+1 FROM t
)
SELECT n FROM t LIMIT 100;
Это работает, потому что Digital Q.DataBaseреализация
вычисляет ровно столько строк 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, или MERGE. В каждом из этих случаев фактически создаются временные таблицы, к которым можно обращаться в основной команде.
WITH #
Вы можете использовать инструкции, изменяющие данные (INSERT,
UPDATE, DELETE, или
MERGE) в 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 в WITH
удаляет указанные строки из products, возвращая их содержимое посредством своего RETURNING предложении; и затем основной запрос считывает этот результат и вставляет его в
products_log.
Тонкий момент приведенного выше примера заключается в том, что WITH предложение
присоединяется к INSERT, а не к под-SELECT внутри
команды INSERT. Это необходимо, так как команды изменения данных разрешены только в WITH предложениях, которые присоединены
к инструкции верхнего уровня. Однако применяются обычные WITH правила
видимости, поэтому можно ссылаться на WITH
результат команды из под-SELECT.
Команды изменения данных в WITH обычно содержат
RETURNING предложения (см. Раздел 2.3.4),
как показано в примере выше.
Именно вывод предложения 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 в WITH: как было сказано в
предыдущем разделе, выполнение SELECT осуществляется лишь в той степени, в которой основной запрос требует их вывода.
Вложенные инструкции в WITH выполняются параллельно друг с другом и с основным запросом. Таким образом, при использовании изменяющих данные инструкций в WITH, порядок фактического выполнения указанных обновлений является непредсказуемым. Все инструкции выполняются в рамках
одного и того же snapshot (см. Глава 2.10), поэтому они
не могут «видеть» результаты изменений, внесенных друг другом в целевые таблицы. Это сглаживает последствия непредсказуемости фактического порядка обновления строк и означает, что 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 правила, которое разворачивается в несколько инструкций.