RETURN QUERY и RETURN NEXT в PostgreSQL: функции PL/pgSQL, IF и FOUND

RETURN QUERY и RETURN NEXT в PostgreSQL: функции PL/pgSQL, IF и FOUND Полезное

RETURN QUERY в PostgreSQL — это команда языка PL/pgSQL, которая добавляет в результат функции все строки указанного запроса. Работает она только в функциях, возвращающих набор строк (RETURNS SETOF ... или RETURNS TABLE (...)), и не завершает функцию: выполнение идет дальше, к следующему оператору.

Ниже — рабочая функция с 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)?
Нет, процедура не возвращает набор строк. Данные из нее отдают через выходные параметры или записывают в таблицу.

OTUS Журнал