Neo4j / Graph DBs concept page. Native graph storage (nodes/edges as fixed-size records, doubly-linked rel lists, index-free adjacency). Cypher pattern matching. Replication via Raft (Causal Cluster). Compared with Postgres recursive CTE and vector embeddings. Five scenarios: friends-of-friends 3-hop traversal vs SQL self-joins, weighted shortest path (bidirectional BFS / Dijkstra), fraud ring detection (cycles of length 3-5), sharding pain (cross-shard traversal latency variance), operational gotchas (unbounded variable-length paths). Two ADRs: Neo4j vs Postgres CTE vs vector embeddings, replicate-the-full-graph vs vertex partitioning.
Graph DB — это storage, где relationships first-class. Найти друзей-друзей или поймать цикл переводов «A → B → C → A» в реляционной базе — это N-кратный self-join поверх B-tree-индекса. На дружбе с типичным средним degree 200 уже на 3-м хопе планировщик строит миллиарды промежуточных кортежей, спиливается в hash join на диск и отдаёт ответ за секунды вместо миллисекунд.
В native graph DB (Neo4j, Memgraph) нода и связь — это fixed-size record на диске, у ноды есть указатель first_rel_id на голову двусвязного списка её рёбер. Обход соседа = pointer chasing, O(1) на хоп независимо от размера графа. Это называется index-free adjacency и это вся суть.
Используется там, где запросы по сути своей структурные: friends-of-friends ≥ 2 хопа, shortest path, обнаружение циклов / fraud rings, permission-графы (Google Zanzibar, AWS IAM), bill-of-materials, knowledge graphs, routing. Не используется для KV, time-series, реляционки — overkill и хуже по операциям.
Три способа спросить «кто связан с Alice через 3 хопа»:
WITH RECURSIVE — каждый хоп = self-join friendships по индексу. Идёт через планировщик и буферный кеш. Прекрасно на 1 хопе, терпимо на 2, катастрофа на 3+.MATCH (a)-[:FRIENDS*1..3]->(fof) — компилируется в обход дерева. Ходит по физическим указателям в rel-store. Нет join'ов, нет промежуточных таблиц, нет планировщика — только BFS с дедупликацией.Graph DB выигрывает не потому что быстрее на операцию — а потому что уходит от join'а вообще. Это другой способ хранения, не другой движок поверх того же хранения.
Distribution: «зашардить граф» звучит просто, но cut'ы — NP-hard, cross-shard рёбра неизбежны (в любом реальном соцграфе есть «гигантская компонента»). Поэтому промышленный default — replicate-the-full-graph (Neo4j Causal Cluster, Memgraph HA): один Raft-лидер на запись, N follower'ов на чтение, каждый держит весь граф. Sharding (TigerGraph, JanusGraph) включают только когда граф перестаёт помещаться в один большой узел (~10B рёбер на 256-512 GB RAM + NVMe).
Четыре группы:
node-store (fixed-size записи), rel-store (двусвязный список рёбер), prop-store + string-store. Цепочка node-store → rel-store → prop-store показывает один pointer chase для одного обхода соседа.WITH RECURSIVE и self-join на friendships. Намеренно рядом, чтобы видеть разницу в стоимости.Edges — физические соединения: app ↔ cluster по Bolt, leader → followers по Raft, обращения к storage. Reverse-анимация по тем же edges изображает ответы.
Friends-of-friends на 3 хопа. В Neo4j: lookup стартовой ноды по индексу → first_rel_id → walk двусвязного списка. На каждом хопе расширяемся, дедуплицируем, идём дальше. ~24 тысячи уникальных FoF за 5 мс. В Postgres тот же запрос разворачивается в 3 self-join'а: 200 → 40K → 8M промежуточных строк, hash join спиливается на диск, ~480 мс p50, 2 с p99, lock contention на индексе. Победа графовой БД не в константе, а в избегании join'а.
shortestPath((a)-[:FRIENDS*..6]-(b)) — bidirectional BFS, расширяем frontier с обоих концов одновременно. Встретились на середине → путь короче в среднем в 2× раз меньше посещённых нод по сравнению с однонаправленным BFS. Для weighted-графов (road network с distance_km) — Dijkstra/A* через Graph Data Science library. Заметка: Google Maps не использует Neo4j — у них in-memory road graph с hierarchical contraction (CRP). Тот же класс алгоритмов, purpose-built engine.
Чистый графовый запрос, который embeddings не могут. Ищем циклы 3-5 хопов между accounts, где все рёбра :TRANSFER с amount > 9000 (structuring threshold). Cypher: MATCH (a:Account)-[:TRANSFER*3..5]->(a) WHERE all(r IN relationships(p) WHERE r.amount > 9000). Эмбеддинги кодируют «account A похож на account B», но не «A → B → C → A». SQL 5-уровневый self-join даёт O(N⁵) промежуточных строк, планировщик сдаётся. Реально используется: eBay, FinCEN (анализ SAR), Panama / Paradise Papers (журналистский анализ через Neo4j).
Гипотетический случай: граф зашардили по user_id hash на 4 узла. FoF теперь: Alice на shard-1, её 200 друзей — на shards 1-4 рандомно. Хоп 2 = 200 cross-shard RPC. Параллельно — 5 мс, но 200× амплификация QPS. Последовательно — 200 мс. p50 = 8 мс, p99 = 400 мс — bimodal latency, SLO написать невозможно. Fix-ы: community-aware partitioning (METIS/Kahip, NP-hard, требует rebalance с ростом графа) или просто replicate-the-full-graph и принять single-leader writes ~20K/s. LinkedIn, eBay, Adobe идут вторым путём до ~10B рёбер.
Cypher достаточно выразителен, чтобы убить кластер одним запросом. MATCH (a)-[:FRIENDS*]-(b) без bound и без LIMIT → BFS с backtracking перечисляет все пути (не ноды), на связном соцграфе это миллиарды путей, heap exhausted за 30 секунд, leader падает, Raft re-election, кратковременная недоступность. Production-правила: всегда *1..6 максимум (для online — *1..3), всегда LIMIT на терминальный RETURN, PROFILE каждый новый запрос (искать Expand(All) без upper bound и AllNodesScan без индекса), dbms.memory.heap = 31g (compressed oops cliff на 32g), CREATE INDEX на старт-нодах иначе full label scan.
ADR-001: Native graph DB vs Postgres recursive CTE vs vector embeddings.
Не «или-или», а right tool per query class. Postgres остаётся system of record (users, friendships, transactions — relational integrity важна). Neo4j — derived/projection store, кормится через CDC, отвечает на структурные вопросы (FoF ≥ 2 хопа, shortest path, cycle detection, Louvain community detection, permission-графы). Vector store (pgvector до ~50M векторов, потом Qdrant) — semantic similarity, игнорирующая explicit edges. Рекомендации — гибрид: candidate generation через vector top-200 → re-rank через graph features (mutual friends count, shortest path length, Personalized PageRank score).
Жёсткие правила: никогда не использовать Neo4j как primary store для не-графовых данных (users.email, orders.total) — лучше платить join-penalty в Postgres, чем операционную стоимость двух источников правды. Никогда не писать recursive CTE глубже 2 хопов в production hot path — этот запрос промотируется в Neo4j или пре-материализуется.
ADR-002: Replicate-the-full-graph (Causal Cluster) vs vertex-partitioned sharding.
Default — replicate-the-full-graph, пока провабельно не перестало помещаться. Sweet spot: до ~10B рёбер на 256-512 GB RAM + NVMe, реплицировать на 3-5 follower'ов (Raft quorum), reads масштабируются линейно, writes упираются в leader (~20K/s mixed workload, меньше для тяжёлых traversal'ов).
К vertex partitioning переходить, когда (а) write throughput устойчиво > 50K/s, либо (б) граф пересёк ~50B рёбер и не влезает даже с NVMe spill. При этом: брать алгоритм, уважающий locality (community-aware, не random hash), готовиться к p99 в 5-10× от p50, проектировать запросы с shard-aware хинтами, и закладывать на порядок больше операционной сложности (split-brain, partition rebalance, cross-shard transactions).
Анти-паттерн: стартовать на TigerGraph/JanusGraph «to be future-proof» до реального scale-проблема — платишь операционный налог с первого дня за ёмкость, которая не нужна.
LIMIT на open-ended traversal — explosion на миллионы paths.*1..N с большим N + cartesian product — backtracking легко даёт миллиарды путей.PROFILE — медленный запрос замечается уже в проде после OOM лидера.tenant_id), графовая структура работает против тебя.