UPDATE в SQL: синтаксис, примеры и обновление по другой таблице
UPDATE в SQL и PostgreSQL: обновление одного и нескольких полей, CASE, UPDATE … FROM по другой таблице, RETURNING и безопасный порядок через транзакцию.
Содержание статьи
Маркетинг переименовал канал: всё, что раньше называлось paid_search, теперь search_ads. В справочнике пользователей 4613 строк, и 1015 из них нужно поправить так, чтобы отчёты по каналам не развалились на два названия. Это работа для UPDATE. Команда короткая, но ошибается она молча: без WHERE она перепишет всю таблицу и сообщит об этом одной строкой UPDATE 4613.
Что делает UPDATE в SQL
UPDATE — команда SQL, которая меняет значения в уже существующих строках таблицы. Она выбирает строки условием WHERE, присваивает колонкам из SET новые значения или выражения и возвращает число изменённых строк. Строки не добавляются и не удаляются: меняется только содержимое указанных колонок, остальные остаются как были.
Все примеры ниже выполнены на копии учебной базы «Маяк» в PostgreSQL 14. Песочница SQL-курса принимает только SELECT и WITH, поэтому сами UPDATE запускайте на своём PostgreSQL: локальном или в Docker. Для каждого изменения в статье есть SELECT, который показывает, какие строки оно затронет, — его можно выполнить в песочнице.
Синтаксис UPDATE
Порядок частей фиксирован: таблица, SET со списком присваиваний через запятую, затем необязательные FROM, WHERE и RETURNING. Справа от знака равенства может стоять константа, выражение от старых значений строки, подзапрос или колонка из таблицы в FROM.
update table_name [as alias]
set column1 = expression1,
column2 = expression2
[from other_table] -- расширение PostgreSQL
[where condition]
[returning columns];Как обновить одну колонку
Вернёмся к каналу. Сначала узнайте, сколько строк подходит под условие: select count(*) from users where channel = 'paid_search' возвращает 1015. Затем та же фильтрация уходит в UPDATE.
PostgreSQL отвечает UPDATE 1015. Совпадение с предпросмотром — главный признак, что условие написано верно. Если бы ответ был 0, значит, в значении опечатка или лишний пробел; если 4613 — WHERE не сработал.
update users
set channel = 'search_ads'
where channel = 'paid_search';Как обновить несколько полей сразу
Колонки в SET перечисляются через запятую. Пользователь с подпиской 4 перешёл с тарифа basic на pro: меняем план и цену одной командой. RETURNING в конце показывает строку уже после изменения, отдельный SELECT не нужен.
Все выражения в SET вычисляются по старым значениям строки. Если написать set plan = 'pro', monthly_price = case when plan = 'pro' then 29 end, CASE увидит прежний план basic, а не только что присвоенный pro, и цена станет NULL.
| subscription_id | user_id | plan | monthly_price | status |
|---|---|---|---|---|
| 4 | 21 | pro | 29 | active |
update subscriptions
set plan = 'pro',
monthly_price = 29
where subscription_id = 4
returning subscription_id, user_id, plan, monthly_price, status;Что будет, если выполнить UPDATE без WHERE
Изменятся все строки таблицы. База не спросит подтверждения и не выдаст предупреждения. В таблице подписок три статуса: 515 active, 169 churned и 228 past_due. Команда update subscriptions set status = 'active' отвечает UPDATE 912, и после неё все 912 подписок активны. Отток исчез из отчёта за одну секунду.
Чаще всего так случается, когда в редакторе выделили и запустили только первые две строки запроса, а строка с WHERE осталась за выделением. Защиты две: запускать изменения внутри транзакции и сверять число из ответа с ожидаемым. Порядок действий разобран ниже.
PostgreSQL пишет UPDATE N, где N — сколько строк изменено. Если N равно размеру таблицы, а вы меняли часть строк, делайте ROLLBACK сразу, пока транзакция открыта.
UPDATE с выражением и CASE
Справа от = можно ссылаться на текущее значение колонки. Повышение цены тарифа team на 10%: update subscriptions set monthly_price = monthly_price * 1.1 where plan = 'team' — UPDATE 102. В учебной базе цена хранится как double precision, и после умножения в ней лежит 42.900000000000006. Для денег храните суммы в numeric или округляйте явно. Учтите, что round(monthly_price * 1.1, 2) на double precision падает с ERROR: function round(double precision, integer) does not exist; рабочий вариант — round((monthly_price * 1.1)::numeric, 2), он записывает 42.9.
Когда новое значение зависит от категории, используйте CASE: одна команда вместо трёх UPDATE по тарифам. Ниже новые цены получают только неотменённые подписки — UPDATE 743, а 169 оттёкших сохраняют старую цену.
У CASE без ELSE есть ловушка. Если убрать WHERE и забыть ветку для team, выражение для этих строк вернёт NULL, и 102 подписки останутся без цены. Добавляйте else monthly_price, чтобы непредусмотренные строки сохраняли своё значение.
update subscriptions
set monthly_price = case plan
when 'basic' then 21
when 'pro' then 32
when 'team' then 43
else monthly_price
end
where status <> 'churned';UPDATE … FROM: как обновить данные из другой таблицы
В PostgreSQL нет синтаксиса update … join, привычного по MySQL. Запрос update users u join channel_map m on … падает с ERROR: syntax error at or near "join". Вместо этого вторая таблица перечисляется в FROM, а условие соединения пишется в WHERE.
Допустим, аналитикам нужна колонка channel_group: платные каналы против бесплатных. Соответствие хранится в маленькой таблице channel_map из четырёх строк. Колонку добавляет alter table users add column channel_group text, а заполняет UPDATE с FROM: UPDATE 4613, 2784 пользователя в группе free и 1829 в paid.
Целевую таблицу не нужно повторять в FROM. Если написать from users as u join channel_map as m on m.channel = u.channel, PostgreSQL воспримет u как второй, независимый экземпляр таблицы. Условия, связывающего его с обновляемой строкой, нет, и каждая строка получает значение от случайной пары. В нашем прогоне все 4613 пользователей, включая платные каналы, получили free.
create table channel_map (channel text, channel_group text);
insert into channel_map values
('organic', 'free'), ('referral', 'free'),
('paid_search', 'paid'), ('partner', 'paid');
update users as u
set channel_group = m.channel_group
from channel_map as m
where m.channel = u.channel;Ловушка UPDATE FROM: одна строка и несколько совпадений
Задача: записать в подписку дату последней оплаты. В таблицу добавили колонку last_paid_at date, и первое, что приходит в голову, — соединить подписки с платежами по user_id. Команда отрабатывает с ответом UPDATE 912 и без ошибок.
Проблема в том, что у 290 пользователей больше одной оплаты. Когда строке целевой таблицы соответствует несколько строк из FROM, PostgreSQL обновляет её один раз значением из любой из них — какой именно, не определено. Сверка с max(paid_at) показала, что в нашем прогоне у 148 подписок записана не последняя дата. При следующем запуске число может быть другим.
Правило простое: источник в FROM должен давать не больше одной строки на ключ соединения. Сначала агрегируйте, потом обновляйте. Проверить источник заранее можно запросом select user_id, count(*) from payments group by user_id having count(*) > 1: если он что-то вернул, соединять с сырыми платежами нельзя.
-- неверно: update subscriptions as s set last_paid_at = p.paid_at
-- from payments as p where p.user_id = s.user_id;
update subscriptions as s
set last_paid_at = lp.last_paid
from (
select user_id, max(paid_at) as last_paid
from payments
group by user_id
) as lp
where lp.user_id = s.user_id;Безопасный порядок: SELECT, BEGIN, UPDATE, проверка, COMMIT
Реальная задача из той же базы: пометить как past_due активные подписки, у которых последняя оплата была больше 35 дней назад относительно 30 августа. Работайте в пять шагов.
Первый шаг — SELECT с тем же соединением и тем же WHERE, что будет в UPDATE. Он возвращает 165, и этот запрос можно выполнить в песочнице курса. Второй — begin. Третий — сам UPDATE; ответ UPDATE 165 совпадает с предпросмотром. Четвёртый — проверка результата: теперь статусов active 350, churned 169, past_due 393, то есть 228 + 165. Пятый — commit, если всё сходится, или rollback, если нет.
Пока транзакция не зафиксирована, изменения видны только в вашей сессии, а ROLLBACK возвращает таблицу в исходное состояние. Не оставляйте такую транзакцию открытой надолго: обновлённые строки заблокированы для других, кто пытается их изменить.
select count(*) as will_change -- 165
from subscriptions as s
join (
select user_id, max(paid_at) as last_paid
from payments
group by user_id
) as lp on lp.user_id = s.user_id
where s.status = 'active'
and lp.last_paid < date '2026-08-30' - 35;
begin;
update subscriptions as s
set status = 'past_due'
from (
select user_id, max(paid_at) as last_paid
from payments
group by user_id
) as lp
where lp.user_id = s.user_id
and s.status = 'active'
and lp.last_paid < date '2026-08-30' - 35; -- UPDATE 165
select status, count(*) from subscriptions group by status;
commit; -- или rollback;Почему UPDATE не находит строки с NULL
У 55 пользователей не указана страна. Запрос update users set country = 'unknown' where country = null отвечает UPDATE 0: сравнение с NULL через = даёт не true, а «неизвестно», и строка не проходит фильтр. Правильное условие — where country is null, оно даёт UPDATE 55.
Зеркальная ошибка в отрицании. where country <> 'RU' отбирает 1843 строки, а where country is distinct from 'RU' — 1898. Разница — те же 55 строк без страны. Если UPDATE должен затронуть «всех, кроме RU», решите явно, входят ли в это множество пустые значения.
| WHERE | Строк | Почему |
|---|---|---|
| country = null | 0 | сравнение с NULL никогда не true |
| country is null | 55 | правильная проверка на пустоту |
| country <> 'RU' | 1843 | строки с NULL не проходят |
| country is distinct from 'RU' | 1898 | NULL считается отличным от RU |
Чем UPDATE отличается от UPSERT
UPDATE меняет только существующие строки: если под условие ничего не попало, ответ будет UPDATE 0, и новая строка не появится. Когда нужно «обновить, если есть, иначе вставить» — например, при перезагрузке дневной витрины, — используйте INSERT … ON CONFLICT DO UPDATE. Он разобран в отдельной статье про upsert.
Чеклист перед UPDATE
Пять вопросов, на которые стоит ответить до запуска команды на рабочей таблице.
- Есть ли SELECT с тем же WHERE и совпадает ли его результат с ожиданием?
- Открыта ли транзакция, чтобы при ошибке сделать ROLLBACK?
- Даёт ли источник в FROM не больше одной строки на ключ соединения?
- Нет ли в CASE строк, для которых не предусмотрена ветка?
- Как условие обращается с NULL в фильтруемых колонках?
Что почитать дальше
Проверьте в песочнице, какие строки затронул бы каждый UPDATE из статьи: запустите их SELECT-версии и сравните числа. Сами изменения выполняйте на своей копии базы.
Материалы по теме

INSERT INTO в SQL: вставка строк, INSERT SELECT и RETURNING
INSERT INTO в SQL и PostgreSQL: вставка нескольких строк, DEFAULT и identity, INSERT … SELECT для витрины, RETURNING и частые ошибки.

DELETE в SQL: как удалить строки и не потерять лишнее
DELETE в SQL и PostgreSQL: удаление по условию, DELETE … USING, удаление дублей, RETURNING, разница DELETE, TRUNCATE и DROP, внешние ключи.

CREATE TABLE в SQL: как создать таблицу, типы и ограничения
CREATE TABLE в SQL: как создать таблицу в PostgreSQL, выбрать типы, задать PRIMARY KEY, NOT NULL, CHECK и внешний ключ, CREATE TABLE AS.