В графе клуба есть неровности: у Ильи и Яны не указан город, у Яны вообще нет связей, у Егора нет года рождения. В прошлом уроке такие участники просто выпадали из ответа. Но список «все участники и их города» без Ильи и Яны будет врать. Этот урок про то, как оставить строку, даже если часть паттерна не нашлась.
Список гостей с плюс-одним
Организатор вечеринки составляет список: каждый гость и его спутник. Если гость пришёл один, его не вычёркивают, а в графе «спутник» ставят прочерк. 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 и выполни снова: что стало с ответом? Потом подставь вместо Яны Илью и посмотри, какие связи у него есть.
Проверь себя
Задание
Дальше
Кластер про чтение закончен: мы умеем находить, фильтровать и не терять строки. В следующем уроке начинаем ходить по графу на несколько шагов: друзья друзей, цепочки разных связей и круги.
домашка