В прошлом уроке второй запуск CREATE молча сделал вторую Аню. В жизни скрипты запускают повторно постоянно: после сбоя, при обновлении данных, по расписанию. Нужна запись, которая создаёт узел, только если его ещё нет. В Cypher это MERGE. Урок снова начинается с пустой базы.
Ключ от шкафчика
В спортзале на стойке висят ключи от шкафчиков. Ты подходишь и говоришь номер: если ключ на месте, тебе его выдают, если такого шкафчика нет, его заводят и выдают новый ключ. Повтори просьбу десять раз, шкафчик всё равно будет один.
MERGE работает так же. Он ищет паттерн целиком, как MATCH. Нашёл, значит использует найденное. Не нашёл, значит создаёт весь паттерн, как CREATE. Такое поведение называют идемпотентностью: повторный запуск ничего не портит.
MERGE (a:Person {name: 'Аня'});
0 rows
Created 1 node, set 1 property, added 1 label
MERGE (a:Person {name: 'Аня'});
0 rows
Второй раз итога изменений нет: Аня нашлась, создавать нечего.
ON CREATE SET и ON MATCH SET
Часто нужно по-разному обойтись с новым и найденным узлом. Например, считать визиты: новому участнику поставить один визит, старому прибавить. Для этого у MERGE есть две ветки: ON CREATE SET и ON MATCH SET.
MERGE (b:Person {name: 'Борис'})
ON CREATE SET b.visits = 1, b.created = date('2026-10-24')
ON MATCH SET b.visits = b.visits + 1
RETURN b;
+-----------------------------------------------------------+
| b |
+-----------------------------------------------------------+
| (:Person {name: "Борис", visits: 1, created: 2026-10-24}) |
+-----------------------------------------------------------+
1 row
Created 1 node, set 3 properties, added 1 label
MERGE (b:Person {name: 'Борис'})
ON CREATE SET b.visits = 1, b.created = date('2026-10-24')
ON MATCH SET b.visits = b.visits + 1
RETURN b;
+-----------------------------------------------------------+
| b |
+-----------------------------------------------------------+
| (:Person {name: "Борис", visits: 2, created: 2026-10-24}) |
+-----------------------------------------------------------+
1 row
Set 1 property
Первый запуск создал Бориса и прошёл по ветке ON CREATE. Второй нашёл его и прибавил визит. Свойство name в паттерне это ключ поиска, а остальное лучше выставлять в ветках: если записать в паттерн и visits: 1, второй запуск не найдёт Бориса с другим числом визитов и создаст нового.
MERGE связи
Связь мержат так же, но сначала находят оба конца:
MATCH (a:Person {name: 'Аня'}), (b:Person {name: 'Борис'})
MERGE (a)-[r:KNOWS]->(b)
ON CREATE SET r.since = 2015
RETURN r;
+------------------------+
| r |
+------------------------+
| [:KNOWS {since: 2015}] |
+------------------------+
1 row
Created 1 relationship, set 1 property
MATCH (a:Person {name: 'Аня'}), (b:Person {name: 'Борис'})
MERGE (a)-[r:KNOWS]->(b)
ON CREATE SET r.since = 2015
RETURN r;
+------------------------+
| r |
+------------------------+
| [:KNOWS {since: 2015}] |
+------------------------+
1 row
Связь одна, сколько бы раз ни запускался запрос.
Ловушка длинного паттерна
MERGE ищет паттерн целиком. Если не совпала хотя бы одна часть, он создаёт весь паттерн заново, включая узлы, которые уже были. Попробуем поселить Аню в Москве одним запросом:
MERGE (a:Person {name: 'Аня'})-[:LIVES_IN]->(c:City {name: 'Москва'});
0 rows
Created 2 nodes, created 1 relationship, set 2 properties, added 2 labels
Связи «Аня живёт в Москве» не было, поэтому MERGE создал весь путь: новую Аню, новый город и связь. Проверим:
MATCH (p:Person)
RETURN p.name, count(*) AS copies
ORDER BY p.name;
+------------------+
| p.name | copies |
+------------------+
| "Аня" | 2 |
| "Борис" | 1 |
+------------------+
2 rows
Правильный способ: сначала MERGE для каждого узла отдельно, потом MERGE для связи между ними. Тогда каждая часть ищется сама по себе:
MERGE (v:Person {name: 'Вера'})
MERGE (c:City {name: 'Казань'})
MERGE (v)-[:LIVES_IN]->(c);
0 rows
Created 2 nodes, created 1 relationship, set 2 properties, added 2 labels
MERGE (v:Person {name: 'Вера'})
MERGE (c:City {name: 'Казань'})
MERGE (v)-[:LIVES_IN]->(c);
0 rows
MERGE по списку
Загрузку часто пишут для целого списка значений. UNWIND разворачивает список в строки, и MERGE выполняется для каждой:
UNWIND ['Глеб', 'Аня', 'Глеб'] AS name
MERGE (p:Person {name: name})
RETURN p.name, p.visits;
+-------------------+
| p.name | p.visits |
+-------------------+
| "Глеб" | NULL |
| "Аня" | NULL |
| "Аня" | NULL |
| "Глеб" | NULL |
+-------------------+
4 rows
Created 1 node, set 1 property, added 1 label
Глеба в списке два, но узел создан один: вторая строка нашла узел, который создала первая. А вот Аня дала две строки ответа. После ловушки длинного паттерна в базе две Ани, и MERGE вернул обе: если совпадений несколько, он берёт их все. Это ещё одна причина держать ключи уникальными.
null в ключе
Если значение ключа оказалось null, MERGE откажется работать: искать «узел, у которого имя неизвестно» бессмысленно.
MERGE (x:Person {name: null});
22N31: data exception - invalid properties in merge pattern. The node property `name` in '(:Person {name: null})' is invalid. 'MERGE' cannot be used with a graph element property value that is null.
22G03: data exception - invalid value type
Такое случается при загрузке файла с пустыми полями. Как с ними обходиться, расскажем в уроке про LOAD CSV.
Попробуй сам
Песочница снова пустая. Запрос заводит двух участников и знакомство через MERGE. Запусти его несколько раз подряд и следи, меняется ли картинка и что пишет итог изменений.
Потом замени три MERGE на один длинный MERGE (a:Person {name: 'Аня'})-[:KNOWS]->(b:Person {name: 'Глеб'}) и запусти дважды. Сколько стало Ань?
Проверь себя
Задание
Дальше
Мы умеем создавать граф и не плодить дубли. Пора научиться спрашивать. В следующем уроке возвращаемся к графу книжного клуба и разбираем MATCH и RETURN: что вернуть, как назвать колонки, как отсортировать и обрезать ответ.
домашка