SQL

БЛОК 6. CRUD — 18. UPDATE

📚 22 вопросовПройти тест →
Лекция

БЛОК 6. CRUD — 18. UPDATE

SQL

18. UPDATE

🧭 Введение: почему UPDATE требует дисциплины

UPDATE меняет уже существующие данные, поэтому ошибка здесь обычно дороже, чем в INSERT.
Если неверно задать условие, можно массово испортить рабочие записи за один запрос.
В этой лекции разберём практику, которая нужна в проде:
  • базовый UPDATE с WHERE;
  • массовые обновления и их риски;
  • UPDATE с JOIN (обзорно, на уровне рабочего паттерна);
  • типичную аварийную ошибку UPDATE без WHERE.
💡 Совет: Перед UPDATE всегда проверяйте условие через SELECT, чтобы видеть, какие строки реально затронутся.
Вывод: Надёжный UPDATE начинается не с SET, а с точного и проверенного WHERE.

⚠️ Проблема -> решение

Типичная проблема:
  1. обновляют данные без точного фильтра;
  2. не оценивают объём затрагиваемых строк;
  3. смешивают бизнес-логику и «быстрые правки» в одном запросе.
Последствия:
  1. массовая порча данных;
  2. долгие блокировки;
  3. сложный rollback и ручное восстановление.
Решение:
  1. сначала SELECT с тем же WHERE;
  2. в важных случаях работать в транзакции;
  3. для больших объёмов обновлять батчами.
🟢 Если совсем просто: 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 rowschanged 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.
Как сделать источник однозначным:
  1. DISTINCT ON (key) (PostgreSQL);
  2. GROUP BY key + агрегат (MAX/MIN и т.д.);
  3. отдельный 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 + контроль объёма + безопасный процесс запуска.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 6. CRUD — 18. UPDATE»

Пройти тест →