Программирование SQLDML и запросыРазработчик серверной части

При UPDATE значение столбца берут из коррелированного подзапроса, но соответствующей строки в источнике нет...

При UPDATE значение столбца берут из коррелированного подзапроса, но соответствующей строки в источнике нет. Что произойдёт с обновляемым столбцом?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Если коррелированный скалярный подзапрос не находит строку, его результатом обычно является NULL. Поэтому UPDATE присвоит NULL обновляемому столбцу, если это разрешено ограничениями; строка не сохранит прежнее значение автоматически.

Исторический контекст

Коррелированные подзапросы позволяют вычислять новое значение отдельно для каждой обновляемой строки на основе связанных данных. Такой подход появился как декларативная альтернатива процедурному циклу: СУБД сама определяет набор строк и вычисляет выражение обновления.

Скалярный подзапрос рассматривается как выражение, возвращающее одно значение. Отсутствие строк интерпретируется как отсутствие значения, то есть NULL, а не как команда «ничего не менять».

Постановка проблемы

Предположим, что для части клиентов нет соответствующей записи в таблице-источнике. Если без дополнительной проверки использовать результат подзапроса в UPDATE, прежние значения могут быть заменены на NULL.

Это может привести к ошибке ограничения NOT NULL, нарушению бизнес-логики или тихой потере данных, если столбец допускает NULL. Особенно опасно, когда отсутствие строки означает «данные не изменять», а не «записать неизвестное значение».

Подробное решение

Для каждой строки целевой таблицы СУБД вычисляет коррелированный подзапрос. Если он возвращает одну строку, используется найденное значение; если не возвращает ни одной строки, скалярный результат равен NULL. Если подзапрос возвращает несколько строк, выполнение обычно завершается ошибкой о том, что скалярный подзапрос вернул более одного значения.

UPDATE customers AS c SET discount = ( SELECT r.discount FROM customer_rates AS r WHERE r.customer_id = c.id );

В этом примере отсутствие тарифа для клиента приведёт к присваиванию NULL. Чтобы изменять только клиентов с найденным тарифом, нужно ограничить целевой набор условием EXISTS или использовать иной явно заданный способ сопоставления.

Если отсутствие источника должно означать сохранение старого значения, условие обычно выражают через CASE или COALESCE, но эти варианты имеют разный смысл. COALESCE также заменит найденный NULL запасным значением, тогда как CASE с проверкой существования позволяет различать отсутствие строки и наличие строки с NULL.

Нужно обеспечить не более одной строки источника для каждой целевой строки. Для этого применяют уникальное ограничение на ключ источника либо предварительно агрегируют или детерминированно отбирают одну запись. Выбор «какой-нибудь» строки без заданного правила делает результат недетерминированным или зависимым от конкретной СУБД.

Ситуация из практики

В системе нужно обновить скидку клиентов по таблице актуальных тарифов. Для клиентов без тарифа прежнюю скидку требуется сохранить.

Прямой UPDATE с коррелированным подзапросом прост, но обнуляет скидки без тарифа. Вариант с COALESCE сохраняет старое значение при NULL, однако не различает отсутствие тарифа и тариф, который сам явно задан как NULL. Вариант с EXISTS точнее отражает бизнес-правило: обновлять только клиентов, для которых существует источник.

Выбран второй вариант — ограничение обновляемых строк через EXISTS. Перед запуском также проверяют уникальность тарифа по идентификатору клиента. В результате клиенты без тарифа не изменяются, а неоднозначное сопоставление обнаруживается до массового обновления.

Что кандидаты часто упускают

  1. Вопрос: Что произойдёт, если коррелированный подзапрос вернёт несколько строк?

    Ответ: Скалярный контекст требует не более одного значения. Если подзапрос вернёт несколько строк, UPDATE обычно завершится ошибкой, а не выберет первую строку произвольно. Поэтому уникальность связи должна быть гарантирована ограничением или обеспечена явной агрегацией и правилом выбора.

  2. Вопрос: Чем отличается отсутствие строки в подзапросе от строки, содержащей NULL?

    Ответ: В скалярном результате оба случая могут дать NULL, поэтому простое присваивание не позволяет их различить. Чтобы различать ситуации, отдельно проверяют существование строки, например через EXISTS, а затем выбирают нужное действие: обновить, сохранить прежнее значение или записать NULL.

  3. Вопрос: Почему добавление условия EXISTS не всегда делает UPDATE безопасным?

    Ответ: EXISTS подтверждает наличие хотя бы одной строки, но не гарантирует, что строка ровно одна. Если подзапрос в SET по-прежнему видит несколько строк, UPDATE может завершиться ошибкой. Для безопасности нужны уникальный ключ, агрегация или другое детерминированное правило выбора одной записи.