RETURN QUERY в PostgreSQL — это команда языка PL/pgSQL, которая добавляет в результат функции все строки указанного запроса. Работает она только в функциях, возвращающих набор строк (RETURNS SETOF ... или RETURNS TABLE (...)), и не завершает функцию: выполнение идет дальше, к следующему оператору.
Содержание
- Четыре вида RETURN: что есть что
- Минимальный пример: функция с RETURN QUERY
- RETURN QUERY не завершает функцию
- RETURN NEXT: когда строки собираются по одной
- Типичные ошибки и их тексты
- IF … ELSIF … ELSE и переменные
- FOUND: после каких команд и что означает
- RETURN QUERY EXECUTE: динамический запрос
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — рабочая функция с RETURN QUERY, затем построчный вариант через RETURN NEXT, условия IF ... ELSIF ... ELSE, переменные и FOUND, типичные ошибки с их текстами. Все примеры проверены 30.09.2026 на PostgreSQL 18.6 в psql; сами конструкции не новые и есть и в более ранних версиях.
Четыре вида RETURN: что есть что
В PL/pgSQL слово RETURN входит в четыре разные команды, и их легко спутать.
| Команда | Что делает | Завершает функцию |
|---|---|---|
RETURN выражение; |
возвращает одно значение (скаляр или строку) | да |
RETURN NEXT выражение; |
добавляет в результат одну строку | нет |
RETURN QUERY запрос; |
добавляет в результат все строки запроса | нет |
RETURN; |
завершает функцию, возвращающую набор или void |
да |
Вторая пара терминов — объявление результата. RETURNS SETOF тип говорит, что функция вернет набор значений этого типа (например, SETOF orders — строки со структурой таблицы orders). RETURNS TABLE (столбцы) — то же самое, но со своим списком столбцов: это краткая запись выходных параметров плюс SETOF.
Минимальный пример: функция с RETURN QUERY
Учебная таблица заказов и функция, которая возвращает заказы с заданным статусом. Скрипт можно целиком вставить в psql в пустой тестовой базе.
CREATE TABLE orders (
id integer PRIMARY KEY,
customer text NOT NULL,
amount numeric(10,2) NOT NULL CHECK (amount >= 0),
status text NOT NULL CHECK (status IN ('new', 'paid', 'cancelled'))
);
INSERT INTO orders VALUES
(1, 'anna', 1200.00, 'paid'),
(2, 'anna', 300.50, 'new'),
(3, 'boris', 5400.00, 'paid'),
(4, 'boris', 150.00, 'cancelled');
CREATE FUNCTION orders_by_status(p_status text)
RETURNS TABLE (order_id integer, customer text, amount numeric)
LANGUAGE plpgsql STABLE
AS $$
BEGIN
RETURN QUERY
SELECT o.id, o.customer, o.amount
FROM orders AS o
WHERE o.status = p_status
ORDER BY o.id;
END;
$$;
SELECT * FROM orders_by_status('paid');
Запрос вернет две строки — оплаченные заказы 1 и 3:
order_id | customer | amount
----------+----------+---------
1 | anna | 1200.00
3 | boris | 5400.00
(2 rows)
Что здесь важно. Функция вызывается в FROM, как таблица. Столбцы запроса сопоставляются со столбцами из RETURNS TABLE по порядку, а не по именам. Финальный RETURN; не обязателен: дойдя до END, функция с набором строк завершится сама.
Столбцы таблицы в запросе названы через псевдоним o. не ради красоты. Имена из RETURNS TABLE внутри функции становятся переменными, и без псевдонима PostgreSQL не поймет, что имеется в виду — столбец или переменная. Эта ошибка разобрана ниже.
RETURN QUERY не завершает функцию
Так как команда только добавляет строки, в одной функции их может быть несколько — результаты склеятся. После каждого RETURN QUERY переменная FOUND показывает, вернул ли именно этот запрос хотя бы одну строку.
CREATE FUNCTION orders_or_stub(p_customer text)
RETURNS TABLE (order_id integer, amount numeric, note text)
LANGUAGE plpgsql STABLE
AS $$
BEGIN
RETURN QUERY
SELECT o.id, o.amount, 'оплачен'::text
FROM orders AS o
WHERE o.customer = p_customer AND o.status = 'paid';
RETURN QUERY
SELECT o.id, o.amount, 'ждет оплаты'::text
FROM orders AS o
WHERE o.customer = p_customer AND o.status = 'new';
IF NOT FOUND THEN
RAISE NOTICE 'у клиента % нет неоплаченных заказов', p_customer;
END IF;
RETURN;
END;
$$;
SELECT * FROM orders_or_stub('anna');
SELECT * FROM orders_or_stub('boris');
Для anna придут обе строки: заказ 1 с пометкой «оплачен» и заказ 2 с пометкой «ждет оплаты». Для boris придет одна строка (заказ 3), а перед ней — сообщение, потому что второй запрос оказался пустым:
NOTICE: у клиента boris нет неоплаченных заказов
order_id | amount | note
----------+---------+---------
3 | 5400.00 | оплачен
(1 row)
Граница, о которой стоит знать: текущая реализация сначала собирает весь результат функции, и только потом отдает его вызывающему запросу. Пока набор помещается в work_mem, он лежит в памяти, дальше пишется на диск. Для миллионов строк обычный запрос или представление часто выгоднее такой функции — это нужно измерять на своих данных.
RETURN NEXT: когда строки собираются по одной
RETURN NEXT нужен, когда строку результата приходится вычислять в цикле. В функции с RETURNS TABLE (или с выходными параметрами) значения присваиваются переменным-столбцам, а RETURN NEXT; без аргумента сохраняет их текущие значения как очередную строку.
CREATE FUNCTION running_total()
RETURNS TABLE (order_id integer, amount numeric, total numeric)
LANGUAGE plpgsql STABLE
AS $$
DECLARE
r record;
BEGIN
total := 0;
FOR r IN SELECT o.id, o.amount FROM orders AS o
WHERE o.status <> 'cancelled' ORDER BY o.id
LOOP
order_id := r.id;
amount := r.amount;
total := total + r.amount;
RETURN NEXT;
END LOOP;
END;
$$;
SELECT * FROM running_total();
Функция вернет три неотмененных заказа с накопленной суммой:
order_id | amount | total
----------+---------+---------
1 | 1200.00 | 1200.00
2 | 300.50 | 1500.50
3 | 5400.00 | 6900.50
(3 rows)
Пример учебный: тот же результат дает один запрос с оконной функцией sum(amount) OVER (ORDER BY id), и он обычно быстрее цикла. Как выбирать:
| Задача | Что взять |
|---|---|
| результат выражается одним запросом | RETURN QUERY (или функция на LANGUAGE sql) |
| нужно склеить несколько запросов или выбрать запрос по условию | несколько RETURN QUERY и IF |
| строка вычисляется пошагово, с состоянием между строками | цикл и RETURN NEXT |
| имя столбца или таблицы приходит параметром | RETURN QUERY EXECUTE |
Для функции со скалярным набором аргумент указывается явно: в RETURNS SETOF integer пишут RETURN NEXT i * i;.
Типичные ошибки и их тексты
Имя столбца совпало с переменной
Неверно — столбцы результата названы так же, как столбцы таблицы, а в запросе нет псевдонима:
CREATE FUNCTION bad_ambiguous(p_status text)
RETURNS TABLE (id integer, amount numeric)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT id, amount FROM orders WHERE status = p_status;
END;
$$;
SELECT * FROM bad_ambiguous('paid');
Функция создается без ошибок, а падает при вызове:
ERROR: column reference "id" is ambiguous
LINE 1: SELECT id, amount FROM orders WHERE status = p_status
^
DETAIL: It could refer to either a PL/pgSQL variable or a table column.
Исправление — то, что сделано в первом примере: псевдоним таблицы (o.id, o.amount) и имена выходных столбцов, не совпадающие со столбцами таблицы (order_id). Параметрам удобно давать префикс p_, переменным — v_.
Типы запроса и результата не совпали
Неверно — amount в таблице имеет тип numeric, а в объявлении функции стоит integer:
CREATE FUNCTION bad_types()
RETURNS TABLE (order_id integer, amount integer)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY SELECT o.id, o.amount FROM orders AS o;
END;
$$;
SELECT * FROM bad_types();
ERROR: structure of query does not match function result type
DETAIL: Returned type numeric(10,2) does not match expected type integer in column "amount" (position 2).
В отличие от обычного RETURN выражение, здесь типы автоматически не приводятся. Исправление — объявить столбец как numeric или привести явно в запросе: o.amount::integer (с потерей копеек, если это допустимо). С тем же заголовком ошибки и деталью Number of returned columns (1) does not match expected column count (2) функция упадет, если в запросе меньше столбцов, чем объявлено.
Еще три частых сообщения
| Сообщение | Причина | Что делать |
|---|---|---|
query has no destination for result data |
в теле стоит голый SELECT без RETURN QUERY и без INTO |
добавить RETURN QUERY, INTO переменная или PERFORM, если результат не нужен |
cannot use RETURN QUERY in a non-SETOF function |
функция объявлена как RETURNS integer, а не набор |
объявить RETURNS SETOF ... или RETURNS TABLE |
control reached end of function without RETURN |
скалярная функция дошла до END, не встретив RETURN |
добавить RETURN во все ветки |
Вторая ошибка из таблицы возникает уже при CREATE FUNCTION, первая и третья — при вызове.
IF … ELSIF … ELSE и переменные
Условный оператор в PL/pgSQL имеет три формы: IF ... THEN ... END IF, с веткой ELSE и с цепочкой ELSIF. Условия проверяются сверху вниз, выполняется первая истинная ветка, остальные пропускаются. В примере заодно видны переменные: они объявляются в секции DECLARE, значение из запроса кладется в них через SELECT ... INTO.
CREATE FUNCTION order_label(p_id integer)
RETURNS text
LANGUAGE plpgsql STABLE
AS $$
DECLARE
v_amount numeric;
v_status text;
BEGIN
SELECT o.amount, o.status INTO v_amount, v_status
FROM orders AS o
WHERE o.id = p_id;
IF NOT FOUND THEN
RETURN 'заказа нет';
ELSIF v_status = 'cancelled' THEN
RETURN 'отменен';
ELSIF v_amount >= 1000 THEN
RETURN 'крупный';
ELSE
RETURN 'обычный';
END IF;
END;
$$;
SELECT id, order_label(id) FROM generate_series(1, 5) AS id;
id | order_label
----+-------------
1 | крупный
2 | обычный
3 | крупный
4 | отменен
5 | заказа нет
(5 rows)
Три детали, на которых чаще всего ошибаются.
Написание. Правильно ELSIF, допускается синоним ELSEIF. Запись ELSE IF в два слова открывает вложенный IF, которому нужен свой END IF; без него получится syntax error at or near ";".
NULL в условии. Если условие дало NULL, ветка THEN не выполняется — управление уходит в ELSE. Переменная без начального значения равна NULL, поэтому IF v_limit > 100 для незаданного v_limit молча пойдет в ELSE. Пустое значение проверяют явно: IF v_limit IS NULL THEN.
Запрос в условии. Конструкции IF SELECT ... нет. Наличие строк проверяют через IF EXISTS (SELECT 1 FROM orders WHERE ...) THEN, число — через скалярный подзапрос в скобках: IF (SELECT count(*) FROM orders WHERE status = 'paid') > 1 THEN.
FOUND: после каких команд и что означает
FOUND — логическая переменная, локальная для каждого вызова функции; в начале вызова она равна false.
| Команда | FOUND равна true, если |
|---|---|
SELECT ... INTO |
запрос вернул строку |
PERFORM |
запрос вернул хотя бы одну строку |
UPDATE, INSERT, DELETE, MERGE |
затронута хотя бы одна строка |
FETCH |
курсор вернул строку |
MOVE |
курсор удалось переместить |
RETURN QUERY, RETURN QUERY EXECUTE |
запрос вернул хотя бы одну строку |
цикл FOR или FOREACH |
тело выполнилось хотя бы раз (значение ставится при выходе из цикла) |
Остальные команды FOUND не трогают. В частности, обычный EXECUTE ее значение не меняет — после него число строк берут командой GET DIAGNOSTICS v_rows = ROW_COUNT;.
SELECT ... INTO без уточнений снисходителен: если строк несколько, он возьмет первую из возвращенных (без ORDER BY — какую именно, не определено) и промолчит, если строк нет — запишет NULL. В прогоне запрос по клиенту anna с двумя заказами вернул id = 1, FOUND = t, по несуществующему клиенту — id = <NULL>, FOUND = f. Когда строка должна быть ровно одна, пишут INTO STRICT: тогда лишние строки дают ошибку query returned more than one row, а отсутствие строк — query returned no rows.
RETURN QUERY EXECUTE: динамический запрос
Если от параметра зависит не значение, а имя столбца или таблицы, запрос собирают строкой и выполняют через RETURN QUERY EXECUTE. Здесь легко получить SQL-инъекцию, поэтому правил три: значения передавать через USING (в тексте они $1, $2), идентификаторы подставлять через format() со спецификатором %I, а допустимые имена сверять со списком.
CREATE FUNCTION top_orders(p_sort text, p_min numeric)
RETURNS SETOF orders
LANGUAGE plpgsql STABLE
AS $$
BEGIN
IF p_sort IS NULL OR p_sort NOT IN ('id', 'amount', 'customer') THEN
RAISE EXCEPTION 'сортировка по "%" не разрешена', p_sort;
END IF;
RETURN QUERY EXECUTE format(
'SELECT * FROM orders WHERE amount >= $1 ORDER BY %I DESC', p_sort)
USING p_min;
END;
$$;
SELECT * FROM top_orders('amount', 300);
SELECT * FROM top_orders('amount; DROP TABLE orders', 0);
Первый вызов вернет заказы 3, 1 и 2 — по убыванию суммы. Второй остановится на проверке списка, таблица останется на месте:
ERROR: сортировка по "amount; DROP TABLE orders" не разрешена
Это базовая защита учебного примера, а не полный перечень мер. Например, NULL в p_min функция не отклоняет: условие amount >= NULL не истинно ни для одной строки, и вызов молча вернет пустой набор. %I экранирует идентификатор, но не проверяет, что такой столбец разрешено показывать, поэтому список допустимых имен нужен отдельно. Права на саму функцию и на таблицу, а для функций с SECURITY DEFINER еще и фиксированный search_path — отдельная настройка, которая в пример не вошла.
Выводы
RETURN QUERYдобавляет в результат все строки запроса,RETURN NEXT— одну строку; обе команды не завершают функцию и работают только приRETURNS SETOFилиRETURNS TABLE.- Столбцы запроса сопоставляются с объявленным результатом по порядку, типы должны совпадать точно; иначе будет
structure of query does not match function result type. - Имена из
RETURNS TABLE— это переменные: столбцам таблицы в запросе нужен псевдоним, иначеcolumn reference is ambiguous. - В
IFусловие со значениемNULLуходит вELSE; правильное написание —ELSIF, а запрос в условии пишется черезEXISTS (...)или подзапрос в скобках. FOUNDпоказывает, нашла ли строки последняя команда; для строго одной строки нуженINTO STRICT, для динамики —RETURN QUERY EXECUTEсUSINGи%I.
Где применяется / связь с практикой
Функции с набором строк встречаются в отчетах, в API поверх базы, в миграциях и служебных скриптах администратора. Там же всплывают вопросы, которые выходят за рамки синтаксиса: как функция влияет на план запроса, когда ее результат материализуется, какие права и блокировки она берет. О планах, индексах и транзакциях — в статье про индексы, EXPLAIN, транзакции и VACUUM.
Освойте тему на практике
Системно эти темы разбирают на курсе PostgreSQL для администраторов баз данных и разработчиков. Познакомиться с форматом и преподавателями можно на открытых уроках Otus.
FAQ
Чем функция на PL/pgSQL с RETURN QUERY отличается от функции на LANGUAGE sql?
Функция на SQL состоит только из запросов и возвращает результат последнего; простую такую функцию планировщик может встроить в вызывающий запрос, но это зависит от ее свойств и подтверждается планом. PL/pgSQL нужен, когда требуются переменные, условия, циклы или обработка ошибок.
Можно ли вызвать функцию с набором строк в списке SELECT, а не в FROM?
Можно, но SELECT orders_by_status('paid'); вернет каждую строку одним составным значением вида (1,anna,1200.00). Чтобы получить отдельные столбцы, функцию вызывают в FROM.
Что такое CASE в PL/pgSQL и чем он отличается от IF?
Это второй условный оператор с ветками WHEN. Главное отличие в поведении: если ни одна ветка не подошла и ELSE нет, IF ничего не делает, а CASE вызывает исключение CASE_NOT_FOUND.
Работают ли RETURN NEXT и RETURN QUERY в процедурах (CREATE PROCEDURE)?
Нет, процедура не возвращает набор строк. Данные из нее отдают через выходные параметры или записывают в таблицу.



