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.