DELETE и DETACH DELETE: удаляем безопасно

Почему «просто удалить узел» в графе не работает и как удалять правильно.

DELETE удаляет узлы и связи, но узел со связями удалить нельзя; DETACH DELETE сначала рвёт все связи узла, затем удаляет его самого.

Правило целостности графа

В графе не бывает «висячих» связей — ребро всегда соединяет два существующих узла. Поэтому если вы попробуете удалить узел, у которого есть связи, обычным DELETE, Neo4j откажет с ошибкой: иначе остались бы рёбра в никуда.

// Ошибка, если у Боба есть связи:
MATCH (p:Person {name:'Боб'})
DELETE p

Это не каприз, а фундаментальный инвариант модели. Каждое ребро физически хранит ссылки на оба своих конца и записано в списках смежности обоих узлов. Удалив узел, но оставив ребро, мы получили бы запись, указывающую в пустоту, — обход по такому ребру привёл бы в несуществующий узел. Реляционные базы решают похожую проблему внешними ключами и каскадами; граф решает её жёстким запретом на висячие рёбра прямо на уровне движка. Текст ошибки прямо подсказывает выход: «Cannot delete node, because it still has relationships. To delete this node, you must first delete its relationships» — то есть нужен DETACH DELETE.

DETACH DELETE

Правильный способ удалить узел вместе со всеми его рёбрами — DETACH DELETE:

MATCH (p:Person {name:'Боб'})
DETACH DELETE p

Он атомарно удаляет связи Боба и сам узел. Это самый частый способ удаления сущности целиком. «Атомарно» здесь значит: либо удалятся и узел, и все его рёбра, либо при ошибке откатится всё — промежуточного состояния, где половина рёбер срезана, а узел остался, не бывает.

Картинка до и после наглядно показывает, что DETACH убирает не только сам узел, но и все инцидентные ему рёбра, оставляя соседей нетронутыми:

До:    (Алиса)──ЗНАЕТ──>(Боб)<──ЗНАЕТ──(Вера)
                          │
                       ACTED_IN
                          ↓
                       (Фильм)

DETACH DELETE (Боб)

После: (Алиса)        (Вера)        (Фильм)
       — рёбра Боба и сам Боб исчезли, соседи целы

Соседние узлы (Алиса, Вера, Фильм) при этом не страдают — удаляются только рёбра, у которых Боб был одним из концов. Это ровно то, чего ждёшь от «удалить пользователя»: его связи рвутся, но друзья и фильмы остаются в базе.

Удаляем только связь

Иногда нужно убрать связь, оставив узлы. Тогда DELETE применяют к переменной связи:

MATCH (a:Person {name:'Алиса'})-[r:ЗНАЕТ]->(b:Person {name:'Боб'})
DELETE r

Алиса и Боб остаются, исчезает только их дружба. Это «развод», а не «удаление человека». На практике связи удаляют чаще узлов: отписаться, разлайкать, отменить заказ, разорвать зависимость — всё это удаление одного ребра при сохранении обоих узлов. Запомните разницу как мантру: DELETE r рвёт отношение, DETACH DELETE n сносит сущность.

Полная очистка базы

Снести вообще всё (например, чтобы переиграть учебный датасет):

MATCH (n)
DETACH DELETE n

На большой базе это тяжёлая операция в одной транзакции — для миллионов узлов делают батчами:

MATCH (n)
CALL { WITH n DETACH DELETE n } IN TRANSACTIONS OF 10000 ROWS

Здесь движок удаляет узлы порциями по 10 000, фиксируя каждую и освобождая память, — так очистка миллионной базы не упрётся в лимит памяти одной гигантской транзакции.

Как работает под капотом

DETACH DELETE — это, по сути, «найти все рёбра узла, удалить их, затем удалить запись узла». Удаление ребра вычёркивает его из списков смежности обоих концов и освобождает записи в store-файлах. Поскольку всё идёт в транзакции, либо удаляется всё указанное, либо ничего. Удалённые id могут позже переиспользоваться — ещё один довод не привязываться к внутреннему id.

Стоит понять разницу в стоимости между удалением ребра и удалением узла. Удаление одного ребра — операция почти постоянной цены: вычеркнуть его из двух списков смежности и пометить запись свободной. А вот DETACH DELETE узла стоит пропорционально его степени: чтобы убрать узел с миллионом рёбер (суперузел), движок обязан сперва удалить миллион рёбер. Поэтому удаление одного суперузла внезапно оказывается одной из самых дорогих операций в базе — это та же проблема степеней, что и при обходах.

Про переиспользование id важно повторить отдельно: после удаления узла или ребра его внутренний идентификатор освобождается и может быть позже выдан совершенно другой, новой сущности. Если ваше приложение где-то сохранило id(n) как «вечную ссылку», после удаления и последующих вставок этот id может указывать уже не на то, что вы думали. Вывод тот же, что и в уроках про модель: идентифицируйте узлы своим бизнес-ключом (например userId) и уникальным ограничением, а не внутренним id Neo4j.

Частые ошибки

  • DELETE узла со связями. Получите ошибку целостности; нужен DETACH DELETE.
  • DETACH DELETE, когда хотели убрать лишь связь. Снесёте и узлы тоже; для связи — DELETE r.
  • Очистка всей базы одной транзакцией. На больших данных падает по памяти — батчируйте.
  • Удаление без точного MATCH. Слишком широкий паттерн сметёт лишнее — всегда проверяйте, что найдёте, через RETURN перед DELETE.
  • Опора на внутренний id после удаления. Освобождённые id переиспользуются новыми сущностями; сохранённый id(n) может «протухнуть». Идентифицируйте узлы бизнес-ключом.
  • Очистка суперузла наравне с обычным. Помните: цена DETACH DELETE растёт со степенью — удаление одного узла с миллионом рёбер тяжелее, чем тысячи мелких.

Итоги

  • Связь всегда соединяет существующие узлы — висячих рёбер не бывает.
  • Узел со связями удаляется только через DETACH DELETE (рвёт рёбра + удаляет узел).
  • DELETE r убирает связь, сохраняя узлы.
  • Очистку больших объёмов делайте батчами; перед удалением проверяйте MATCH через RETURN.
Проверьте себя
1. Почему обычный DELETE узла со связями выдаёт ошибку?
AТак нельзя удалять вообще
BИначе остались бы висячие связи в никуда — граф запрещает рёбра без узлов
CDELETE работает только со связями
DНужны права администратора
2. Как удалить только связь между Алисой и Бобом, сохранив их самих?
ADETACH DELETE обоих
BMATCH (a)-[r:ЗНАЕТ]->(b) ... DELETE r
CDELETE a, b
DREMOVE r
3. Что делает MATCH (n) DETACH DELETE n?
AУдаляет один узел
BУдаляет все узлы и связи — полностью очищает базу
CУдаляет только связи
DНичего
4. Почему DETACH DELETE суперузла с миллионом рёбер дорог?
AИз-за блокировки всей базы
BЦена пропорциональна степени: сперва надо удалить все рёбра узла, и только потом сам узел
CDETACH DELETE всегда сканирует всю базу
DСуперузлы удалять запрещено