Dieser Artikel ist derzeit auf Russisch — die englische Übersetzung ist in Arbeit.
В PostgreSQL после INSERT, UPDATE или DELETE можно сразу вернуть данные затронутых строк.
Для этого есть клауза:
RETURNING
Она позволяет сделать запись в таблицу и тут же получить результат обратно, почти как в SELECT.
Например, вы вставляете пользователя и хотите сразу узнать его сгенерированный id.
Без RETURNING пришлось бы делать два запроса:
INSERT INTO users (email, name)
VALUES ('a@example.com', 'Ann');
SELECT id
FROM users
WHERE email = 'a@example.com';
С RETURNING всё делается одной командой:
INSERT INTO users (email, name)
VALUES ('a@example.com', 'Ann')
RETURNING id;
PostgreSQL вставит строку и сразу вернёт её id.
Это удобно, безопасно и экономит лишний поход в базу.
Зачем нужен RETURNING
Самый частый сценарий:
вставили строку — нужно получить её id.
Например, есть таблица users:
CREATE TABLE users (
id bigserial PRIMARY KEY,
email text NOT NULL UNIQUE,
name text NOT NULL,
country text,
created_at timestamptz NOT NULL DEFAULT now()
);
Вставляем пользователя:
INSERT INTO users (email, name, country)
VALUES ('a@example.com', 'Ann', 'DE')
RETURNING id, created_at;
Результат:
id | created_at
---+-------------------------------
1 | 2026-06-18 10:15:30.123456+00
PostgreSQL вернул значения, которые появились после вставки:
- сгенерированный
id;
- значение
created_at из DEFAULT now();
- любые другие поля, которые заполнила база.
Главная идея:
RETURNING возвращает строки, которые были реально затронуты командой.
RETURNING работает не только с INSERT
RETURNING можно использовать с разными DML-командами:
INSERT ... RETURNING
UPDATE ... RETURNING
DELETE ... RETURNING
То есть можно вернуть:
- вставленные строки;
- обновлённые строки;
- удалённые строки.
Примеры:
INSERT INTO users (email, name)
VALUES ('a@example.com', 'Ann')
RETURNING id, email;
UPDATE users
SET country = 'ES'
WHERE email = 'a@example.com'
RETURNING id, email, country;
DELETE FROM users
WHERE email = 'a@example.com'
RETURNING id, email;
Во всех трёх случаях PostgreSQL не просто выполняет команду, а ещё отдаёт результат.
INSERT RETURNING: получить id новой строки
Допустим, приложение создаёт пользователя.
INSERT INTO users (email, name, country)
VALUES ('ada@example.com', 'Ada', 'GB')
RETURNING id;
Результат:
id
--
42
Теперь приложение сразу знает id созданного пользователя.
Это лучше, чем делать второй запрос по email.
Почему?
Потому что:
- меньше round-trip до базы;
- меньше кода;
- нет риска выбрать не ту строку;
- всё происходит внутри одной SQL-команды;
- возвращаются именно значения той строки, которую вставила база.
Если после создания пользователя нужно создать связанные записи, RETURNING особенно полезен.
RETURNING несколько колонок
Можно вернуть не только id.
Например:
INSERT INTO users (email, name, country)
VALUES ('ada@example.com', 'Ada', 'GB')
RETURNING id, email, name, country, created_at;
Результат:
id | email | name | country | created_at
---+-----------------+------+---------+-------------------------------
42 | ada@example.com | Ada | GB | 2026-06-18 10:15:30.123456+00
Это удобно, когда приложение хочет сразу получить готовую запись в том виде, в котором она лежит в базе.
RETURNING *
Если нужны все колонки, можно написать:
RETURNING *
Пример:
INSERT INTO orders (user_id, amount, status)
VALUES (42, 99.50, 'pending')
RETURNING *;
Результат может быть таким:
id | user_id | amount | status | created_at
-----+---------+--------+---------+-------------------------------
1001 | 42 | 99.50 | pending | 2026-06-18 10:20:00.123456+00
RETURNING * полезен, если:
- часть колонок заполняется через
DEFAULT;
- есть
bigserial или identity-колонка;
- есть generated columns;
- есть триггеры, которые меняют значения при записи;
- приложению нужна вся итоговая строка.
Но в API и продакшн-коде часто лучше явно перечислять нужные поля:
RETURNING id, status, created_at
Так запрос стабильнее и не зависит от будущих изменений схемы.
RETURNING с выражениями
В RETURNING можно писать не только названия колонок, но и выражения.
Например, обновим сумму заказа на 10% и вернём новую сумму:
UPDATE orders
SET amount = amount * 1.10
WHERE status = 'pending'
RETURNING
id,
amount AS new_amount;
Можно добавить вычисление:
UPDATE orders
SET amount = amount * 1.10
WHERE status = 'pending'
RETURNING
id,
amount AS new_amount,
round(amount / 1.10, 2) AS approx_old_amount;
Здесь важно: в UPDATE ... RETURNING колонка amount — это уже новое значение после обновления.
То есть если было 100, а стало 110, то RETURNING amount вернёт 110.
UPDATE RETURNING: вернуть обновлённые строки
Допустим, нужно поднять зарплату всем сотрудникам отдела eng на 5% и сразу вернуть новые значения.
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, name, salary AS new_salary;
Результат:
id | name | new_salary
---+-------+-----------
1 | Anna | 210000
2 | Bob | 168000
3 | Kate | 157500
Команда обновила строки и сразу вернула их.
Это удобно для:
- админок;
- API;
- логирования;
- проверки результата;
- последующей обработки в приложении.
RETURNING в UPDATE показывает новые значения
Это важная ловушка.
В запросе:
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, salary;
salary в RETURNING — это уже зарплата после увеличения.
Если вам нужно получить старую зарплату, одного простого RETURNING salary недостаточно.
Можно вычислить старое значение обратной операцией, если это безопасно:
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING
id,
round(salary / 1.05, 2) AS old_salary,
salary AS new_salary;
Но такой подход подходит не всегда.
Более надёжный вариант — заранее сохранить старые значения в CTE.
WITH old_rows AS (
SELECT
id,
salary AS old_salary
FROM employees
WHERE dept = 'eng'
),
updated AS (
UPDATE employees e
SET salary = e.salary * 1.05
FROM old_rows old
WHERE e.id = old.id
RETURNING
e.id,
old.old_salary,
e.salary AS new_salary
)
SELECT
id,
old_salary,
new_salary
FROM updated
ORDER BY id;
Так можно получить честную пару:
было / стало
DELETE RETURNING: вернуть удалённые строки
RETURNING работает и с DELETE.
Например, удалить отменённые заказы и вернуть, что именно удалили:
DELETE FROM orders
WHERE status = 'cancelled'
RETURNING id, user_id, amount, status;
Результат:
id | user_id | amount | status
-----+---------+--------+----------
1005 | 42 | 50.00 | cancelled
1009 | 77 | 99.00 | cancelled
Это удобно для аудита.
Без RETURNING пришлось бы сначала делать:
SELECT ...
а потом:
DELETE ...
Между этими двумя командами данные могли бы измениться.
С DELETE ... RETURNING вы точно получаете строки, которые были удалены именно этой командой.
RETURNING возвращает несколько строк
RETURNING возвращает не одну строку, а столько строк, сколько было реально затронуто.
Например:
UPDATE orders
SET status = 'archived'
WHERE created_at < now() - interval '1 year'
RETURNING id, status;
Если обновилось 1000 заказов, RETURNING вернёт 1000 строк.
То же самое с массовым INSERT:
INSERT INTO roles (code, title)
VALUES
('admin', 'Administrator'),
('manager', 'Manager'),
('student', 'Student')
RETURNING id, code;
Результат:
id | code
---+---------
1 | admin
2 | manager
3 | student
RETURNING и порядок строк
Порядок строк в RETURNING не стоит считать гарантированным.
Например:
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, salary;
Может вернуть строки не в том порядке, в котором вы ожидаете.
Если нужен стабильный порядок, оберните команду в CTE и отсортируйте внешним SELECT.
WITH bumped AS (
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, salary AS new_salary
)
SELECT
id,
new_salary
FROM bumped
ORDER BY id;
Главное правило:
Нужен порядок — сортируйте внешним SELECT ... ORDER BY.
RETURNING внутри CTE
Самый мощный приём — использовать RETURNING внутри CTE.
Так можно выполнить несколько связанных действий одним SQL-запросом.
Например:
- вставить пользователя;
- получить его
id;
- создать для него первый заказ.
WITH new_user AS (
INSERT INTO users (email, name, country)
VALUES ('team@example.com', 'Team', 'ES')
RETURNING id
)
INSERT INTO orders (user_id, amount, status)
SELECT
id,
0,
'pending'
FROM new_user
RETURNING id AS order_id, user_id;
Что происходит:
new_user вставляет пользователя и возвращает id
второй INSERT использует этот id для создания заказа
Всё выполняется как один SQL-запрос.
Это удобно, потому что:
- не нужно делать отдельный запрос за id;
- меньше логики в приложении;
- вся цепочка атомарна;
- результат можно вернуть сразу.
Пример: создать пользователя и настройки
Допустим, при создании пользователя нужно сразу создать дефолтные настройки.
WITH new_user AS (
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
RETURNING id
)
INSERT INTO user_settings (user_id, key, value)
SELECT
id,
'language',
'ru'
FROM new_user
RETURNING user_id, key, value;
Результат:
user_id | key | value
--------+----------+------
42 | language | ru
Здесь RETURNING помогает передать id из первой операции во вторую.
Пример: перенести удалённые строки в архив
DELETE ... RETURNING можно использовать для архивации.
Например, перенести старые заказы в таблицу orders_archive.
WITH deleted_orders AS (
DELETE FROM orders
WHERE created_at < now() - interval '2 years'
RETURNING *
)
INSERT INTO orders_archive
SELECT *
FROM deleted_orders;
Что происходит:
DELETE удаляет старые заказы;
RETURNING * возвращает удалённые строки;
- внешний
INSERT записывает их в архив.
Это один SQL-запрос.
Но в реальном проекте с таким подходом нужно быть аккуратным:
- схемы таблиц должны совпадать;
- объём удаляемых данных может быть большим;
- лучше архивировать батчами;
- нужно учитывать foreign keys и зависимости.
Пример: посчитать, сколько строк обновилось
Можно обернуть UPDATE ... RETURNING в CTE и посчитать строки.
WITH updated AS (
UPDATE orders
SET status = 'archived'
WHERE created_at < now() - interval '1 year'
RETURNING id
)
SELECT count(*) AS updated_count
FROM updated;
Результат:
updated_count
-------------
128
Это удобно, когда приложению не нужны все обновлённые строки, а нужно только количество.
RETURNING и ON CONFLICT DO NOTHING
RETURNING хорошо сочетается с ON CONFLICT.
Но важно понимать поведение.
Пример:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING
RETURNING id, email;
Если пользователь был вставлен, RETURNING вернёт строку.
Если пользователь с таким email уже существовал, DO NOTHING пропустит вставку, и RETURNING вернёт 0 строк.
То есть:
вставили новую строку -> RETURNING вернул строку
пропустили из-за конфликта -> RETURNING пустой
Это полезно для идемпотентных операций.
Например, таблица обработанных событий:
CREATE TABLE processed_events (
event_id text PRIMARY KEY,
processed_at timestamptz NOT NULL DEFAULT now()
);
Попытка отметить событие как обработанное:
INSERT INTO processed_events (event_id)
VALUES ('evt_123')
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
Если RETURNING вернул event_id, событие новое и его можно обрабатывать.
Если результат пустой, событие уже было обработано раньше.
RETURNING и ON CONFLICT DO UPDATE
Если при конфликте вы не пропускаете строку, а обновляете её, RETURNING вернёт обновлённую строку.
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada New')
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name
RETURNING id, email, name;
Если пользователя не было — он вставится, и RETURNING вернёт вставленную строку.
Если пользователь уже был — он обновится, и RETURNING вернёт обновлённую строку.
Это отличие от DO NOTHING.
DO NOTHING -> при конфликте RETURNING пустой
DO UPDATE -> при конфликте RETURNING вернёт обновлённую строку
RETURNING не коммитит транзакцию
RETURNING возвращает значения сразу после выполнения команды, но это не значит, что транзакция уже зафиксирована.
Например:
BEGIN;
INSERT INTO users (email, name)
VALUES ('a@example.com', 'Ann')
RETURNING id;
ROLLBACK;
RETURNING может вернуть id, но после ROLLBACK строки в таблице не будет.
То есть RETURNING показывает результат внутри текущей транзакции.
Фиксация изменений всё равно зависит от COMMIT.
RETURNING и триггеры / defaults
RETURNING возвращает итоговые значения строки после применения логики базы при записи.
Например:
CREATE TABLE orders (
id bigserial PRIMARY KEY,
amount numeric NOT NULL,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);
Запрос:
INSERT INTO orders (amount)
VALUES (99.90)
RETURNING id, amount, status, created_at;
вернёт не только amount, который вы передали, но и:
- сгенерированный
id;
- дефолтный
status;
- дефолтный
created_at.
Если в базе есть триггеры, которые меняют строку при вставке или обновлении, RETURNING позволяет приложению получить уже итоговый вариант строки.
RETURNING и права доступа
Чтобы вернуть колонку через RETURNING, у пользователя должны быть права на чтение этой колонки.
Например:
INSERT INTO users (email, name)
VALUES ('a@example.com', 'Ann')
RETURNING id, email;
нужны права не только на вставку, но и на чтение возвращаемых полей.
В обычных приложениях это редко становится сюрпризом, но в системах с тонкой настройкой прав стоит помнить:
RETURNING — это не только запись, но и чтение возвращаемых данных.
Когда RETURNING особенно полезен
RETURNING стоит использовать, когда нужно:
- получить
id после вставки;
- получить значения по умолчанию;
- получить строку после триггеров;
- вернуть обновлённые значения;
- понять, какие строки были удалены;
- связать несколько операций через CTE;
- обработать
ON CONFLICT DO NOTHING;
- построить атомарную цепочку действий;
- уменьшить количество запросов из приложения.
Пример типичного API:
INSERT INTO users (email, name, country)
VALUES ($1, $2, $3)
RETURNING id, email, name, country, created_at;
Приложение сразу получает объект созданного пользователя.
Когда RETURNING не нужен
RETURNING не всегда нужен.
Если вы делаете массовое обновление миллионов строк и не собираетесь читать результат, лучше не возвращать все строки.
Например:
UPDATE events
SET processed = true
WHERE processed = false;
Если добавить:
RETURNING *
PostgreSQL начнёт отдавать огромный результат клиенту.
Это может быть дорого по памяти, сети и времени.
Правило простое:
Возвращайте только то, что действительно нужно.
Лучше:
RETURNING id
чем:
RETURNING *
если вам нужен только идентификатор.
А если результат вообще не нужен — не используйте RETURNING.
MySQL: RETURNING обычно нет
В MySQL нет такого же универсального RETURNING, как в PostgreSQL.
Обычно после вставки автоинкрементного id используют:
INSERT INTO users (email, name, country)
VALUES ('a@example.com', 'Ann', 'DE');
SELECT LAST_INSERT_ID();
LAST_INSERT_ID() возвращает id, сгенерированный в текущем соединении.
Это важно: он не должен перепутаться с вставками других соединений.
Но всё равно это отдельный запрос.
Для обновлений и удалений в MySQL обычно используют другие подходы:
- сначала
SELECT, потом UPDATE или DELETE;
- транзакции;
- временные таблицы;
- application-level логику.
MariaDB в некоторых версиях поддерживает RETURNING для части DML-команд, но это не то же самое, что универсальная привычная PostgreSQL-семантика. При переносе всегда проверяйте конкретную СУБД и версию.
ClickHouse
В ClickHouse RETURNING в стиле PostgreSQL обычно не используется.
ClickHouse — аналитическая колоночная СУБД, где вставки часто батчевые, а модель записи сильно отличается от PostgreSQL.
Если нужны ключи строк, их обычно:
- генерируют на стороне приложения;
- передают в данных при вставке;
- строят из бизнес-ключей;
- рассчитывают заранее.
То есть PostgreSQL-паттерн:
INSERT ... RETURNING id
на ClickHouse напрямую обычно не переносится.
Практические шаблоны
Вставить пользователя и вернуть id
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
RETURNING id;
Вставить пользователя и вернуть готовую строку
INSERT INTO users (email, name, country)
VALUES ('ada@example.com', 'Ada', 'GB')
RETURNING id, email, name, country, created_at;
Вставить заказ и вернуть все поля
INSERT INTO orders (user_id, amount, status)
VALUES (42, 99.50, 'pending')
RETURNING *;
Обновить строки и вернуть новые значения
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, name, salary AS new_salary;
Удалить строки и вернуть удалённые данные
DELETE FROM orders
WHERE status = 'cancelled'
RETURNING id, user_id, amount;
Вставить родителя и детей через CTE
WITH new_user AS (
INSERT INTO users (email, name, country)
VALUES ('team@example.com', 'Team', 'ES')
RETURNING id
)
INSERT INTO orders (user_id, amount, status)
SELECT
id,
0,
'pending'
FROM new_user
RETURNING id AS order_id, user_id;
Отсортировать результат UPDATE
WITH bumped AS (
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, salary AS new_salary
)
SELECT
id,
new_salary
FROM bumped
ORDER BY id;
Посчитать количество изменённых строк
WITH updated AS (
UPDATE orders
SET status = 'archived'
WHERE created_at < now() - interval '1 year'
RETURNING id
)
SELECT count(*) AS updated_count
FROM updated;
Идемпотентная вставка с проверкой результата
INSERT INTO processed_events (event_id)
VALUES ('evt_123')
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
Upsert с возвратом итоговой строки
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada New')
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name
RETURNING id, email, name;
Частые ошибки
Делают второй SELECT за id
Менее удачно:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada');
SELECT id
FROM users
WHERE email = 'ada@example.com';
Лучше:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
RETURNING id;
Думают, что UPDATE RETURNING вернёт старые значения
Запрос:
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING salary;
вернёт новую зарплату, а не старую.
Если нужны старые и новые значения, подготовьте старые значения отдельно через CTE.
Ждут строку при ON CONFLICT DO NOTHING
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING
RETURNING id;
Если строка уже была, RETURNING будет пустым.
Это нормальное поведение.
Ожидают гарантированный порядок
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, salary;
Порядок результата не стоит считать гарантированным.
Если нужен порядок:
WITH updated AS (
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, salary
)
SELECT *
FROM updated
ORDER BY id;
Используют RETURNING * на больших операциях
UPDATE events
SET processed = true
WHERE processed = false
RETURNING *;
Если обновятся миллионы строк, клиенту придётся получить огромный результат.
Возвращайте только нужные поля или не используйте RETURNING, если результат не нужен.
Что важно запомнить
RETURNING позволяет получить данные строк, которые были затронуты INSERT, UPDATE или DELETE.
Пример:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
RETURNING id, created_at;
Главные правила:
RETURNING экономит второй запрос;
- работает с
INSERT, UPDATE, DELETE;
- возвращает реально затронутые строки;
- можно возвращать колонки, выражения и алиасы;
RETURNING * возвращает всю итоговую строку;
- в
UPDATE ... RETURNING значения уже новые;
- в
DELETE ... RETURNING возвращаются удалённые строки;
- порядок результата не гарантирован;
- если нужен порядок, используйте CTE и внешний
ORDER BY;
- при
ON CONFLICT DO NOTHING пропущенные строки не возвращаются;
- при
ON CONFLICT DO UPDATE возвращается обновлённая строка;
- не используйте
RETURNING * на больших операциях без необходимости.
Короткий вывод
RETURNING — это способ сразу получить результат записи в PostgreSQL.
Вставили пользователя:
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
RETURNING id;
Обновили зарплаты:
UPDATE employees
SET salary = salary * 1.05
WHERE dept = 'eng'
RETURNING id, salary AS new_salary;
Удалили строки:
DELETE FROM orders
WHERE status = 'cancelled'
RETURNING id, amount;
Главная мысль:
RETURNING убирает лишний SELECT после записи и возвращает именно те строки, которые команда реально вставила, обновила или удалила.
Это делает код проще, уменьшает количество запросов к базе и помогает строить атомарные цепочки через CTE.
В PostgreSQL после
INSERT,UPDATEилиDELETEможно сразу вернуть данные затронутых строк.Для этого есть клауза:
Она позволяет сделать запись в таблицу и тут же получить результат обратно, почти как в
SELECT.Например, вы вставляете пользователя и хотите сразу узнать его сгенерированный
id.Без
RETURNINGпришлось бы делать два запроса:INSERT INTO users (email, name) VALUES ('a@example.com', 'Ann'); SELECT id FROM users WHERE email = 'a@example.com';С
RETURNINGвсё делается одной командой:INSERT INTO users (email, name) VALUES ('a@example.com', 'Ann') RETURNING id;PostgreSQL вставит строку и сразу вернёт её
id.Это удобно, безопасно и экономит лишний поход в базу.
Зачем нужен RETURNING
Самый частый сценарий:
Например, есть таблица
users:CREATE TABLE users ( id bigserial PRIMARY KEY, email text NOT NULL UNIQUE, name text NOT NULL, country text, created_at timestamptz NOT NULL DEFAULT now() );Вставляем пользователя:
INSERT INTO users (email, name, country) VALUES ('a@example.com', 'Ann', 'DE') RETURNING id, created_at;Результат:
PostgreSQL вернул значения, которые появились после вставки:
id;created_atизDEFAULT now();Главная идея:
RETURNING работает не только с INSERT
RETURNINGможно использовать с разными DML-командами:То есть можно вернуть:
Примеры:
INSERT INTO users (email, name) VALUES ('a@example.com', 'Ann') RETURNING id, email;UPDATE users SET country = 'ES' WHERE email = 'a@example.com' RETURNING id, email, country;DELETE FROM users WHERE email = 'a@example.com' RETURNING id, email;Во всех трёх случаях PostgreSQL не просто выполняет команду, а ещё отдаёт результат.
INSERT RETURNING: получить id новой строки
Допустим, приложение создаёт пользователя.
INSERT INTO users (email, name, country) VALUES ('ada@example.com', 'Ada', 'GB') RETURNING id;Результат:
Теперь приложение сразу знает
idсозданного пользователя.Это лучше, чем делать второй запрос по email.
Почему?
Потому что:
Если после создания пользователя нужно создать связанные записи,
RETURNINGособенно полезен.RETURNING несколько колонок
Можно вернуть не только
id.Например:
INSERT INTO users (email, name, country) VALUES ('ada@example.com', 'Ada', 'GB') RETURNING id, email, name, country, created_at;Результат:
Это удобно, когда приложение хочет сразу получить готовую запись в том виде, в котором она лежит в базе.
RETURNING *
Если нужны все колонки, можно написать:
RETURNING *Пример:
INSERT INTO orders (user_id, amount, status) VALUES (42, 99.50, 'pending') RETURNING *;Результат может быть таким:
RETURNING *полезен, если:DEFAULT;bigserialили identity-колонка;Но в API и продакшн-коде часто лучше явно перечислять нужные поля:
Так запрос стабильнее и не зависит от будущих изменений схемы.
RETURNING с выражениями
В
RETURNINGможно писать не только названия колонок, но и выражения.Например, обновим сумму заказа на 10% и вернём новую сумму:
UPDATE orders SET amount = amount * 1.10 WHERE status = 'pending' RETURNING id, amount AS new_amount;Можно добавить вычисление:
UPDATE orders SET amount = amount * 1.10 WHERE status = 'pending' RETURNING id, amount AS new_amount, round(amount / 1.10, 2) AS approx_old_amount;Здесь важно: в
UPDATE ... RETURNINGколонкаamount— это уже новое значение после обновления.То есть если было
100, а стало110, тоRETURNING amountвернёт110.UPDATE RETURNING: вернуть обновлённые строки
Допустим, нужно поднять зарплату всем сотрудникам отдела
engна 5% и сразу вернуть новые значения.UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, name, salary AS new_salary;Результат:
Команда обновила строки и сразу вернула их.
Это удобно для:
RETURNING в UPDATE показывает новые значения
Это важная ловушка.
В запросе:
UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, salary;salaryвRETURNING— это уже зарплата после увеличения.Если вам нужно получить старую зарплату, одного простого
RETURNING salaryнедостаточно.Можно вычислить старое значение обратной операцией, если это безопасно:
UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, round(salary / 1.05, 2) AS old_salary, salary AS new_salary;Но такой подход подходит не всегда.
Более надёжный вариант — заранее сохранить старые значения в CTE.
WITH old_rows AS ( SELECT id, salary AS old_salary FROM employees WHERE dept = 'eng' ), updated AS ( UPDATE employees e SET salary = e.salary * 1.05 FROM old_rows old WHERE e.id = old.id RETURNING e.id, old.old_salary, e.salary AS new_salary ) SELECT id, old_salary, new_salary FROM updated ORDER BY id;Так можно получить честную пару:
DELETE RETURNING: вернуть удалённые строки
RETURNINGработает и сDELETE.Например, удалить отменённые заказы и вернуть, что именно удалили:
DELETE FROM orders WHERE status = 'cancelled' RETURNING id, user_id, amount, status;Результат:
Это удобно для аудита.
Без
RETURNINGпришлось бы сначала делать:SELECT ...а потом:
DELETE ...Между этими двумя командами данные могли бы измениться.
С
DELETE ... RETURNINGвы точно получаете строки, которые были удалены именно этой командой.RETURNING возвращает несколько строк
RETURNINGвозвращает не одну строку, а столько строк, сколько было реально затронуто.Например:
UPDATE orders SET status = 'archived' WHERE created_at < now() - interval '1 year' RETURNING id, status;Если обновилось 1000 заказов,
RETURNINGвернёт 1000 строк.То же самое с массовым
INSERT:INSERT INTO roles (code, title) VALUES ('admin', 'Administrator'), ('manager', 'Manager'), ('student', 'Student') RETURNING id, code;Результат:
RETURNING и порядок строк
Порядок строк в
RETURNINGне стоит считать гарантированным.Например:
UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, salary;Может вернуть строки не в том порядке, в котором вы ожидаете.
Если нужен стабильный порядок, оберните команду в CTE и отсортируйте внешним
SELECT.WITH bumped AS ( UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, salary AS new_salary ) SELECT id, new_salary FROM bumped ORDER BY id;Главное правило:
RETURNING внутри CTE
Самый мощный приём — использовать
RETURNINGвнутри CTE.Так можно выполнить несколько связанных действий одним SQL-запросом.
Например:
id;WITH new_user AS ( INSERT INTO users (email, name, country) VALUES ('team@example.com', 'Team', 'ES') RETURNING id ) INSERT INTO orders (user_id, amount, status) SELECT id, 0, 'pending' FROM new_user RETURNING id AS order_id, user_id;Что происходит:
Всё выполняется как один SQL-запрос.
Это удобно, потому что:
Пример: создать пользователя и настройки
Допустим, при создании пользователя нужно сразу создать дефолтные настройки.
WITH new_user AS ( INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') RETURNING id ) INSERT INTO user_settings (user_id, key, value) SELECT id, 'language', 'ru' FROM new_user RETURNING user_id, key, value;Результат:
Здесь
RETURNINGпомогает передатьidиз первой операции во вторую.Пример: перенести удалённые строки в архив
DELETE ... RETURNINGможно использовать для архивации.Например, перенести старые заказы в таблицу
orders_archive.WITH deleted_orders AS ( DELETE FROM orders WHERE created_at < now() - interval '2 years' RETURNING * ) INSERT INTO orders_archive SELECT * FROM deleted_orders;Что происходит:
DELETEудаляет старые заказы;RETURNING *возвращает удалённые строки;INSERTзаписывает их в архив.Это один SQL-запрос.
Но в реальном проекте с таким подходом нужно быть аккуратным:
Пример: посчитать, сколько строк обновилось
Можно обернуть
UPDATE ... RETURNINGв CTE и посчитать строки.WITH updated AS ( UPDATE orders SET status = 'archived' WHERE created_at < now() - interval '1 year' RETURNING id ) SELECT count(*) AS updated_count FROM updated;Результат:
Это удобно, когда приложению не нужны все обновлённые строки, а нужно только количество.
RETURNING и ON CONFLICT DO NOTHING
RETURNINGхорошо сочетается сON CONFLICT.Но важно понимать поведение.
Пример:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING RETURNING id, email;Если пользователь был вставлен,
RETURNINGвернёт строку.Если пользователь с таким email уже существовал,
DO NOTHINGпропустит вставку, иRETURNINGвернёт 0 строк.То есть:
Это полезно для идемпотентных операций.
Например, таблица обработанных событий:
CREATE TABLE processed_events ( event_id text PRIMARY KEY, processed_at timestamptz NOT NULL DEFAULT now() );Попытка отметить событие как обработанное:
INSERT INTO processed_events (event_id) VALUES ('evt_123') ON CONFLICT (event_id) DO NOTHING RETURNING event_id;Если
RETURNINGвернулevent_id, событие новое и его можно обрабатывать.Если результат пустой, событие уже было обработано раньше.
RETURNING и ON CONFLICT DO UPDATE
Если при конфликте вы не пропускаете строку, а обновляете её,
RETURNINGвернёт обновлённую строку.INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada New') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING id, email, name;Если пользователя не было — он вставится, и
RETURNINGвернёт вставленную строку.Если пользователь уже был — он обновится, и
RETURNINGвернёт обновлённую строку.Это отличие от
DO NOTHING.RETURNING не коммитит транзакцию
RETURNINGвозвращает значения сразу после выполнения команды, но это не значит, что транзакция уже зафиксирована.Например:
BEGIN; INSERT INTO users (email, name) VALUES ('a@example.com', 'Ann') RETURNING id; ROLLBACK;RETURNINGможет вернутьid, но послеROLLBACKстроки в таблице не будет.То есть
RETURNINGпоказывает результат внутри текущей транзакции.Фиксация изменений всё равно зависит от
COMMIT.RETURNING и триггеры / defaults
RETURNINGвозвращает итоговые значения строки после применения логики базы при записи.Например:
CREATE TABLE orders ( id bigserial PRIMARY KEY, amount numeric NOT NULL, status text NOT NULL DEFAULT 'pending', created_at timestamptz NOT NULL DEFAULT now() );Запрос:
INSERT INTO orders (amount) VALUES (99.90) RETURNING id, amount, status, created_at;вернёт не только
amount, который вы передали, но и:id;status;created_at.Если в базе есть триггеры, которые меняют строку при вставке или обновлении,
RETURNINGпозволяет приложению получить уже итоговый вариант строки.RETURNING и права доступа
Чтобы вернуть колонку через
RETURNING, у пользователя должны быть права на чтение этой колонки.Например:
INSERT INTO users (email, name) VALUES ('a@example.com', 'Ann') RETURNING id, email;нужны права не только на вставку, но и на чтение возвращаемых полей.
В обычных приложениях это редко становится сюрпризом, но в системах с тонкой настройкой прав стоит помнить:
Когда RETURNING особенно полезен
RETURNINGстоит использовать, когда нужно:idпосле вставки;ON CONFLICT DO NOTHING;Пример типичного API:
INSERT INTO users (email, name, country) VALUES ($1, $2, $3) RETURNING id, email, name, country, created_at;Приложение сразу получает объект созданного пользователя.
Когда RETURNING не нужен
RETURNINGне всегда нужен.Если вы делаете массовое обновление миллионов строк и не собираетесь читать результат, лучше не возвращать все строки.
Например:
UPDATE events SET processed = true WHERE processed = false;Если добавить:
RETURNING *PostgreSQL начнёт отдавать огромный результат клиенту.
Это может быть дорого по памяти, сети и времени.
Правило простое:
Лучше:
чем:
RETURNING *если вам нужен только идентификатор.
А если результат вообще не нужен — не используйте
RETURNING.MySQL: RETURNING обычно нет
В MySQL нет такого же универсального
RETURNING, как в PostgreSQL.Обычно после вставки автоинкрементного id используют:
INSERT INTO users (email, name, country) VALUES ('a@example.com', 'Ann', 'DE'); SELECT LAST_INSERT_ID();LAST_INSERT_ID()возвращает id, сгенерированный в текущем соединении.Это важно: он не должен перепутаться с вставками других соединений.
Но всё равно это отдельный запрос.
Для обновлений и удалений в MySQL обычно используют другие подходы:
SELECT, потомUPDATEилиDELETE;MariaDB в некоторых версиях поддерживает
RETURNINGдля части DML-команд, но это не то же самое, что универсальная привычная PostgreSQL-семантика. При переносе всегда проверяйте конкретную СУБД и версию.ClickHouse
В ClickHouse
RETURNINGв стиле PostgreSQL обычно не используется.ClickHouse — аналитическая колоночная СУБД, где вставки часто батчевые, а модель записи сильно отличается от PostgreSQL.
Если нужны ключи строк, их обычно:
То есть PostgreSQL-паттерн:
INSERT ... RETURNING idна ClickHouse напрямую обычно не переносится.
Практические шаблоны
Вставить пользователя и вернуть id
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') RETURNING id;Вставить пользователя и вернуть готовую строку
INSERT INTO users (email, name, country) VALUES ('ada@example.com', 'Ada', 'GB') RETURNING id, email, name, country, created_at;Вставить заказ и вернуть все поля
INSERT INTO orders (user_id, amount, status) VALUES (42, 99.50, 'pending') RETURNING *;Обновить строки и вернуть новые значения
UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, name, salary AS new_salary;Удалить строки и вернуть удалённые данные
DELETE FROM orders WHERE status = 'cancelled' RETURNING id, user_id, amount;Вставить родителя и детей через CTE
WITH new_user AS ( INSERT INTO users (email, name, country) VALUES ('team@example.com', 'Team', 'ES') RETURNING id ) INSERT INTO orders (user_id, amount, status) SELECT id, 0, 'pending' FROM new_user RETURNING id AS order_id, user_id;Отсортировать результат UPDATE
WITH bumped AS ( UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, salary AS new_salary ) SELECT id, new_salary FROM bumped ORDER BY id;Посчитать количество изменённых строк
WITH updated AS ( UPDATE orders SET status = 'archived' WHERE created_at < now() - interval '1 year' RETURNING id ) SELECT count(*) AS updated_count FROM updated;Идемпотентная вставка с проверкой результата
INSERT INTO processed_events (event_id) VALUES ('evt_123') ON CONFLICT (event_id) DO NOTHING RETURNING event_id;Upsert с возвратом итоговой строки
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada New') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING id, email, name;Частые ошибки
Делают второй SELECT за id
Менее удачно:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada'); SELECT id FROM users WHERE email = 'ada@example.com';Лучше:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') RETURNING id;Думают, что UPDATE RETURNING вернёт старые значения
Запрос:
UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING salary;вернёт новую зарплату, а не старую.
Если нужны старые и новые значения, подготовьте старые значения отдельно через CTE.
Ждут строку при ON CONFLICT DO NOTHING
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') ON CONFLICT (email) DO NOTHING RETURNING id;Если строка уже была,
RETURNINGбудет пустым.Это нормальное поведение.
Ожидают гарантированный порядок
UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, salary;Порядок результата не стоит считать гарантированным.
Если нужен порядок:
WITH updated AS ( UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, salary ) SELECT * FROM updated ORDER BY id;Используют RETURNING * на больших операциях
UPDATE events SET processed = true WHERE processed = false RETURNING *;Если обновятся миллионы строк, клиенту придётся получить огромный результат.
Возвращайте только нужные поля или не используйте
RETURNING, если результат не нужен.Что важно запомнить
RETURNINGпозволяет получить данные строк, которые были затронутыINSERT,UPDATEилиDELETE.Пример:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') RETURNING id, created_at;Главные правила:
RETURNINGэкономит второй запрос;INSERT,UPDATE,DELETE;RETURNING *возвращает всю итоговую строку;UPDATE ... RETURNINGзначения уже новые;DELETE ... RETURNINGвозвращаются удалённые строки;ORDER BY;ON CONFLICT DO NOTHINGпропущенные строки не возвращаются;ON CONFLICT DO UPDATEвозвращается обновлённая строка;RETURNING *на больших операциях без необходимости.Короткий вывод
RETURNING— это способ сразу получить результат записи в PostgreSQL.Вставили пользователя:
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada') RETURNING id;Обновили зарплаты:
UPDATE employees SET salary = salary * 1.05 WHERE dept = 'eng' RETURNING id, salary AS new_salary;Удалили строки:
DELETE FROM orders WHERE status = 'cancelled' RETURNING id, amount;Главная мысль:
Это делает код проще, уменьшает количество запросов к базе и помогает строить атомарные цепочки через CTE.