18. UPDATE
🧭 Введение: почему UPDATE требует дисциплины
UPDATE меняет уже существующие данные, поэтому ошибка здесь обычно дороже, чем в INSERT.Если неверно задать условие, можно массово испортить рабочие записи за один запрос.
В этой лекции разберём практику, которая нужна в проде:
- базовый
UPDATEсWHERE; - массовые обновления и их риски;
UPDATEсJOIN(обзорно, на уровне рабочего паттерна);- типичную аварийную ошибку
UPDATEбезWHERE.
💡 Совет:
Перед
UPDATE всегда проверяйте условие через SELECT, чтобы видеть, какие строки реально затронутся.✅ Вывод:
Надёжный
UPDATE начинается не с SET, а с точного и проверенного WHERE.⚠️ Проблема -> решение
Типичная проблема:
- обновляют данные без точного фильтра;
- не оценивают объём затрагиваемых строк;
- смешивают бизнес-логику и «быстрые правки» в одном запросе.
Последствия:
- массовая порча данных;
- долгие блокировки;
- сложный rollback и ручное восстановление.
Решение:
- сначала
SELECTс тем жеWHERE; - в важных случаях работать в транзакции;
- для больших объёмов обновлять батчами.
🟢 Если совсем просто:
UPDATE безопасен, когда вы точно знаете какие строки изменяются и сколько их.🎯 Как понять, что этап прошёл успешно:
Изменились только ожидаемые записи, и их количество совпало с планом.
🛠️ Чем помогает и как работает
UPDATE нужен в каждом CRUD-проекте:- изменение профиля пользователя;
- смена статуса заказа;
- массовая деактивация старых сущностей;
- синхронизация денормализованных полей.
🟢 Если совсем просто:
UPDATE изменяет значения в уже существующих строках.🎯 Как понять, что этап прошёл успешно:
Вы умеете безопасно делать точечные и массовые обновления без побочных эффектов.
Чем помогает:
- позволяет исправлять и актуализировать данные;
- поддерживает рабочие состояния в бизнес-процессах;
- даёт массовые изменения без цикла в приложении.
Как это работает:
- Шаг 1: выбираем таблицу и поля в
SET; - Шаг 2: задаём точное условие в
WHERE; - Шаг 3: проверяем набор через
SELECT; - Шаг 4: выполняем
UPDATE; - Шаг 5: проверяем число затронутых строк и результат.
✅ Вывод:
Сильный
UPDATE = корректный фильтр + контроль объёма + проверка результата.📚 Ключевые термины (простыми словами)
Перед практикой синхронизируем словарь.
🟢 Если совсем просто:
В
UPDATE главное понимать: что меняем, где меняем и сколько строк меняем.🎯 Как понять, что этап прошёл успешно:
Вы уверенно различаете точечное, массовое и join-обновление.
- Target rows — строки, которые попадут под
WHERE. - SET clause — список полей и новых значений.
- Predicate — условие фильтрации (
WHERE). - Mass update — обновление большого числа строк одним запросом.
- UPDATE ... FROM — PostgreSQL-паттерн обновления через присоединённую таблицу.
- Touched rows — число строк, которые запрос затронул/посчитал как обновлённые.
- Changed rows — строки, где значение фактически стало другим.
- Rollback — откат изменений транзакции.
✅ Вывод:
Эти термины закрывают базу для безопасной работы с
UPDATE.✏️ 1. UPDATE с WHERE: базовый и обязательный паттерн
Это фундамент: меняем только нужные строки и только нужные поля.
🟢 Если совсем просто:
WHERE в UPDATE защищает данные от случайного массового изменения.🎯 Как понять, что этап прошёл успешно:
Запрос изменил именно те строки, которые вы ожидали.
Назначение:
Точечно обновить состояние конкретной сущности.
Простыми словами:
Сначала выбираем кого обновлять (
WHERE), потом что менять (SET).Для новичка:
Никогда не начинайте
UPDATE с написания SET; начинайте с проверки WHERE через SELECT.Аналогия:
Это как редактировать карточку одного клиента, а не всю таблицу клиентов.
Пример:
UPDATE usersSET full_name = 'Alice Johnson', updated_at = NOW()WHERE id = 42;Production-бонус:
RETURNING (PostgreSQL):UPDATE usersSET full_name = 'Alice Johnson', updated_at = NOW()WHERE id = 42RETURNING id, full_name, updated_at;Важная ловушка:
Если поле может уже быть актуальным, сужайте
touched rows ≠ changed rows:
В проде число «обновлённых» строк не всегда равно числу реально изменённых значений.Если поле может уже быть актуальным, сужайте
UPDATE условием «менять только если отличается».UPDATE usersSET full_name = 'Alice Johnson', updated_at = NOW()WHERE id = 42 AND full_name IS DISTINCT FROM 'Alice Johnson';✅ Вывод:
Если бизнесу важен «факт реального изменения», добавляйте условие отличия в
WHERE.Production-паттерн: optimistic concurrency (PostgreSQL):
UPDATE usersSET full_name = :new_name, updated_at = NOW()WHERE id = :id AND updated_at = :prev_updated_atRETURNING id, full_name, updated_at;Если
RETURNING вернул 0 строк — запись уже успели изменить параллельно, нужен retry/конфликт.✅ Вывод:
UPDATE ... WHERE ... RETURNING даёт безопасный и наблюдаемый CRUD-паттерн.🔎 Как это происходит на практике:
- Контекст: пользователь изменил имя в профиле.
- Действия: backend обновляет строку по
id. - Результат: меняется одна запись, API получает актуальные данные сразу.
Характеристики:
- предсказуемо;
- легко ревьюить;
- минимальный риск побочных изменений.
Когда использовать:
Когда обновляете конкретную сущность или небольшой набор.
✅ Вывод:
Любой рабочий
UPDATE начинается с точного WHERE.📦 2. Массовое обновление: когда строк много
Массовые
UPDATE нужны регулярно, но именно здесь чаще всего появляются блокировки и ошибки масштаба.🟢 Если совсем просто:
Большой
UPDATE может быть корректным по логике, но тяжёлым по нагрузке.🎯 Как понять, что этап прошёл успешно:
Данные обновились по плану, а система не «легла» из-за одной операции.
Назначение:
Изменить состояние большого сегмента данных одним SQL.
Простыми словами:
Меняем много строк сразу по бизнес-условию.
Для новичка:
Перед запуском большого
UPDATE всегда оценивайте объём: SELECT COUNT(*) ... WHERE ....Аналогия:
Это как менять тариф сразу у тысячи клиентов: быстро, но нужна аккуратная подготовка.
Пример:
UPDATE usersSET is_active = false, updated_at = NOW()WHERE last_login_at < CURRENT_DATE - INTERVAL '365 days';Production-приём: обновление батчами:
WITH batch AS ( SELECT id FROM users WHERE last_login_at < CURRENT_DATE - INTERVAL '365 days' ORDER BY id LIMIT 5000)UPDATE users uSET is_active = false, updated_at = NOW()FROM batch bWHERE u.id = b.id;Цикл выполнения батчей:
Один батч — это только один шаг. На практике батчи повторяют, пока
UPDATE не вернёт 0 строк.-- Псевдо-алгоритм:-- 1) выполняем batched UPDATE-- 2) смотрим число затронутых строк-- 3) если 0 -> заканчиваем; иначе запускаем следующий батчЕсли батчи крутят несколько воркеров (PostgreSQL):
WITH batch AS ( SELECT id FROM users WHERE last_login_at < CURRENT_DATE - INTERVAL '365 days' ORDER BY id LIMIT 5000 FOR UPDATE SKIP LOCKED)UPDATE users uSET is_active = false, updated_at = NOW()FROM batch bWHERE u.id = b.id;✅ Вывод:
Для больших объёмов безопаснее идти батчами, чем одним гигантским
UPDATE.🔎 Как это происходит на практике:
- Контекст: nightly job по очистке «старых» аккаунтов.
- Действия: обновляют записи частями, контролируя нагрузку.
- Результат: предсказуемое выполнение без длинных стопоров.
Характеристики:
- эффективно для массовых операций;
- чувствительно к объёму и индексам;
- требует контроля плана запуска.
Когда использовать:
Когда бизнес-правило относится к большим сегментам данных.
✅ Вывод:
Массовый
UPDATE проектируется так же внимательно, как миграция.🔗 3. UPDATE с JOIN (обзорно)
Иногда новые значения нужно взять из другой таблицы. В PostgreSQL для этого обычно используют
UPDATE ... FROM.🟢 Если совсем просто:
JOIN в UPDATE позволяет обновлять данные на основе связанных таблиц.🎯 Как понять, что этап прошёл успешно:
Целевая таблица синхронизирована с источником без случайных перезаписей.
Назначение:
Обновить поле в таблице-цели, опираясь на данные из таблицы-источника.
Простыми словами:
Присоединяем источник и переносим нужное значение в
SET.Для новичка:
В PostgreSQL чаще пишут
UPDATE ... FROM, а не UPDATE ... JOIN как в некоторых других СУБД.Аналогия:
Это как сверка двух реестров, где один обновляет другой.
Пример (PostgreSQL):
UPDATE orders oSET customer_email = u.email, updated_at = NOW()FROM users uWHERE o.user_id = u.id AND o.customer_email IS DISTINCT FROM u.email;Важная ловушка:
Если
Сначала дедуплицируйте источник (например,
JOIN даёт несколько строк-источников на одну строку-цель, результат может быть непредсказуемым.Сначала дедуплицируйте источник (например,
DISTINCT ON/агрегация), потом обновляйте.Правило кардинальности:
Для корректного
UPDATE ... FROM держите правило 1 target row -> 1 source row.Как сделать источник однозначным:
DISTINCT ON (key)(PostgreSQL);GROUP BY key+ агрегат (MAX/MINи т.д.);- отдельный CTE
source_dedup, затемUPDATE ... FROM source_dedup.
WITH src AS ( SELECT DISTINCT ON (h.user_id) h.user_id, h.email FROM user_emails_history h ORDER BY h.user_id, h.changed_at DESC)UPDATE users uSET email = src.email, updated_at = NOW()FROM srcWHERE u.id = src.user_id;✅ Вывод:
UPDATE ... FROM мощный, но требует контроля кардинальности join-набора.🔎 Как это происходит на практике:
- Контекст: денормализованное поле в
ordersнужно синхронизировать сusers. - Действия: batch-обновление через
FROM. - Результат: данные в заказах соответствуют актуальному источнику.
Характеристики:
- удобно для синхронизаций;
- чувствительно к качеству join-условия;
- требует проверки на дубли в источнике.
Когда использовать:
Когда новое значение зависит от связанной таблицы.
✅ Вывод:
Перед
UPDATE с join всегда валидируйте уникальность источника на ключ соединения.🚨 4. Ошибка UPDATE без WHERE: почему это критично
Самая частая авария в SQL-операциях изменения данных — забытый
WHERE.🟢 Если совсем просто:
UPDATE без WHERE пытается изменить все строки таблицы.🎯 Как понять, что этап прошёл успешно:
Вы встроили в процесс защиту от случайного полного обновления.
Назначение:
Понять риск и зафиксировать безопасный шаблон выполнения.
Простыми словами:
Один неверный запрос может повредить прод за секунды.
Для новичка:
Критичные обновления запускайте в транзакции: сначала проверка, потом
COMMIT.Пример аварийного запроса:
UPDATE usersSET is_active = false;Безопасный рабочий шаблон:
BEGIN; SELECT COUNT(*) AS target_countFROM usersWHERE last_login_at < CURRENT_DATE - INTERVAL '365 days'; UPDATE usersSET is_active = false, updated_at = NOW()WHERE last_login_at < CURRENT_DATE - INTERVAL '365 days'; COMMIT;Защита через ограничитель партии (аналог
LIMIT для UPDATE):
В PostgreSQL нет прямого UPDATE ... LIMIT, поэтому ограничение делают через CTE/батч.WITH guard_batch AS ( SELECT id FROM users WHERE last_login_at < CURRENT_DATE - INTERVAL '365 days' ORDER BY id LIMIT 5000)UPDATE users uSET is_active = false, updated_at = NOW()FROM guard_batch gWHERE u.id = g.id;Практика: если ожидаете небольшой объём, ограничивайте партию и проверяйте, что следующих партий нет.
✅ Вывод:
Безопасный
UPDATE = проверка набора + транзакция + осознанный COMMIT.🆚 Сравнение подходов UPDATE
| Подход | Что делает | Когда подходит | Риск |
|---|---|---|---|
UPDATE ... WHERE id = ... | Точечное обновление | CRUD-операция одной сущности | Минимальный при точном WHERE |
Массовый UPDATE | Обновляет большой сегмент | Ночные джобы, миграции состояния | Блокировки и долгий runtime |
UPDATE ... FROM | Обновление по связанной таблице | Синхронизация/денормализация | Ошибка кардинальности join |
✅ Вывод:
Выбор паттерна зависит от объёма данных и источника нового значения.
🧠 Must-Know (запомнить)
- Любой
UPDATEдолжен иметь осознанныйWHERE, если вы не планируете изменить всю таблицу. - Перед
UPDATEполезно запускатьSELECTс тем же фильтром. - Разделяйте
touched rowsиchanged rows: это не всегда одно и то же. - Если поле может уже быть актуальным, добавляйте условие отличия (
IS DISTINCT FROMв PostgreSQL). - Массовые обновления проверяйте по объёму (
COUNT(*)) до запуска. - Для больших объёмов используйте батчи, а не один гигантский statement.
- Batched update повторяют, пока
UPDATEне возвращает 0 строк. - Для параллельных воркеров в PostgreSQL используйте
FOR UPDATE SKIP LOCKED. - В PostgreSQL для join-обновлений используйте
UPDATE ... FROM. - В
UPDATE ... FROMдержите правило1 target row -> 1 source row. - В критичных сценариях запускайте
UPDATEв транзакции. RETURNINGпомогает проверить результат без отдельного запроса.- Для API-конкуренции применяйте optimistic concurrency (
WHERE ... updated_at = :prev_updated_at). - Если нужен «мягкий предохранитель», ограничивайте UPDATE через CTE-батч.
- Обновлять «вслепую» в проде — прямой путь к инцидентам.
- Проверяйте число затронутых строк в логах и мониторинге.
✅ Вывод:
Сильная практика
UPDATE строится на предсказуемости и контроле объёма изменений.❌ Частые мифы
❌ Миф: Если запрос короткий, проверка не нужна.
✅ Как правильно: даже короткий
UPDATE проверяем через SELECT-превью.
📎 Почему это важно: короткий запрос тоже может затронуть всю таблицу.❌ Миф: Массовое обновление всегда лучше делать одним запросом.
✅ Как правильно: большие объёмы часто безопаснее обновлять батчами.
📎 Почему это важно: меньше риск долгих блокировок и таймаутов.
❌ Миф:
UPDATE ... FROM автоматически безопасен.
✅ Как правильно: перед обновлением контролируйте уникальность join-источника.
📎 Почему это важно: дубли в источнике дают непредсказуемый результат.❌ Миф: Ошибку без
WHERE легко откатить «потом».
✅ Как правильно: лучше предотвратить ошибку, чем восстанавливать данные постфактум.
📎 Почему это важно: откат и восстановление в проде часто дорогие по времени и риску.🎤 Часто спрашивают на собеседованиях
❓ Вопрос: Почему
UPDATE без WHERE считается критичной ошибкой?
✅ Ответ: потому что он может изменить все строки таблицы одним запросом.❓ Вопрос: Что вы делаете перед запуском массового
UPDATE?
✅ Ответ: проверяю набор через SELECT/COUNT, оцениваю объём и только потом выполняю обновление.❓ Вопрос: Когда нужен batched update?
✅ Ответ: когда обновляем очень большой объём, чтобы снизить риск долгих блокировок.
❓ Вопрос: Как выглядит
UPDATE с join в PostgreSQL?
✅ Ответ: через UPDATE target SET ... FROM source WHERE ....❓ Вопрос: Какая ловушка у
UPDATE ... FROM?
✅ Ответ: неуникальные совпадения в источнике; нужно контролировать кардинальность join.❓ Вопрос: Зачем
RETURNING в UPDATE?
✅ Ответ: чтобы сразу получить изменённые строки и проверить результат без лишнего SELECT.❓ Вопрос: Почему
SELECT перед UPDATE — это best practice?
✅ Ответ: он показывает точный набор строк и снижает вероятность ошибочного массового изменения.❓ Вопрос: Что делать, если
UPDATE ... WHERE ... RETURNING вернул 0 строк при конкурентном API-запросе?
✅ Ответ: трактовать как optimistic concurrency conflict: запись уже изменилась, нужен retry или сообщение о конфликте.🚨 Типичные ошибки
- Запускать
UPDATEбезWHEREпо привычке «дописать потом». - Использовать слишком широкий фильтр и случайно задевать лишние строки.
- Делать массовый
UPDATEбез предварительной оценки объёма. - Игнорировать блокировки и влияние на онлайн-нагрузку.
- Считать, что число затронутых строк всегда равно числу реально изменённых значений.
- Не проверять источник на дубли перед
UPDATE ... FROM. - Делать
UPDATE ... FROM, где одному target соответствуют несколько source-строк. - Выполнять только один batch и не доводить цикл до
0 rows. - Обновлять в проде без транзакционного сценария для критичных данных.
- Не читать число затронутых строк после выполнения.
✅ Вывод:
Большинство проблем с
UPDATE предотвращаются дисциплиной выполнения.✅ Best Practices
- Пишите
UPDATEтолько с явным и провереннымWHERE. - Перед запуском обновления проверяйте те же условия через
SELECT. - Если значение может уже совпадать, фильтруйте по отличию (
IS DISTINCT FROM/NULL-safe логика). - Для массовых изменений фиксируйте ожидаемый объём строк заранее.
- Запускайте большие обновления батчами и повторяйте цикл до
0 rows. - Для конкурентных батч-воркеров в PostgreSQL используйте
FOR UPDATE SKIP LOCKED. - Для join-обновлений делайте источник однозначным на ключ соединения.
- В PostgreSQL используйте
RETURNINGдля быстрой валидации результата. - Для API-параллелизма используйте optimistic concurrency по
updated_at/version. - Критичные изменения оборачивайте в транзакцию с осознанным
COMMIT. - Если ожидаете небольшой объём, ограничивайте масштаб обновления через CTE-батч.
- Добавляйте post-check: выборка изменённых строк и бизнес-метрики.
✅ Вывод:
Надёжный
UPDATE — это процесс с валидацией до и после выполнения.🏁 Заключение
UPDATE — центральная команда CRUD, которая напрямую влияет на целостность данных.Если вы контролируете фильтр, объём и сценарий выполнения, обновления становятся предсказуемыми.
✅ Вывод:
Production-ready
UPDATE = точный WHERE + контроль объёма + безопасный процесс запуска.