OPTIONAL MATCH и работа с отсутствием
Как вернуть узлы вместе с их связями, не теряя тех, у кого связей нет — графовый LEFT JOIN.
OPTIONAL MATCH — вариант MATCH, который при отсутствии совпадения не отбрасывает строку, а подставляет
nullвместо ненайденных переменных.
Это последний кирпич в фундаменте чтения. Мы уже умеем находить паттерны (MATCH), фильтровать их (WHERE) и оформлять результат (RETURN). Но во всех этих инструментах есть общая черта: они работают с тем, что точно есть. А реальные данные неполны: у человека может не быть аватарки, у заказа — отзыва, у фильма — указанного режиссёра. Если строить отчёт обычным MATCH, все такие «неполные» сущности тихо выпадут, и вы получите искажённую картину. OPTIONAL MATCH — это про умение работать с отсутствием как с полноправным случаем, а не как с ошибкой.
Проблема обычного MATCH
Обычный MATCH работает как INNER JOIN: если паттерн не нашёлся, строка исчезает целиком. Запрос «все люди и фильмы, где они снимались» потеряет людей, которые нигде не снимались:
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
RETURN p.name, m.titleРежиссёры без актёрских ролей сюда не попадут. А нам, может, нужны все люди — с фильмами или без. Это типичная задача отчётности: «список всех сотрудников и их проектов, включая тех, кто пока без проекта», «все товары и их отзывы, даже если отзывов ноль». Везде, где требуется «полный левый список плюс то, что к нему прицепилось», обычный MATCH подводит, потому что молча выкидывает бездетные строки. Важно осознать: он делает это тихо, без ошибки — вы просто недосчитаетесь людей и можете долго не замечать пропажу.
OPTIONAL MATCH спешит на помощь
Сначала находим всех людей обычным MATCH, потом опционально цепляем фильмы:
MATCH (p:Person)
OPTIONAL MATCH (p)-[:ACTED_IN]->(m:Movie)
RETURN p.name, m.titleТеперь в результате есть все люди. У тех, кто нигде не снимался, m.title будет null. Это прямой аналог SQL LEFT JOIN.
Сопоставим формы напрямую, чтобы закрепить аналогию. Слева — Cypher, справа — его SQL-двойник:
SELECT p.name, m.title
FROM person p
LEFT JOIN acted_in a ON a.person_id = p.id
LEFT JOIN movie m ON m.id = a.movie_id;Видно, что в SQL «левизна» выражается ключевым словом LEFT в JOIN, а в Cypher — ключевым словом OPTIONAL перед MATCH. Идея одна: ведущая сторона (люди) сохраняется целиком, а ведомая (фильмы) подцепляется, где есть, и заполняется null, где нет.
Обработка null через coalesce
Чтобы вместо null показать заглушку, есть функция coalesce — возвращает первый ненулевой аргумент:
MATCH (p:Person)
OPTIONAL MATCH (p)-[:ACTED_IN]->(m:Movie)
RETURN p.name, coalesce(m.title, '— нет ролей —') AS filmИмя coalesce переводится примерно как «слиться воедино»: функция перебирает аргументы слева направо и возвращает первый, который не null. Записать можно и цепочкой: coalesce(p.nickname, p.name, 'аноним') вернёт ник, если он есть, иначе имя, иначе заглушку. Это рабочий приём для подстановки значений по умолчанию прямо в RETURN, без ветвлений.
Считаем с учётом нулей
OPTIONAL MATCH незаменим для подсчётов «у кого сколько» с нулями. «Сколько фильмов у каждого человека, включая тех, у кого ноль»:
MATCH (p:Person)
OPTIONAL MATCH (p)-[:ACTED_IN]->(m:Movie)
RETURN p.name, count(m) AS films
ORDER BY films DESCХитрость: count(m) считает только не-null значения, поэтому у бесфильмовых людей честно выйдет 0, а сами они из результата не пропадут. Сравните с ловушкой count(*): звёздочка считает строки, а у бесфильмового человека строка-то есть (одна, с null вместо фильма), так что count(*) выдал бы ему 1 вместо честного 0. Поэтому при подсчётах после OPTIONAL MATCH считайте именно опциональную переменную — count(m), а не count(*). Это частый источник «магической единицы» в отчётах.
Как работает под капотом
OPTIONAL MATCH выполняется как обычный обход, но с гарантией: если для текущей строки совпадений нет, строка не удаляется, а переменные опционального паттерна становятся null. Поэтому порядок важен: сначала идёт обязательная часть (что точно должно быть), затем опциональная (что может быть). Поменяете местами — и смысл LEFT JOIN сломается. По сути, это управление тем, какая сторона «ведущая».
Есть и тонкость «всё или ничего» внутри одного OPTIONAL MATCH: либо весь его паттерн совпал, либо все его переменные становятся null разом. Нельзя получить «полусовпадение». Так, OPTIONAL MATCH (p)-[:ACTED_IN]->(m:Movie)<-[:DIRECTED]-(d:Person) для человека, который снимался, но в фильме без указанного режиссёра, не даст совпадения вовсе — и m, и d окажутся null, хотя фильм формально есть. Если важно получить фильм даже без режиссёра, разнесите это на два отдельных OPTIONAL MATCH: первый цепляет фильм, второй — опционально режиссёра к найденному фильму. Дробление длинного опционального паттерна на короткие — стандартный способ управлять тем, что именно «обнуляется вместе».
Частые ошибки
- Ставить WHERE на опциональную переменную в основном блоке. Условие на
mпосле OPTIONAL MATCH снова отбросит null-строки — пишите фильтр внутри опциональной части. - Путать порядок. Обязательный MATCH — первым, OPTIONAL — после; иначе теряете нужные узлы.
- Забывать про null в агрегатах.
count(m)игнорирует null (даёт 0), ноcollect(m)соберёт список без null — это обычно то, что нужно.
Итоги
OPTIONAL MATCH— графовый LEFT JOIN: сохраняет строку, подставляя null вместо ненайденного.- Порядок строг: обязательная часть — первой, опциональная — после.
coalesceзаменяет null заглушкой;countпо null даёт честный 0.- Фильтры на опциональные переменные ставьте внутри OPTIONAL-части, иначе вернёте INNER-семантику.