Паттерны Cypher: запрос как рисунок графа
Почему Cypher называют «ASCII-артом для графов» и как читается строка из скобок и стрелок.
Cypher — декларативный язык запросов Neo4j, в котором паттерн графа рисуется текстом: узлы — круглыми скобками
(), связи — стрелками со скобками-[]->.
Главная интуиция
Если в SQL вы описываете таблицы и условия соединения, то в Cypher вы рисуете форму подграфа, который хотите найти или создать. Это и есть его суперсила: запрос визуально похож на то, что вы ищете. Сравните рисунок на доске и строку Cypher:
Рисунок: (Алиса) ──ЗНАЕТ──> (Боб)
Cypher: (a:Person {name:'Алиса'})-[:ЗНАЕТ]->(b:Person {name:'Боб'})Почему это важно для обучения? Когда вы проектируете запрос к реляционной базе, вам приходится держать в голове две разные модели: бизнес-смысл («друзья Алисы») и техническую реализацию («таблица users, таблица friendships, два JOIN по внешним ключам»). В Cypher эти две модели совпадают: вы пишете прямо то, что нарисовали бы на салфетке. Чем меньше зазор между мыслью и кодом, тем меньше ошибок и тем быстрее вы рассуждаете о сложных обходах вроде «друзья друзей, которые лайкнули тот же пост».
Сразу зафиксируем словарь, чтобы дальше не путаться. Узел (node) — это сущность: человек, фильм, компания. Связь (relationship) — именованное ребро между двумя узлами: ЗНАЕТ, ACTED_IN, РАБОТАЕТ_В. Метка (label) — это «тип» узла, например :Person или :Movie; у одного узла может быть несколько меток. Свойство (property) — пара «ключ-значение» на узле или на связи: name:'Алиса', since:2020. Паттерн — это просто комбинация этих четырёх вещей, выписанная скобками и стрелками.
Анатомия паттерна
Разберём по частям паттерн (a:Person {name:'Алиса'})-[r:ЗНАЕТ]->(b:Person):
(a:Person {name:'Алиса'})— узел.a— переменная (чтобы сослаться позже),:Person— метка,{name:'Алиса'}— фильтр по свойству.-[r:ЗНАЕТ]->— связь.r— переменная связи,:ЗНАЕТ— тип,->— направление слева направо.(b:Person)— второй узел, тоже Person, без фильтра по свойствам.
Направление и его отсутствие
Стрелка задаёт направление обхода. Три варианта:
| Паттерн | Смысл |
(a)-[:ЗНАЕТ]->(b) | связь идёт от a к b |
(a)<-[:ЗНАЕТ]-(b) | связь идёт от b к a |
(a)-[:ЗНАЕТ]-(b) | любое направление (ненаправленный обход) |
Обратите внимание: данные всегда хранятся направленными, но искать можно без учёта направления, убрав стрелку. Это частая потребность: «кто как-либо связан с Алисой». Представьте соцсеть с типом связи :FOLLOWS (подписка). Направление здесь несёт смысл: «Алиса подписана на Боба» — это не то же самое, что «Боб подписан на Алису». А вот для связи :ДРУЖИТ, которая по своей природе взаимна, направление в данных есть (его не выкинуть), но при чтении мы почти всегда убираем стрелку, потому что дружба симметрична.
Когда направление меняет ответ
Маленький мысленный эксперимент. У нас есть факты: Алиса подписана на Боба, Боб подписан на Веру. Сравните два запроса-паттерна. (:Person {name:'Алиса'})-[:FOLLOWS]->(x) найдёт только Боба — тех, на кого подписана сама Алиса. А (:Person {name:'Алиса'})<-[:FOLLOWS]-(x) найдёт её подписчиков — тех, кто подписан на неё. Если же стрелку убрать совсем, (:Person {name:'Алиса'})-[:FOLLOWS]-(x) вернёт обе стороны разом. Одна стрелочка — три разных вопроса.
Цепочки и ветвления
Паттерны соединяются в цепочки и деревья — так выражаются многошаговые обходы:
(a:Person)-[:ЗНАЕТ]->(b:Person)-[:ЗНАЕТ]->(c:Person)Это «a знает b, b знает c» — наши друзья друзей. Заметьте, как естественно растёт длина обхода: добавили ещё один -[:ЗНАЕТ]->(d:Person) — и получили друзей друзей друзей. В реляционной базе каждый такой шаг — это ещё один JOIN, и на третьем-четвёртом запрос становится нечитаемым. В Cypher вы просто продолжаете рисовать линию. Именно поэтому графовые базы так хороши в задачах «найди путь» и «на сколько рукопожатий мы знакомы».
А вот ветвление от одного узла:
┌──[:ACTED_IN]──>(:Movie {title:'Матрица'})
(p:Person)│
└──[:ACTED_IN]──>(:Movie {title:'Джон Уик'})Записывается двумя паттернами через запятую: (p)-[:ACTED_IN]->(m1), (p)-[:ACTED_IN]->(m2). Переменная p повторяется в обоих кусках — и Cypher понимает, что речь об одном и том же человеке. Это ключевой приём: общая переменная «склеивает» отдельные паттерны в единый подграф. Так выражаются запросы вида «человек, снявшийся и в Матрице, и в Джоне Уике» — то есть Киану Ривз.
Якорь и ребро как фильтры
Полезно думать про каждую часть паттерна как про фильтр, который что-то отсекает. Метка :Movie отсекает всё, что не фильм. Свойство {released:1999} отсекает фильмы других лет. Тип связи :ACTED_IN отсекает связи других типов (скажем, :DIRECTED). Чем больше таких фильтров вы задали, тем меньше подграфов подойдёт под форму — и тем точнее ответ. Пустой паттерн (n) подходит вообще ко всему в базе; добавляя метки, типы и свойства, вы постепенно сужаете его до нужного.
Как работает под капотом
Получив паттерн, планировщик Cypher выбирает точку привязки (anchor) — узел, который дешевле всего найти (обычно по индексу: {name:'Алиса'} при наличии индекса на :Person(name)). От него он разворачивает обход по стрелкам, проверяя метки, типы и свойства как фильтры. Поэтому чем конкретнее заданы свойства якорного узла, тем меньше работы у движка.
Разберём это на конкретике. Пусть паттерн такой: (a:Person {name:'Алиса'})-[:ЗНАЕТ]->(b:Person). У планировщика два кандидата на якорь: узел a и узел b. Узел b — это «любой человек», его нельзя найти точечно, пришлось бы сканировать всех людей в базе. А узел a задан именем; если на :Person(name) есть индекс, движок прыгнет прямо к Алисе за один шаг. Поэтому планировщик выберет якорем a, найдёт Алису, а затем пройдётся только по её исходящим связям :ЗНАЕТ. Это и есть знаменитая «безындексная смежность» (index-free adjacency): чтобы узнать соседей узла, не нужно искать по всей базе — связи физически записаны рядом с узлом, как список указателей.
Отсюда практический вывод: стоимость графового обхода зависит не от размера всей базы, а от локальной плотности — сколько связей выходит из узлов на пути. Найти друзей конкретного человека одинаково быстро и в базе на тысячу человек, и в базе на сто миллионов, если у самого человека связей немного. В реляционной базе тот же запрос с JOIN-ами замедлялся бы по мере роста таблиц. Это фундаментальная причина, по которой графовые СУБД берут для соцсетей, рекомендаций и графов знаний.
Частые ошибки
- Забыть направление, когда оно важно.
(a)-[:FOLLOWS]-(b)найдёт подписку в любую сторону — иногда это не то, что нужно. - Перепутать скобки. Узлы — круглые
(), связи — квадратные[]. Это первое, что путают новички. - Лишние переменные. Если переменная
rилиbдальше не используется, её можно опустить — паттерн станет чище. - Считать, что метка — это таблица. Метка не ограничивает узел одним «типом»: у одного узла может быть и
:Person, и:Directorодновременно. Мыслить метками как жёсткими таблицами — реляционная привычка, мешающая в графе.
Итоги
- Cypher описывает форму подграфа текстом: узлы —
(), связи —-[]->. - Узел несёт переменную, метку и фильтр-свойства; связь — переменную, тип и направление.
- Стрелка задаёт направление; убрав её, ищем связь в любую сторону.
- Паттерны соединяются в цепочки и ветви; планировщик стартует с самого дешёвого якорного узла.