Все материалы
Продуктовая аналитикагайдстарт

UPDATE в SQL: синтаксис, примеры и обновление по другой таблице

UPDATE в SQL и PostgreSQL: обновление одного и нескольких полей, CASE, UPDATE … FROM по другой таблице, RETURNING и безопасный порядок через транзакцию.

КейсПрактика25 сентября 2026 г.8 мин

Маркетинг переименовал канал: всё, что раньше называлось 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.

Общий вид команды в PostgreSQL
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 1015 (PostgreSQL)
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.

Что вернул RETURNING
subscription_iduser_idplanmonthly_pricestatus
421pro29active
Два поля одной командой: UPDATE 1
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 743
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.

Заполнение колонки по таблице соответствия: UPDATE 4613 (PostgreSQL)
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: если он что-то вернул, соединять с сырыми платежами нельзя.

Верно: одна строка на пользователя в подзапросе, 0 расхождений
-- неверно: 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», решите явно, входят ли в это множество пустые значения.

Условие и число строк, которые оно затрагивает (users, 4613 строк)
WHEREСтрокПочему
country = null0сравнение с NULL никогда не true
country is null55правильная проверка на пустоту
country <> 'RU'1843строки с NULL не проходят
country is distinct from 'RU'1898NULL считается отличным от RU

Чем UPDATE отличается от UPSERT

UPDATE меняет только существующие строки: если под условие ничего не попало, ответ будет UPDATE 0, и новая строка не появится. Когда нужно «обновить, если есть, иначе вставить» — например, при перезагрузке дневной витрины, — используйте INSERT … ON CONFLICT DO UPDATE. Он разобран в отдельной статье про upsert.

Чеклист перед UPDATE

Пять вопросов, на которые стоит ответить до запуска команды на рабочей таблице.

  • Есть ли SELECT с тем же WHERE и совпадает ли его результат с ожиданием?
  • Открыта ли транзакция, чтобы при ошибке сделать ROLLBACK?
  • Даёт ли источник в FROM не больше одной строки на ключ соединения?
  • Нет ли в CASE строк, для которых не предусмотрена ветка?
  • Как условие обращается с NULL в фильтруемых колонках?

Что почитать дальше

Проверьте в песочнице, какие строки затронул бы каждый UPDATE из статьи: запустите их SELECT-версии и сравните числа. Сами изменения выполняйте на своей копии базы.

Продолжить чтение
Вся библиотека