Предскажите результат удаления: какие строки останутся после выполнения оператора и будет ли удаление распространяться на подчинённых сотрудников следующих уровней?
CREATE TABLE employees (
employee_id INTEGER,
manager_id INTEGER
);
INSERT INTO employees VALUES
(1, NULL), (2, 1), (3, 2), (4, 3);
DELETE FROM employees
WHERE manager_id IN (
SELECT employee_id
FROM employees
WHERE manager_id IS NULL
);
Будет удалена только строка с employee_id = 2, потому что подзапрос возвращает идентификатор корневого сотрудника 1, а условие DELETE проверяет manager_id = 1. Строки с идентификаторами 3 и 4 автоматически не удаляются: оператор не выполняется итеративно по изменяющейся таблице.
После удаления останутся сотрудники 1, 3 и 4.
SQL задуман как декларативный язык работы с множествами строк. В DML-операторе описывается множество строк, подходящих под условие, а не алгоритм последовательного обхода строк с повторной проверкой после каждого изменения.
Такой подход отделяет намерение запроса от способа выполнения. СУБД может выбрать план с индексом, хешированием или другим способом, сохраняя логическую семантику единого оператора.
В таблице задана цепочка подчинения: сотрудник 2 подчиняется 1, сотрудник 3 — 2, а сотрудник 4 — 3. Подзапрос находит сотрудников без руководителя, то есть только 1.
Распространённая ошибка — рассуждать процедурно: сначала удалить 2, затем обнаружить, что 3 теперь косвенно связан с удалённой строкой, и продолжить удаление. Такой алгоритм данным DELETE не задан, поэтому он не выполняется.
Логически подзапрос определяет множество значений employee_id из исходного состояния, подходящих под manager_id IS NULL. В нашем примере это множество {1}.
Затем внешнее условие выбирает строки, для которых manager_id IN (1). Подходит только строка (2, 1). Удаление этой строки не приводит к повторному запуску подзапроса и не добавляет в множество кандидатов идентификаторы 2 или 3.
Это не означает, что физически СУБД обязана сначала полностью материализовать подзапрос. Оптимизатор может выбрать другой план, но результат должен соответствовать логике одного SQL-оператора. Конкретная СУБД также может иметь ограничения на обращение к изменяемой таблице в подзапросе, поэтому синтаксис нужно проверять для выбранной реализации SQL.
Если требуется удалить всю иерархию, условие IN недостаточно. Нужно явно вычислить транзитивное множество потомков, например рекурсивным CTE:
Рекурсивный запрос явно задаёт нужную семантику. При этом следует учитывать циклы в данных, ограничения внешних ключей, каскадное удаление и необходимость транзакции.
При удалении подразделения команда хотела удалить всех его сотрудников и подчинённые команды. Первоначальный DELETE удалял только непосредственных подчинённых, оставляя более глубокие уровни и создавая неконсистентные данные.
Повторять обычный DELETE в цикле можно, но это усложняет код, требует контроля завершения и увеличивает число операций. Использовать ON DELETE CASCADE проще, однако каскад подходит только тогда, когда удаление всех зависимых строк всегда является правильным правилом модели.
Было выбрано явное вычисление дерева через рекурсивный CTE и один DELETE в транзакции. Это сделало область удаления проверяемой, позволило предварительно вывести список затрагиваемых идентификаторов и не смешало бизнес-правило удаления иерархии с общим поведением внешнего ключа.
Вопрос: Изменится ли результат, если подзапрос использует SELECT DISTINCT employee_id?
Ответ: Нет, если employee_id уже уникален или повторяющиеся значения не меняют множество, проверяемое через IN. IN проверяет наличие совпадения, а не количество совпавших строк. DISTINCT может уменьшить промежуточный объём данных, но не меняет логический результат при одинаковом наборе значений.
Вопрос: Что произойдёт, если вместо IN использовать NOT IN, а подзапрос вернёт NULL?
Ответ: Из-за трёхзначной логики SQL сравнение с неопределённым значением может дать UNKNOWN. Для NOT IN наличие NULL в результате подзапроса способно сделать условие неизвестным для строк, не имеющих совпадения, поэтому они не будут удалены. Для проверки отсутствия связанной строки обычно безопаснее рассматривать NOT EXISTS, явно учитывая условие связи.
Вопрос: Почему для удаления всех потомков нельзя полагаться на порядок обработки строк планом выполнения?
Ответ: SQL не обещает порядок обработки строк, если он не является частью семантики конкретного оператора. План может использовать индекс, сортировку, хеширование или параллельное выполнение, поэтому результат не должен зависеть от того, какая строка физически обработана первой. Если порядок или повторное прохождение действительно необходимо, его нужно выразить средствами SQL, например рекурсивным CTE, либо реализовать отдельным процедурным алгоритмом с явно контролируемыми шагами.