Раздел 39 · Neo4j и графовые базы

OPTIONAL MATCH и работа с отсутствием

junior~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В графе клуба есть неровности: у Ильи и Яны не указан город, у Яны вообще нет связей, у Егора нет года рождения. В прошлом уроке такие участники просто выпадали из ответа. Но список «все участники и их города» без Ильи и Яны будет врать. Этот урок про то, как оставить строку, даже если часть паттерна не нашлась.

Список гостей с плюс-одним

Организатор вечеринки составляет список: каждый гость и его спутник. Если гость пришёл один, его не вычёркивают, а в графе «спутник» ставят прочерк. MATCH ведёт себя как строгий организатор: нет спутника, нет и гостя в списке. OPTIONAL MATCH ведёт себя по-доброму: гость остаётся, а вместо спутника прочерк, то есть null.

Разница на одном запросе

Сначала обычный MATCH:

MATCH (p:Person)-[:LIVES_IN]->(c:City)
RETURN p.name, c.name AS city
ORDER BY p.name;
+--------------------+
| p.name  | city     |
+--------------------+
| "Аня"   | "Москва" |
| "Борис" | "Москва" |
| "Вера"  | "Казань" |
| "Глеб"  | "Москва" |
| "Дина"  | "Казань" |
| "Егор"  | "Пермь"  |
| "Жанна" | "Пермь"  |
| "Зоя"   | "Пермь"  |
+--------------------+

8 rows

Восемь строк из десяти участников. Теперь вынесем город в OPTIONAL MATCH:

MATCH (p:Person)
OPTIONAL MATCH (p)-[:LIVES_IN]->(c:City)
RETURN p.name, c.name AS city
ORDER BY p.name;
+--------------------+
| p.name  | city     |
+--------------------+
| "Аня"   | "Москва" |
| "Борис" | "Москва" |
| "Вера"  | "Казань" |
| "Глеб"  | "Москва" |
| "Дина"  | "Казань" |
| "Егор"  | "Пермь"  |
| "Жанна" | "Пермь"  |
| "Зоя"   | "Пермь"  |
| "Илья"  | NULL     |
| "Яна"   | NULL     |
+--------------------+

10 rows

Все десять. Для Ильи и Яны переменная c получила null, и c.name тоже null: свойство от null это снова null. Если ты знаешь LEFT JOIN, это он и есть: левая часть сохраняется целиком.

Важно, где стоит OPTIONAL. Первый MATCH обязательный: он задаёт, какие строки вообще будут. OPTIONAL MATCH только дополняет каждую строку, если может.

coalesce: значение по умолчанию

null в отчёте выглядит неприятно. Функция coalesce возвращает первый аргумент, который не null:

MATCH (p:Person)
OPTIONAL MATCH (p)-[:LIVES_IN]->(c:City)
RETURN p.name, coalesce(c.name, 'не указан') AS city
ORDER BY p.name;
+-----------------------+
| p.name  | city        |
+-----------------------+
| "Аня"   | "Москва"    |
| "Борис" | "Москва"    |
| "Вера"  | "Казань"    |
| "Глеб"  | "Москва"    |
| "Дина"  | "Казань"    |
| "Егор"  | "Пермь"     |
| "Жанна" | "Пермь"     |
| "Зоя"   | "Пермь"     |
| "Илья"  | "не указан" |
| "Яна"   | "не указан" |
+-----------------------+

10 rows

Найти тех, у кого нет

OPTIONAL MATCH вместе с проверкой на null находит отсутствующее. Участники без города:

MATCH (p:Person)
OPTIONAL MATCH (p)-[:LIVES_IN]->(c:City)
WITH p, c
WHERE c IS NULL
RETURN p.name;
+--------+
| p.name |
+--------+
| "Илья" |
| "Яна"  |
+--------+

2 rows

WITH передаёт строки дальше, как конвейер, а WHERE после него фильтрует уже готовые строки. Тот же ответ проще получить через WHERE NOT (p)-[:LIVES_IN]->() из прошлого урока, но приём с OPTIONAL MATCH пригодится, когда нужно ещё что-то посчитать.

Подсчёт с нулями

Сколько книг прочитал каждый участник, включая тех, кто не прочитал ни одной:

MATCH (p:Person)
OPTIONAL MATCH (p)-[:READ]->(b:Book)
RETURN p.name, count(b) AS books
ORDER BY books, p.name;
+-----------------+
| p.name  | books |
+-----------------+
| "Яна"   | 0     |
| "Дина"  | 1     |
| "Егор"  | 1     |
| "Зоя"   | 1     |
| "Аня"   | 2     |
| "Борис" | 2     |
| "Вера"  | 2     |
| "Глеб"  | 2     |
| "Жанна" | 2     |
| "Илья"  | 2     |
+-----------------+

10 rows

Яна в ответе с нулём. count(b) считает только значения, которые не null, поэтому строка с пустым b дала ноль, а не единицу. Агрегаты разберём в уроке про count и collect.

Где ставить условие

Условие WHERE сразу после OPTIONAL MATCH относится к нему, а не ко всему запросу. Оно сужает, что считать совпадением, но не выбрасывает строки:

MATCH (p:Person)
OPTIONAL MATCH (p)-[:READ]->(b:Book)
WHERE b.year > 1960
RETURN p.name, collect(b.title) AS books
ORDER BY p.name;
+----------------------------------------------------------------------+
| p.name  | books                                                      |
+----------------------------------------------------------------------+
| "Аня"   | ["Пикник на обочине", "Мастер и Маргарита"]                |
| "Борис" | ["Мастер и Маргарита"]                                     |
| "Вера"  | ["Понедельник начинается в субботу", "Пикник на обочине"]  |
| "Глеб"  | ["Мастер и Маргарита", "Понедельник начинается в субботу"] |
| "Дина"  | []                                                         |
| "Егор"  | ["Пикник на обочине"]                                      |
| "Жанна" | []                                                         |
| "Зоя"   | ["Понедельник начинается в субботу"]                       |
| "Илья"  | ["Пикник на обочине", "Мастер и Маргарита"]                |
| "Яна"   | []                                                         |
+----------------------------------------------------------------------+

10 rows

У Дины и Жанны список пуст, у Бориса осталась одна книга из двух: «Обломов» и «Двенадцать стульев» изданы раньше 1960 года и не прошли условие, но сами участники остались. Если бы условие стояло после WITH, строки участников без подходящих книг пропали бы.

Правила работы с null

Коротко о том, как ведёт себя null, ты увидишь его ещё не раз:

  • любая арифметика с null даёт null: null + 1 это null;
  • любое сравнение с null даёт null, даже null = null;
  • WHERE пропускает только TRUE, поэтому null отсеивается;
  • IS NULL и IS NOT NULL всегда отвечают TRUE или FALSE;
  • агрегаты вроде count(x) и collect(x) пропускают null.

Это та же трёхзначная логика, что и в SQL, её подробно разбирает урок про NULL.

Попробуй сам

Запрос находит Яну и всё, что с ней связано. Связей у неё нет, и OPTIONAL MATCH оставил строку с null. Картинка покажет одинокий узел.

Убери слово OPTIONAL и выполни снова: что стало с ответом? Потом подставь вместо Яны Илью и посмотри, какие связи у него есть.

Проверь себя

Задание

Дальше

Кластер про чтение закончен: мы умеем находить, фильтровать и не терять строки. В следующем уроке начинаем ходить по графу на несколько шагов: друзья друзей, цепочки разных связей и круги.

домашка

Домашка