WHERE: фильтрация совпадений

Как сузить найденные паттерны условиями — от простых сравнений до фильтра по наличию связи.

WHERE — предикат, отбрасывающий те совпадения MATCH, которые не удовлетворяют условию; работает по свойствам, меткам и даже по наличию подграфов.

Свойство в паттерне или в WHERE

Фильтр по равенству можно записать двумя способами — оба верны:

// inline в паттерне
MATCH (m:Movie {released:1999}) RETURN m.title

// через WHERE
MATCH (m:Movie) WHERE m.released = 1999 RETURN m.title

Inline удобен для простого равенства, а WHERE нужен для всего остального: диапазонов, сравнений, логики. Есть и стилистическое соображение: когда фильтров на узле много, инлайновая форма {a:1, b:2, c:3} загромождает паттерн и мешает разглядеть его форму. Вынесли условия в WHERE — и паттерн снова читается как чистый рисунок графа, а условия аккуратно лежат отдельным блоком. Хорошее эмпирическое правило: одно простое равенство, определяющее что это за узел, оставляйте в паттерне; всё остальное — в WHERE.

Сравнения, диапазоны, списки

Операторы привычные. Экранируем угловые скобки в записи, но в редакторе вы пишете обычные < и >:

MATCH (m:Movie)
WHERE m.released >= 2000 AND m.released < 2010
RETURN m.title, m.released

IN проверяет вхождение в список, STARTS WITH / CONTAINS / ENDS WITH — строковые предикаты:

MATCH (p:Person)
WHERE p.name STARTS WITH 'Tom' OR p.born IN [1956, 1964]
RETURN p.name, p.born

Логику комбинируют через AND, OR, NOT и скобки — приоритет привычный, AND связывает сильнее OR, скобки расставляйте при малейшем сомнении. Маленькая памятка по операторам, которую стоит держать перед глазами (помните: в реальном редакторе вы пишете обычные угловые скобки, здесь они экранированы для отображения):

ОператорСмысл
=равно
<>не равно
<, <=, >, >=сравнения чисел и дат
INвхождение в список
STARTS WITH / CONTAINS / ENDS WITHстроковые предикаты
=~совпадение с регулярным выражением

Обратите внимание на «не равно»: в Cypher это <>, а не !=. Знак =~ включает регулярные выражения — например, WHERE p.name =~ '(?i)tom.*' найдёт всех Томов без учёта регистра.

Фильтр по наличию связи

Вот где Cypher красивее SQL: условие можно ставить на наличие подграфа. «Режиссёры, которые сами нигде не снимались»:

MATCH (p:Person)-[:DIRECTED]->(:Movie)
WHERE NOT (p)-[:ACTED_IN]->(:Movie)
RETURN DISTINCT p.name

Здесь NOT (p)-[:ACTED_IN]->(:Movie) — это паттерн-предикат: «не существует такой связи». В новых версиях то же пишут через WHERE NOT EXISTS { (p)-[:ACTED_IN]->(:Movie) }.

Это та самая возможность, ради которой многие и переходят на графовые базы. Сформулируйте на SQL вопрос «режиссёры, которые сами нигде не снимались» — и вы получите подзапрос с NOT EXISTS или LEFT JOIN ... IS NULL поверх таблицы-связки, который не всякий джуниор напишет с первого раза. В Cypher вы просто дорисовываете к паттерну фразу «и при этом нет вот такой стрелки». Условие живёт на форме графа, а не на значениях столбцов. Положительный вариант тоже бывает нужен: WHERE EXISTS { (p)-[:DIRECTED]->(:Movie) } оставит только тех, у кого режиссёрская связь есть. Связка EXISTS/NOT EXISTS над подпаттерном — один из самых выразительных инструментов Cypher.

Фильтр по метке и null

Метку тоже можно проверять в WHERE: WHERE p:Director. А IS NULL / IS NOT NULL ловят отсутствие свойства — в графе у узлов схема гибкая, и свойства может просто не быть:

MATCH (p:Person)
WHERE p.twitter IS NOT NULL
RETURN p.name, p.twitter

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

WHERE — это фильтр в конвейере: он отбрасывает строки потока, не прошедшие предикат. Важно для производительности, что планировщик старается протолкнуть фильтр как можно раньше (predicate pushdown) — например, применить условие на свойство сразу при сканировании узлов по индексу, а не после полного обхода. Поэтому условие на индексированное свойство якорного узла почти бесплатно, а условие на дальний узел после большого обхода — дороже.

Отдельно стоит понять трёхзначную логику Cypher, унаследованную от SQL. Сравнение с null даёт не true и не false, а третье значение — null («неизвестно»). Предикат WHERE пропускает строку, только если итог истинен; «неизвестно» трактуется как «не пропускать». Отсюда классическая ловушка: WHERE p.age > 18 молча выкинет всех, у кого свойства age вообще нет, потому что null > 18 — это null, а не false. То же с отрицанием: WHERE NOT p.age > 18 тоже не вернёт безвозрастных, ведь NOT null по-прежнему null. Если хотите явно учесть таких, добавляйте OR p.age IS NULL. В графе, где схема гибкая и свойства у узлов могут отсутствовать, эта тонкость встречается куда чаще, чем в строго-табличном SQL.

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

  • Путать = и присваивание. В WHERE сравнение — это = (одинарное), как в SQL, не ==.
  • Сравнивать с несуществующим свойством. WHERE p.age > 18 молча отбросит узлы, у которых свойства age вообще нет (там null).
  • Тяжёлый NOT-паттерн на большом результате. Проверка «нет такой связи» по миллионам узлов дорогая — фильтруйте раньше.

Итоги

  • WHERE фильтрует совпадения по свойствам, диапазонам, спискам и строковым предикатам.
  • Уникальная сила Cypher — фильтр по наличию/отсутствию связи (паттерн-предикат, EXISTS).
  • IS NULL/IS NOT NULL важны: в графе свойство может просто отсутствовать.
  • Планировщик проталкивает фильтр раньше; условие на индексированный якорь — почти бесплатно.
Проверьте себя
1. Как в Cypher найти режиссёров, которые нигде не снимались?
AMATCH ... WHERE p.acted = false
BMATCH (p)-[:DIRECTED]->() WHERE NOT (p)-[:ACTED_IN]->() ...
CЭто невозможно в Cypher
DТолько через два отдельных запроса
2. Что вернёт WHERE p.age > 18 для узла, у которого свойства age вообще нет?
AОшибку
BУзел будет отброшен (сравнение с null ложно)
CУзел попадёт в результат
Dage примет значение 0
3. Что такое predicate pushdown в контексте WHERE?
AОткладывание фильтра на конец
BПроталкивание условия как можно раньше, чтобы фильтровать при сканировании по индексу
CУдаление WHERE планировщиком
DКэширование результата