MATCH и RETURN: находим и возвращаем

Базовая пара любого чтения в Cypher: найти подграф и решить, что из него вернуть.

MATCH ищет в графе подграфы заданной формы, RETURN определяет, какие узлы, связи и свойства попадут в результат.

Простейший запрос на чтение

MATCH описывает паттерн, RETURN — что отдать. Найдём все фильмы:

MATCH (m:Movie)
RETURN m.title, m.released

Это вернёт таблицу из двух колонок. Можно вернуть и сам узел целиком (RETURN m) — тогда Browser нарисует граф.

Сразу полезное различение, которое экономит много недоумения. RETURN m возвращает узел как объект — со всеми его свойствами, метками и внутренним id; визуально в Neo4j Browser это кружок графа. RETURN m.title возвращает одно скалярное значение — строку. Когда вам нужен табличный отчёт (выгрузить в CSV, показать в таблице), возвращайте конкретные свойства. Когда нужно визуально исследовать граф или передать узел дальше в коде драйвера — возвращайте сам узел. Новички часто пишут RETURN m, ждут аккуратную колонку с названиями, а получают объекты — теперь вы знаете почему.

Псевдонимы и сортировка

AS задаёт имя колонки, ORDER BY сортирует, LIMIT ограничивает, SKIP пропускает — всё как в SQL:

MATCH (m:Movie)
RETURN m.title AS film, m.released AS year
ORDER BY year DESC
LIMIT 5

Это «пять самых свежих фильмов». Сравним с SQL — структура знакома:

SELECT title AS film, released AS year
FROM movie
ORDER BY year DESC
LIMIT 5;

Парность очевидна: RETURNSELECT, MATCH (m:Movie)FROM movie, а AS, ORDER BY, LIMIT работают один в один. SKIP n пропускает первые n строк — связка SKIP + LIMIT даёт постраничную выдачу (пагинацию): SKIP 20 LIMIT 10 — это «третья страница по десять элементов». Этой части языка вообще не нужно учиться заново, если вы знаете SQL: переносите привычки как есть.

Возврат по связям

Главная разница с SQL начинается на связях. «Кто снимался в Матрице»:

MATCH (p:Person)-[:ACTED_IN]->(m:Movie {title:'The Matrix'})
RETURN p.name AS actor
ORDER BY actor

Никакого JOIN — просто паттерн со стрелкой. Вдумайтесь, насколько это короче. В SQL тот же вопрос потребовал бы трёх таблиц (person, acted_in, movie) и двух JOIN-ов с условиями по ключам; легко ошибиться в направлении связи или забыть условие. В Cypher вы буквально рисуете «человек снимался в фильме с таким названием» и сразу читаете результат. А «фильмы и их актёры списком» соберём чуть позже через collect.

Агрегаты без GROUP BY

Ещё одна приятная неожиданность для тех, кто из SQL: в Cypher нет отдельного GROUP BY. Группировка происходит автоматически по тем полям в RETURN, которые не являются агрегатами. Например, «сколько актёров в каждом фильме»:

MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
RETURN m.title AS film, count(p) AS actors
ORDER BY actors DESC

Здесь m.title — не агрегат, по нему и идёт неявная группировка; count(p) считает актёров в каждой группе. Функция collect(p.name) вместо count собрала бы имена в список — так получают «фильм и список его актёров одной строкой».

DISTINCT против дублей

Один узел может приходить по нескольким путям, и тогда в результате будут повторы. DISTINCT их убирает:

MATCH (p:Person)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(co:Person)
RETURN DISTINCT co.name AS colleague

Здесь мы идём «актёр → фильм → другой актёр того же фильма»; без DISTINCT коллега, снявшийся с человеком в нескольких фильмах, повторился бы.

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

MATCH порождает поток строк-совпадений: каждая строка — это один найденный экземпляр паттерна с привязанными переменными. RETURN, ORDER BY, LIMIT, DISTINCT — операторы конвейера, которые этот поток преобразуют. Поэтому Cypher читается как конвейер: «найди паттерны → отфильтруй → отсортируй → ограничь → верни». LIMIT, кстати, позволяет планировщику остановить обход раньше, не материализуя все совпадения.

Здесь кроется тонкость, которая объясняет неожиданные числа в результатах. Если паттерн нашёлся тремя разными способами (скажем, человек снялся в трёх фильмах), MATCH выдаст три строки для этого человека — по одной на каждый вариант привязки переменных. Поэтому, когда вы пишете MATCH (p:Person)-[:ACTED_IN]->(m) RETURN p.name, имя плодовитого актёра появится столько раз, во скольких фильмах он снялся. Это не баг — это прямое следствие того, что строка соответствует совпадению паттерна, а не узлу. Понимание этого снимает 90% вопросов «откуда взялись повторы».

Про порядок операторов конвейера. ORDER BY и DISTINCT — операции блокирующие: чтобы отсортировать или выкинуть дубли, движку нужно увидеть весь поток целиком, поэтому он не может отдать первую строку, пока не получил последнюю. А вот LIMIT без сортировки — потоковая операция: набрал нужное число строк и остановился, не трогая остальное. Отсюда практический совет: LIMIT 10 на сыром обходе почти бесплатен, а ORDER BY ... LIMIT 10 сначала отсортирует всё, и только потом возьмёт десятку — это уже полноценная работа над всем потоком.

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

  • Забыть DISTINCT при обходах туда-обратно. Пути часто дублируют узлы — результат раздувается.
  • Возвращать узел, ожидая таблицу. RETURN m даёт граф/объект; для табличного отчёта возвращайте свойства m.title.
  • Сортировать по невозвращённому полю в старых версиях. Лучше явно вычислять/возвращать поле, по которому сортируете.

Итоги

  • MATCH ищет паттерн, RETURN выбирает, что вернуть; вместе — основа чтения.
  • AS, ORDER BY, SKIP, LIMIT работают как в SQL.
  • Обход по связям заменяет JOIN — просто рисуем стрелки.
  • DISTINCT убирает дубли, неизбежные при обходах по нескольким путям.
Проверьте себя
1. Что делает MATCH (p:Person)-[:ACTED_IN]->(m:Movie {title:'X'}) RETURN p.name?
AУдаляет актёров фильма X
BВозвращает имена людей, снявшихся в фильме X
CСоздаёт связь ACTED_IN
DСчитает фильмы
2. Зачем нужен DISTINCT в обходах вида актёр→фильм→коллега?
AДля сортировки
BЧтобы убрать повторы узлов, приходящих по нескольким путям
CЧтобы ускорить запись
DОн обязателен в каждом RETURN
3. Чем поток MATCH похож на конвейер?
AОн сразу пишет на диск
BКаждая строка — экземпляр паттерна, далее RETURN/ORDER BY/LIMIT преобразуют этот поток
CОн не возвращает строк
DОн всегда возвращает граф