SET и REMOVE: меняем свойства и метки

Как изменять уже существующие узлы и связи: свойства, метки, точечно и пачкой.

SET присваивает или меняет свойства и добавляет метки; REMOVE убирает свойство или метку. Менять стоит то, что сперва найдено через MATCH.

Меняем свойство

Сначала находим узел, потом присваиваем:

MATCH (p:Person {name:'Алиса'})
SET p.born = 1991, p.city = 'Москва'
RETURN p

Если свойства не было — оно добавится; если было — перезапишется. SET одинаково создаёт и обновляет. Это важная черта: в графе у узла нет фиксированной «схемы колонок», поэтому добавить новое свойство одному узлу — нормальная и дешёвая операция, не требующая ALTER TABLE, как в реляционной базе. Два узла :Person вполне могут иметь разный набор свойств.

Порядок «сначала найти, потом менять» — не формальность, а суть модели. SET работает с тем, что приехало из MATCH: на каждую найденную строку он применяет присваивание. Нет совпадений в MATCH — нечего и менять, SET просто ничего не сделает (и не выдаст ошибки). Поэтому если апдейт «не сработал», первым делом проверяют, что MATCH вообще что-то находит.

Вычисляемые присваивания

Справа от SET может стоять любое выражение, в том числе использующее текущее значение свойства. «Поднять рейтинг всех фильмов студии на 1»:

MATCH (m:Movie)
WHERE m.studio = 'A24'
SET m.rating = coalesce(m.rating, 0) + 1
RETURN m.title, m.rating

Функция coalesce подставляет 0 там, где рейтинга ещё не было, иначе null + 1 дал бы null и испортил бы данные. Такой приём — счётчики, инкременты, накопление — частая причина выбрать SET с выражением вместо константы.

Слияние свойств через +=

Чтобы накатить пачку свойств из объекта, не трогая остальные, есть +=:

MATCH (p:Person {name:'Боб'})
SET p += {city:'Казань', verified:true}
RETURN p

А вот простое = с объектом заменяет все свойства целиком: SET p = {name:'Боб'} сотрёт всё, кроме name. Разница между = и += здесь критична.

Добавляем и снимаем метки

Метку добавляют тем же SET, через двоеточие; снимают через REMOVE:

MATCH (p:Person {name:'Вера'})
SET p:Director          // теперь :Person:Director
REMOVE p:Director       // снова только :Person

Так удобно помечать состояние: добавить :Premium при оплате, снять при истечении. Метки в Neo4j — это лёгкие теги, и узел может нести их сразу несколько (:Person:Director:Premium). Помечать ими статус выгодно ещё и потому, что по метке есть быстрый поиск: «все премиум-пользователи» — это MATCH (p:Premium), дешёвый label scan, а не фильтр по булеву свойству.

Динамическое навешивание/снятие меток — типичный приём моделирования жизненного цикла сущности: :Lead:Customer:Churned. Вместо колонки status со строковыми значениями вы оперируете метками, и запросы по состоянию становятся короче и быстрее.

Удаляем свойство

REMOVE убирает свойство (это не то же самое, что выставить null, хотя эффект похож):

MATCH (p:Person {name:'Боб'})
REMOVE p.verified
RETURN p

Тонкость, которую полезно знать: в Neo4j установить свойству значение null и удалить свойство — это одно и то же действие. SET p.verified = null физически убирает ключ verified у узла, ровно как REMOVE p.verified. В графе попросту не хранится «свойство со значением null»: либо свойство есть, либо его нет. Поэтому проверка WHERE p.verified IS NULL истинна и для тех, у кого свойства никогда не было, и для тех, кому его обнулили.

Массовые апдейты

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

MATCH (m:Movie)
WHERE m.released IS NULL
SET m.released = 0

Один MATCH нашёл все подходящие фильмы, один SET поправил их все — никакого цикла писать не нужно, Cypher по своей природе работает над множеством строк. Это и сила, и ловушка: широкий MATCH поправит больше, чем вы ожидали. Перед массовым SET полезно сначала прогнать тот же MATCH ... RETURN count(*) и убедиться, что под фильтр попадает ровно столько узлов, сколько вы держите в голове.

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

SET и REMOVE — операции записи внутри транзакции. Neo4j берёт блокировку на изменяемые узлы/связи, применяет изменения и пишет их в журнал (write-ahead log) для устойчивости. Если запрос затрагивает миллионы узлов, всё это копится в одной транзакции и грузит память — поэтому большие апдейты разбивают на батчи (через CALL { ... } IN TRANSACTIONS или подобное), а не делают одной командой.

Почему именно «копится»? Транзакция в Neo4j атомарна: либо все изменения применяются вместе, либо при ошибке откатываются целиком. Чтобы гарантировать откат, движок держит в памяти состояние всех затронутых записей до самого commit. Миллион изменённых узлов — это миллион записей в памяти транзакции плюс журнал. Батчинг через CALL { ... } IN TRANSACTIONS OF 10000 ROWS разрезает работу на множество мелких транзакций, каждая из которых фиксируется и освобождает память:

MATCH (m:Movie)
WHERE m.released IS NULL
CALL { WITH m SET m.released = 0 } IN TRANSACTIONS OF 10000 ROWS

Платой за это становится потеря «всё-или-ничего»: если упадёт пятый батч, первые четыре уже зафиксированы. Для разовой миграции данных это обычно приемлемый компромисс, но помнить о нём нужно.

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

  • Путать = и += с объектом. SET p = {...} заменяет ВСЕ свойства; SET p += {...} сливает. Это самая болезненная ошибка.
  • SET без MATCH. Менять можно только найденное; забудете MATCH — менять нечего.
  • Гигантский апдейт одной транзакцией. Миллионы изменений сразу могут упасть по памяти — батчируйте.

Итоги

  • SET присваивает свойства и добавляет метки; работает как «создать или обновить».
  • += сливает свойства, а = с объектом — заменяет все; не перепутайте.
  • REMOVE убирает свойство или метку.
  • Массовые апдейты батчируйте — одна гигантская транзакция бьёт по памяти.
Проверьте себя
1. В чём разница между SET p = {...} и SET p += {...}?
AНикакой
B= заменяет ВСЕ свойства узла объектом, += сливает (обновляет/добавляет), не трогая остальные
C+= удаляет узел
D= работает только с метками
2. Как добавить узлу вторую метку :Director?
ACREATE :Director
BSET p:Director
CADD LABEL Director
DMERGE :Director
3. Почему гигантский SET на миллионы узлов лучше батчировать?
AТак красивее
BОдна транзакция копит все изменения в памяти и может упасть; батчи через IN TRANSACTIONS безопаснее
CSET не работает на больших данных
DБатчи быстрее всегда вдвое
4. Что делает SET p.verified = null в Neo4j?
AХранит свойство со значением null
BФизически удаляет свойство verified — то же, что REMOVE p.verified
CВыдаёт ошибку
DУдаляет весь узел