sqlpostgresqlreturninginsert

RETURNING in PostgreSQL: Get IDs and Changed Rows Without a Second Round-Trip

How the RETURNING clause on INSERT/UPDATE/DELETE hands back generated ids and changed columns in one round-trip, with no extra SELECT.

9 мин четенеReferencesql · postgresql · returning · insert · cte · mysql
Тази статия в момента е на руски — английският превод е в процес на изготвяне.

В 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-запросом.

Например:

  1. вставить пользователя;
  2. получить его id;
  3. создать для него первый заказ.
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;

Что происходит:

  1. DELETE удаляет старые заказы;
  2. RETURNING * возвращает удалённые строки;
  3. внешний 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.

Упражнявай се на реални задачи

Решавай задачи в SQL тренажора с незабавно оценяване и подсказки.

Отвори тренажора