UPDATE в SQL: синтаксис, WHERE, UPDATE с JOIN и защита от ошибок

UPDATE в SQL: синтаксис, WHERE, UPDATE с JOIN и защита от ошибок Полезное

UPDATE — это оператор SQL, который меняет значения столбцов в уже существующих строках таблицы. Новые строки добавляет INSERT, удаляет — DELETE, а UPDATE только переписывает значения в строках, отобранных условием WHERE. Без WHERE он изменит все строки таблицы.

Ниже — синтаксис, обновление по другой таблице (здесь диалекты расходятся сильнее всего), RETURNING и защита от случайного обновления всей таблицы. Примеры запускал 23.09.2026 в SQLite 3.51, PostgreSQL 18.6 и MySQL 8.4; T-SQL-варианты проверял на Azure SQL Edge (движок SQL Server 2019), не на SQL Server 2022.

Минимальный рабочий пример

Учебная база — книжный магазин: таблица books (остатки и цены) и sales (продажи за день). Скрипт запускается в SQLite как есть, например sqlite3 -header -column shop.db < shop.sql.

CREATE TABLE books (
  id    INTEGER PRIMARY KEY,
  title TEXT    NOT NULL,
  stock INTEGER NOT NULL CHECK (stock >= 0),
  price INTEGER NOT NULL CHECK (price > 0)
);
CREATE TABLE sales (
  book_id INTEGER NOT NULL REFERENCES books(id),
  qty     INTEGER NOT NULL CHECK (qty > 0)
);
INSERT INTO books VALUES
  (1, 'Мастер и Маргарита', 12, 650),
  (2, 'Анна Каренина',       3, 540),
  (3, 'Мертвые души',        2, 480),
  (4, 'Илиада',              7, 720);
INSERT INTO sales VALUES (2, 2), (3, 1), (2, 1);

UPDATE books
SET price = 590
WHERE id = 2;

SELECT id, title, price FROM books;

Изменилась одна строка — цена «Анны Карениной»:

id  title               price
--  ------------------  -----
1   Мастер и Маргарита  650
2   Анна Каренина       590
3   Мертвые души        480
4   Илиада              720

Цены — целые рубли: в SQLite DECIMAL не дает точной дробной арифметики. CHECK пригодится ниже.

Каждый следующий пример запускается на исходных данных: пересоздайте базу скриптом выше без последнего UPDATE, иначе числа в статье не совпадут с вашими.

Синтаксис: SET и WHERE

UPDATE таблица
SET столбец1 = выражение1,
    столбец2 = выражение2
WHERE условие;

SET задает, какие столбцы и на что менять (константа, выражение от текущих значений, CASE, подзапрос), WHERE — какие строки. Условие то же, что в SELECT.

Скидка 15% и пометка для книг с остатком меньше 5 штук:

UPDATE books
SET price = price * 85 / 100,
    title = title || ' (акция)'
WHERE stock < 5;

Обновятся две строки: у «Анны Карениной» цена станет 459, у «Мертвых душ» — 408 (здесь оба результата целые; при дробном результате SQLite, PostgreSQL и SQL Server отбросят дробную часть целочисленного деления, а в MySQL / дает дробное число, и при записи в INTEGER оно округлится: из цены 7 получится 6, а не 5). Конкатенация || работает в SQLite и PostgreSQL; в MySQL по умолчанию || — это логическое ИЛИ, там нужен CONCAT(title, ' (акция)').

Порядок присваиваний в SET зависит от СУБД

Запрос UPDATE books SET stock = stock + 1, price = stock WHERE id = 4; на одной и той же строке (stock = 7) дает разный результат:

СУБД stock price Почему
PostgreSQL 18, SQLite 3.51 8 7 Все правые части считаются по старым значениям строки
MySQL 8.4 8 8 Присваивания в одной таблице выполняются слева направо, price видит уже новый stock

Для переносимого кода не ссылайтесь в SET на столбец, меняемый тем же запросом.

Главный риск: UPDATE без WHERE

Неверно — забыли условие:

UPDATE books SET price = 500;

Результат: цена 500 у всех четырех книг, без подтверждения. Исправление — транзакция и проверка числа строк до фиксации:

BEGIN;
UPDATE books SET price = 500;
SELECT changes() AS changed;   -- SQLite; в PostgreSQL число строк пишет сам psql: UPDATE 4
ROLLBACK;                      -- ожидали 1 строку, получили 4 - откатываем

После ROLLBACK цены вернутся к 650, 540, 480 и 720. Рабочая привычка для ручных правок: BEGIN -> SELECT с тем же WHERE -> UPDATE -> сверка числа строк -> COMMIT или ROLLBACK. Проверочный SELECT стоит внутри транзакции, а не перед ней.

Граница changes(): функция SQLite считает только строки, которые изменил непосредственно последний оператор. Изменения от триггеров, каскадов внешних ключей и REPLACE в это число не входят — их проверяйте отдельно.

Если данные меняют одновременно

Транзакция из примера выше защищает от собственной ошибки: запрос можно откатить. Но она не гарантирует, что проверенные SELECT-ом строки не изменит кто-то другой, пока вы решаете, делать ли UPDATE. В PostgreSQL на уровне изоляции по умолчанию (Read Committed) даже два SELECT внутри одной транзакции могут увидеть разные данные, если между ними другая транзакция зафиксировала изменения. Поэтому уровней защиты три:

  1. Учебный откат — BEGIN, проверка, UPDATE, сверка числа строк, ROLLBACK при расхождении. Достаточно для ручных правок, когда с таблицей никто больше не работает.
  2. Блокировка выбранных строк — SELECT ... FOR UPDATE внутри транзакции. Другие транзакции, которые попытаются изменить или так же заблокировать эти строки, будут ждать COMMIT или ROLLBACK:
BEGIN;
SELECT id, stock FROM books WHERE id = 3 FOR UPDATE;  -- строка 3 заблокирована до конца транзакции
UPDATE books SET stock = stock - 1 WHERE id = 3;
COMMIT;

Так работают PostgreSQL и MySQL (InnoDB). В SQL Server тот же эффект дает табличная подсказка WITH (UPDLOCK) в SELECT. В SQLite FOR UPDATE нет: там запись блокирует всю базу, и транзакцию для правок открывают как BEGIN IMMEDIATE.

  1. Условное обновление — проверку кладут прямо в WHERE того же UPDATE, и отдельный SELECT не нужен:
UPDATE books SET stock = stock - 1
WHERE id = 3 AND stock >= 1;   -- спишем, только если есть что списать

Если остаток к этому моменту уже 0, запрос обновит 0 строк — это и есть сигнал, что данные изменились. Приложение сверяет число обновленных строк с ожидаемым (1) и при расхождении перечитывает данные. Тот же прием с отдельным столбцом-версией (WHERE id = :id AND version = :v, в SET — version = version + 1) называют оптимистичной блокировкой.

Граница: в PostgreSQL, MySQL и SQLite клиент по умолчанию работает в режиме автокоммита — без явного BEGIN каждый UPDATE фиксируется сразу, и откатить его уже нельзя.

В MySQL есть дополнительный предохранитель — режим безопасных обновлений:

SET SESSION sql_safe_updates = 1;
UPDATE books SET price = 500;
ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column.

С WHERE id = 1 тот же запрос проходит. Клиент mysql включает его флагом --safe-updates. В PostgreSQL и SQLite встроенного аналога нет — только транзакции и дисциплина.

Второй предохранитель — ограничения схемы. Попытка продать больше, чем есть на складе:

UPDATE books SET stock = stock - 5 WHERE id = 3;

SQLite отклонит запрос с сообщением CHECK constraint failed: stock >= 0, остаток «Мертвых душ» останется 2. Без CHECK в таблице просто появился бы отрицательный остаток.

Обновление по данным другой таблицы

Задача: списать со склада продажи из sales.

Вариант 1: коррелированный подзапрос (переносимый)

Неверно:

UPDATE books
SET stock = stock - (SELECT SUM(qty) FROM sales
                     WHERE sales.book_id = books.id);

Результат: NOT NULL constraint failed: books.stock. Для книг без продаж подзапрос возвращает NULL, а stock - NULL тоже NULL. Без NOT NULL остатки двух книг молча стали бы NULL. Исправление — ограничить строки:

UPDATE books
SET stock = stock - (SELECT SUM(qty) FROM sales
                     WHERE sales.book_id = books.id)
WHERE id IN (SELECT book_id FROM sales);

Остатки: 12, 0, 1, 7 — верно (у «Анны Карениной» было две продажи: 2 + 1). Такой запрос работает во всех четырех СУБД.

Вариант 2: UPDATE с JOIN — синтаксис по диалектам

Единого синтаксиса «UPDATE с JOIN» в SQL нет, у каждой СУБД свой:

СУБД Форма
PostgreSQL UPDATE t SET ... FROM другая WHERE условие связи
SQLite (с версии 3.33.0) Как в PostgreSQL: UPDATE t SET ... FROM ... WHERE ...
MySQL, MariaDB UPDATE t JOIN другая ON ... SET ...
SQL Server UPDATE t SET ... FROM t JOIN другая ON ...

Частая ошибка (PostgreSQL):

UPDATE books
SET stock = books.stock - s.qty
FROM sales AS s
WHERE s.book_id = books.id;

Результат: у книги 2 остаток 1 в PostgreSQL 18 и 2 в SQLite 3.51 (MySQL 8.4 на аналогичном JOIN дал 1), а правильно 0. Если строке books соответствуют несколько строк sales, она обновляется один раз по одной из пар, какой — не определено. Ошибки нет, данные тихо испорчены.

Исправление — сначала агрегировать, чтобы на одну книгу приходилась одна строка. PostgreSQL и SQLite:

UPDATE books AS b
SET stock = b.stock - s.sold
FROM (SELECT book_id, SUM(qty) AS sold
      FROM sales GROUP BY book_id) AS s
WHERE s.book_id = b.id;

MySQL:

UPDATE books AS b
JOIN (SELECT book_id, SUM(qty) AS sold
      FROM sales GROUP BY book_id) AS s ON s.book_id = b.id
SET b.stock = b.stock - s.sold;

SQL Server (T-SQL):

UPDATE b
SET b.stock = b.stock - s.sold
FROM books AS b
JOIN (SELECT book_id, SUM(qty) AS sold
      FROM sales GROUP BY book_id) AS s ON s.book_id = b.id;

Во всех случаях остатки 12, 0, 1, 7. В MySQL UPDATE ... JOIN можно менять столбцы сразу нескольких таблиц, но ORDER BY и LIMIT в многотабличной форме запрещены — они есть только у однотабличного UPDATE в MySQL.

RETURNING: увидеть, что изменилось

RETURNING возвращает измененные строки тем же запросом. Пример для PostgreSQL 18:

UPDATE books AS b
SET stock = b.stock - s.sold
FROM (SELECT book_id, SUM(qty) AS sold
      FROM sales GROUP BY book_id) AS s
WHERE s.book_id = b.id
RETURNING b.id, old.stock AS was, new.stock AS now;
 id | was | now
----+-----+-----
  2 |   3 |   0
  3 |   2 |   1
(2 rows)

Обращение к old и new в RETURNING появилось в PostgreSQL 18; в более ранних версиях RETURNING отдает только новые значения. Поддержка по СУБД:

СУБД Как получить измененные строки
PostgreSQL RETURNING (с версии 18 — также old.*/new.*)
SQLite RETURNING с версии 3.35.0, только новые значения
SQL Server OUTPUT inserted.stock, deleted.stock
MySQL 8.4 Нет, нужен отдельный SELECT в той же транзакции

Если не получилось

  • Обновлено 0 строк. WHERE ничего не нашел: проверьте его отдельным SELECT. Частая причина — сравнение с NULL: col = NULL не истинно никогда, нужно col IS NULL. В MySQL 0 бывает и тогда, когда строка найдена, но новое значение совпало со старым: клиент mysql пишет 0 rows affected (Rows matched: 1 Changed: 0). Драйвер, подключенный с флагом CLIENT_FOUND_ROWS, вернет число найденных строк, а не измененных.
  • Обновились все строки. Нет WHERE или условие всегда истинно. Транзакция еще открыта — ROLLBACK. Если UPDATE уже зафиксирован, откатить его нельзя: остановите дальнейшие записи и используйте штатное восстановление СУБД — резервную копию вместе с журналом транзакций до нужного момента (point-in-time recovery). Восстановление из одной полной копии откатит и все корректные изменения после нее, поэтому сначала отработайте процедуру на копии базы.
  • Ошибка синтаксиса у FROM или JOIN. Синтаксис другого диалекта: UPDATE ... JOIN не поймут PostgreSQL и SQLite, UPDATE ... FROM — MySQL. В SQLite старше 3.33.0 FROM не поддерживается — используйте подзапрос.
  • Число получилось не то. Несколько строк-источников на одну целевую — агрегируйте до UPDATE.
  • Запрос завис. Строку держит другая незавершенная транзакция — завершите ее.

Выводы

  • UPDATE меняет значения в существующих строках; какие строки — решает WHERE, без него меняется вся таблица.
  • Безопасный порядок для ручной правки: BEGIN, SELECT с тем же условием, UPDATE, сверка числа строк, COMMIT или ROLLBACK. Если данные меняют параллельно — блокировка строк (SELECT ... FOR UPDATE) или условный UPDATE с проверкой числа обновленных строк.
  • UPDATE с JOIN пишется по-разному в каждом диалекте; переносимый вариант — коррелированный подзапрос с ограничивающим WHERE.
  • При нескольких строках-источниках на одну целевую строку UPDATE с соединением тихо берет одну из них — агрегируйте заранее.
  • RETURNING есть в PostgreSQL и SQLite 3.35+, в SQL Server его роль играет OUTPUT, в MySQL аналога нет.

Где применяется / связь с практикой

UPDATE нужен везде, где данные живут: смена статусов заказов, списание остатков, массовые исправления после миграции. На массовых исправлениях данные и ломают чаще всего, поэтому привычка к транзакциям ценится не меньше синтаксиса.

Освойте тему на практике

Системно разобрать SQL — от SELECT и соединений до транзакций, индексов и различий диалектов — можно на курсе Otus по SQL. Бесплатно познакомиться с преподавателями и форматом можно на открытых уроках Otus.

FAQ

Можно ли обновить строки в определенном порядке или только первые N?
В MySQL однотабличный UPDATE поддерживает ORDER BY и LIMIT. В SQL Server есть UPDATE TOP (n), но какие n строк обновятся, не гарантировано, поэтому порядок задают так же, как в PostgreSQL. В PostgreSQL ORDER BY/LIMIT в UPDATE нет — отбирают ключи подзапросом с ORDER BY ... LIMIT и обновляют по ним; в SQLite ORDER BY/LIMIT в UPDATE работают только в сборке с опцией SQLITE_ENABLE_UPDATE_DELETE_LIMIT (системный sqlite3 3.51 в macOS ее включает, другие сборки — не всегда).

Можно ли установить значение по умолчанию?
Да: SET столбец = DEFAULT подставит значение из определения столбца. Это работает в PostgreSQL, MySQL и SQL Server; SQLite ключевое слово DEFAULT в SET не поддерживает.

Чем UPDATE отличается от UPSERT?
UPDATE не создает строк. UPSERT вставляет строку или обновляет ее при конфликте ключа: ON CONFLICT DO UPDATE в PostgreSQL и SQLite, ON DUPLICATE KEY UPDATE в MySQL, MERGE в SQL Server.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)