Графовые
Графовые базы данных — хранение вершин (сущностей) и рёбер (связей), где связи являются первоклассными гражданами. Эффективный обход графа вместо JOIN, производительность на глубине 2–5 уровней остаётся стабильной. Популярные системы: Neo4j (Cypher, ACID, индексы), JanusGraph (масштабирование, Gremlin), Amazon Neptune (Cypher/Gremlin/SPARQL), ArangoDB (multi-model). Основные паттерны запросов: поиск соседей, поиск путей (кратчайший путь), агрегации, рекомендации, обнаружение циклов. Идеальные сценарии: социальные сети, системы рекомендаций, обнаружение мошенничества, управление доступом, логистика, графы знаний. Сравнение с реляционным подходом, проектирование схемы, индексы, типичные ошибки.
Введение: Связи имеют значение
Представьте, что вы пытаетесь ответить на вопрос: “Как связаны Иван и Мария?” В социальной сети это может быть “Иван — друг Петра, Петр — коллега Анны, Анна — сестра Марии”. В банковской системе — “Счет Ивана перевел деньги на счет Петра, счет Петра — на счет Анны, счет Анны — на счет Марии”. В логистике — “Груз из Москвы в СПб через Тверь и Новгород”.
В реляционной базе данных такой вопрос означает множество JOIN: таблица людей, таблица связей, снова таблица людей, снова связи… Чем глубже связь, тем больше JOIN и тем медленнее запрос. На глубине 5-6 уровней производительность становится катастрофической.
Графовые базы данных созданы для таких вопросов. Они хранят не таблицы, а графы: вершины (узлы, сущности) и ребра (связи). Поиск связей — это не JOIN, а обход графа. Глубина не важна — производительность остается стабильной.
Графовая база данных — это база данных, которая использует графовую структуру для семантических запросов. Она хранит не просто данные, а данные в контексте их связей. Связи — первоклассные граждане, такие же важные, как и сами сущности.
Основные понятия
Вершина (Vertex, Node, Entity)
Вершина — это сущность, объект. Пользователь, товар, город, банковский счет.
Пример вершины в Neo4j:
// Создание вершины "человек"
CREATE (ivan:Person {name: "Иван", age: 30, email: "ivan@example.com"})
// Вершина имеет:
// - Метку (Label): Person (тип вершины)
// - Свойства (Properties): name, age, email
// - Уникальный идентификатор (Id): создается автоматическиРебро (Edge, Relationship, Link)
Ребро — это связь между вершинами. У ребра есть направление, тип и свойства.
// Создание ребра между Иваном и Петром
MATCH (ivan:Person {name: "Иван"})
MATCH (petr:Person {name: "Петр"})
CREATE (ivan)-[:FRIEND {since: 2020, strength: 0.8}]->(petr)
// Ребро имеет:
// - Направление: Иван → Петр
// - Тип: FRIEND
// - Свойства: since, strengthСвойства (Properties)
И вершины, и ребра могут иметь свойства — пары “ключ-значение”, аналогичные столбцам в реляционных таблицах.
Метки (Labels)
Вершины группируются по меткам (аналог таблиц). Одна вершина может иметь несколько меток.
CREATE (ivan:Person:Employee:Customer {name: "Иван"})
// Иван — одновременно и Person, и Employee, и CustomerПочему графовые БД быстрее для связей
Реляционный подход (JOIN)
Вопрос: “Друзья друзей Ивана”
Реляционная схема:
CREATE TABLE persons (
id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE friendships (
person_id INT,
friend_id INT,
FOREIGN KEY (person_id) REFERENCES persons(id),
FOREIGN KEY (friend_id) REFERENCES persons(id)
);Запрос для друзей друзей:
-- Друзья друзей Ивана (2 уровня)
SELECT DISTINCT fof.id, fof.name
FROM persons p
JOIN friendships f1 ON p.id = f1.person_id
JOIN persons f ON f1.friend_id = f.id
JOIN friendships f2 ON f.id = f2.person_id
JOIN persons fof ON f2.friend_id = fof.id
WHERE p.name = 'Иван'
AND fof.id != p.id;Проблемы:
- Количество JOIN растет с глубиной запроса (для 5 уровней — 10 JOIN)
- Сложность запроса растет экспоненциально
- Оптимизатор может выбрать неоптимальный план
- Производительность падает с глубиной
Графовый подход (Обход графа)
// Друзья друзей Ивана (2 уровня)
MATCH (ivan:Person {name: "Иван"})-[:FRIEND*2]-(fof)
RETURN DISTINCT fofЧто происходит под капотом:
- Найти вершину “Иван” по индексу (быстро)
- Пройти по ребрам типа FRIEND на 2 уровня (локально, без JOIN)
- Собрать уникальные вершины
Сложность: O(степень^глубина), но каждый шаг — это локальный обход, а не глобальный JOIN. Для типичных графов (ограниченная степень) это очень быстро.
Языки запросов для графовых БД
Cypher (Neo4j)
Самый популярный язык для графовых БД. Вдохновлен SQL, но с ASCII-синтаксисом для графов.
-- Поиск вершин
MATCH (p:Person {name: "Иван"}) RETURN p
-- Поиск связей
MATCH (ivan:Person {name: "Иван"})-[:FRIEND]-(friend)
RETURN friend.name
-- Пути произвольной длины
MATCH path = (ivan:Person {name: "Иван"})-[:FRIEND*1..5]-(friend)
RETURN path
-- Агрегации
MATCH (ivan:Person {name: "Иван"})-[:FRIEND]-(friend)
RETURN friend.city, COUNT(*) AS friends_count
-- Вложенные запросы (аналог JOIN)
MATCH (ivan:Person {name: "Иван"})
OPTIONAL MATCH (ivan)-[:FRIEND]-(friend)
RETURN ivan, collect(friend) AS friends
-- Создание
CREATE (ivan:Person {name: "Иван", age: 30})
CREATE (ivan)-[:FRIEND {since: 2020}]->(petr)
-- Обновление
MATCH (ivan:Person {name: "Иван"})
SET ivan.age = 31
-- Удаление
MATCH (ivan:Person {name: "Иван"})
DETACH DELETE ivanGremlin (Apache TinkerPop)
Язык обхода графа, используемый в JanusGraph, Amazon Neptune, Cosmos DB.
// Найти друзей друзей Ивана
g.V().has('Person', 'name', 'Иван')
.repeat(out('FRIEND'))
.times(2)
.dedup()
.values('name')SPARQL (RDF графы)
Язык для семантических графов (RDF).
SELECT ?friendName WHERE {
?ivan rdf:type :Person .
?ivan :name "Иван" .
?ivan :friend ?friend .
?friend :name ?friendName .
}Популярные графовые базы данных
Neo4j
Самая популярная графовая БД. Использует Cypher, поддерживает ACID.
-- Создание индекса
CREATE INDEX person_name FOR (p:Person) ON (p.name)
-- Уникальное ограничение
CREATE CONSTRAINT person_email_unique FOR (p:Person) REQUIRE p.email IS UNIQUE
-- Полнотекстовый поиск
CALL db.index.fulltext.createNodeIndex('person_names', ['Person'], ['name'])
-- Поиск кратчайшего пути
MATCH p = shortestPath((ivan:Person {name: "Иван"})-[:FRIEND*]-(anna:Person {name: "Анна"}))
RETURN pХарактеристики Neo4j:
- ACID-транзакции
- Индексы (B-Tree, full-text, spatial)
- Встроенный язык Cypher
- Визуализация графов (Neo4j Browser)
- Кластеризация (causal clustering)
Когда использовать:
- Социальные сети
- Системы рекомендаций
- Обнаружение мошенничества
- Управление доступом (графы прав)
- Поиск путей (логистика)
Apache JanusGraph
Распределенная графовая БД на основе Cassandra/HBase.
// JanusGraph использует Gremlin
graph = JanusGraphFactory.open('conf/janusgraph-cassandra.properties')
g = graph.traversal()
// Создание схемы (опционально)
mgmt = graph.openManagement()
person = mgmt.makeVertexLabel('Person').make()
name = mgmt.makePropertyKey('name').dataType(String.class).make()
mgmt.buildIndex('byName', Vertex.class).addKey(name).buildCompositeIndex()
mgmt.commit()
// Вставка
person = graph.addVertex(T.label, 'Person', 'name', 'Иван', 'age', 30)Характеристики JanusGraph:
- Масштабирование (до сотен узлов)
- Бэкенды: Cassandra, HBase, Bigtable
- Индексация: Elasticsearch, Solr, Lucene
- Gremlin как язык запросов
Когда использовать:
- Очень большие графы (миллиарды вершин)
- Распределенные системы
- Интеграция с экосистемой Hadoop/Spark
Amazon Neptune
Управляемая графовая БД от AWS. Поддерживает и Cypher (Neo4j), и Gremlin (TinkerPop), и SPARQL (RDF).
// Neptune поддерживает Cypher
CREATE (ivan:Person {name: "Иван"})
MATCH (ivan:Person {name: "Иван"}) RETURN ivanХарактеристики Neptune:
- Полностью управляемый сервис
- Поддержка 3 языков (Cypher, Gremlin, SPARQL)
- Автоматическое масштабирование
- Репликация по 3 AZ (Availability Zones)
- Резервное копирование
Когда использовать:
- Проекты на AWS
- Необходимость в управляемой БД
- Нужна поддержка разных графовых моделей
ArangoDB (Multi-model)
Документная + Графовая + Key-value в одной БД.
// ArangoDB Query Language (AQL)
FOR v, e, p IN 1..3 OUTBOUND 'users/123' GRAPH 'social'
FILTER v.age > 18
RETURN { name: v.name, depth: p.vertices.length }Характеристики ArangoDB:
- Одна БД — три модели
- AQL (свой язык, похож на SQL)
- Джойны между моделями
- Масштабирование (шардирование)
Когда использовать:
- Нужны и документы, и графы
- Небольшие проекты без необходимости в специализированной графовой БД
- Прототипирование
Паттерны запросов в графовых БД
Паттерн 1: Поиск соседей (Neighbors)
-- Друзья Ивана
MATCH (ivan:Person {name: "Иван"})-[:FRIEND]-(friend)
RETURN friend
-- Друзья друзей (2 уровня)
MATCH (ivan:Person {name: "Иван"})-[:FRIEND*2]-(friend2)
RETURN friend2Паттерн 2: Поиск путей (Path finding)
-- Все пути между Иваном и Анной (до глубины 5)
MATCH path = (ivan:Person {name: "Иван"})-[:FRIEND*1..5]-(anna:Person {name: "Анна"})
RETURN path
-- Кратчайший путь
MATCH path = shortestPath((ivan:Person {name: "Иван"})-[:FRIEND*]-(anna:Person {name: "Анна"}))
RETURN path
-- Путь с фильтром (только друзья старше 18)
MATCH path = (ivan:Person {name: "Иван"})-[:FRIEND*]-(anna:Person {name: "Анна"})
WHERE ALL(n IN nodes(path) WHERE n.age > 18)
RETURN pathПаттерн 3: Агрегации на графах
-- Количество друзей у каждого человека
MATCH (p:Person)-[:FRIEND]-(friend)
RETURN p.name, COUNT(friend) AS friends_count
ORDER BY friends_count DESC
-- Группировка по свойствам
MATCH (p:Person)-[:FRIEND]-(friend)
RETURN p.city, AVG(friend.age) AS avg_friend_age
-- PageRank (алгоритм графовой аналитики)
CALL algo.pageRank('Person', 'FRIEND', {write: true})Паттерн 4: Рекомендации
-- Рекомендация друзей (друзья друзей, не знакомые напрямую)
MATCH (ivan:Person {name: "Иван"})-[:FRIEND]-(friend)-[:FRIEND]-(recommendation)
WHERE NOT (ivan)-[:FRIEND]-(recommendation) AND ivan <> recommendation
RETURN recommendation.name, COUNT(*) AS common_friends
ORDER BY common_friends DESC
LIMIT 10
-- Рекомендация товаров (пользователи, купившие похожие товары)
MATCH (ivan:User {id: 123})-[:BOUGHT]-(product:Product)<-[:BOUGHT]-(other:User)
MATCH (other)-[:BOUGHT]-(recommendation:Product)
WHERE NOT (ivan)-[:BOUGHT]-(recommendation)
RETURN recommendation.name, COUNT(*) AS frequency
ORDER BY frequency DESCПаттерн 5: Обнаружение мошенничества (циклы)
-- Найти циклы в транзакциях (потенциальное мошенничество)
MATCH path = (a:Account)-[:TRANSFER*2..5]->(a)
WHERE ALL(r IN relationships(path) WHERE r.amount > 10000)
RETURN path
-- Найти подозрительные паттерны (Иван → счет → другой счет → Иван)
MATCH (ivan:Person {name: "Иван"})-[:OWNS]->(acc1:Account)
MATCH (acc1)-[:TRANSFER_TO]->(acc2:Account)
MATCH (acc2)-[:TRANSFER_TO]->(acc3:Account)
MATCH (acc3)-[:OWNS]->(ivan)
WHERE acc1 <> acc2 AND acc2 <> acc3
RETURN acc1.id, acc2.id, acc3.idПроектирование схемы графа
Выбор типов вершин и ребер
Плохо: слишком общие типы
// Слишком общие
CREATE (a:Entity {name: "Иван"})
CREATE (b:Entity {name: "Мария"})
CREATE (a)-[:RELATED]-(b)Хорошо: конкретные типы
// Конкретные типы
CREATE (a:Person:Customer {name: "Иван"})
CREATE (b:Person:Customer {name: "Мария"})
CREATE (a)-[:FRIEND {since: 2020}]->(b)
CREATE (a)-[:PURCHASED {date: "2024-01-15", amount: 5000}]->(:Product {name: "iPhone"})Плотность графа
Нормальный граф (социальная сеть):
// Степень (количество связей на вершину): 50-500
CREATE (u:User {name: "Иван"})
FOREACH (i IN RANGE(1, 100) |
CREATE (u)-[:FRIEND]->(:User {name: "Друг_" + i})
)Плотный граф (полносвязный):
// Проблема: полносвязный граф из 1000 вершин имеет ~500 000 ребер
// Избегайте полносвязных графов — они не масштабируютсяМоделирование отношений во времени
// Вариант 1: свойство на ребре
CREATE (ivan)-[:FRIEND {since: 2020, until: 2023}]->(petr)
// Вариант 2: временная метка на вершине (для истории)
CREATE (friendship:Friendship {since: 2020, until: 2023})
CREATE (ivan)-[:HAS_FRIENDSHIP]->(friendship)
CREATE (petr)-[:HAS_FRIENDSHIP]->(friendship)Индексы в графовых БД
// Neo4j: индексы на свойствах вершин
CREATE INDEX person_name FOR (p:Person) ON (p.name)
CREATE INDEX person_age FOR (p:Person) ON (p.age)
// Составной индекс
CREATE INDEX person_city_age FOR (p:Person) ON (p.city, p.age)
// Текстовый индекс (полнотекстовый поиск)
CALL db.index.fulltext.createNodeIndex('person_names', ['Person'], ['name', 'description'])
// Пространственный индекс (геоданные)
CALL db.index.spatial.addPointIndex('location_index', 'location', {x: 'longitude', y: 'latitude'})Важно: В графовых БД индексы критичны для начального поиска (нахождения стартовой вершины). Без индекса поиск “Иван” может привести к сканированию всех вершин.
Когда использовать графовые БД
Идеальные сценарии
| Сценарий | Почему подходит |
|---|---|
| Социальные сети | Друзья, подписчики, рекомендации — естественная графовая модель |
| Системы рекомендаций | “Пользователи, купившие X, также купили Y” — граф покупок |
| Обнаружение мошенничества | Поиск циклов, подозрительных паттернов, связей между счетами |
| Управление доступом (ABAC/RBAC) | Роли, разрешения, наследование — направленный ациклический граф (DAG) |
| Поиск путей (логистика, маршрутизация) | Кратчайший путь, альтернативные маршруты, оптимизация |
| Системы управления знаниями (Knowledge Graph) | Википедия, базы знаний, семантические связи |
| Сетевые топологии (IT/OT) | Схема сети, зависимости между сервисами, root cause analysis |
| Биоинформатика | Генные сети, взаимодействия белков |
Сомнительные сценарии
| Сценарий | Почему плохо подходит |
|---|---|
| Простые CRUD-приложения | Графовая БД избыточна; проще использовать PostgreSQL или MongoDB |
| Аналитика на больших данных (OLAP) | Есть специализированные инструменты (ClickHouse, Druid) |
| Кеширование | Redis лучше подходит для key-value |
| Огромные графы с плотными связями | Полносвязные графы не масштабируются ни в какой БД |
| Только чтение одного объекта по ID | Нет преимущества перед любой другой БД |
Сравнение графовых БД
| Характеристика | Neo4j | JanusGraph | Amazon Neptune | ArangoDB |
|---|---|---|---|---|
| Модель | Property graph | Property graph | Property graph + RDF | Multi-model |
| Язык | Cypher | Gremlin | Cypher/Gremlin/SPARQL | AQL |
| ACID | Да | Нет (зависит от бэкенда) | Да | Да |
| Масштабирование | Кластер (causal) | Горизонтальное (до сотен узлов) | Горизонтальное | Горизонтальное |
| Индексы | B-Tree, full-text, spatial | Elasticsearch, Solr | Да | Да |
| Визуализация | Neo4j Browser | Gephi, Cytoscape | Neptune Workbench | ArangoDB Web UI |
| Лицензия | GPLv3 (Community) | Apache 2 | Проприетарная | Apache 2 |
| Управляемая версия | Neo4j Aura | Нет | AWS Neptune | ArangoDB Oasis |
Преимущества и недостатки
Преимущества
| Преимущество | Описание |
|---|---|
| Производительность на связях | Обход графа на 2-5 уровней значительно быстрее, чем JOIN в SQL |
| Гибкость схемы | Добавление новых типов вершин и ребер без миграций |
| Естественная модель | Данные выглядят так же, как в реальном мире (объекты и связи) |
| Сложные запросы | Паттерны связей, пути, рекурсия — естественно выражаются |
| Визуализация | Граф легко визуализировать для понимания структуры |
Недостатки
| Недостаток | Описание |
|---|---|
| Нет стандарта | Разные БД используют разные языки (Cypher, Gremlin, SPARQL) |
| Производительность на больших графах | Обход 10 миллионов вершин все еще медленный |
| Плотные графы | Полносвязные графы (каждый с каждым) не масштабируются |
| Аналитика | Глобальные агрегации (среднее по всем вершинам) не оптимизированы |
| Инфраструктура | Меньше инструментов мониторинга, бэкапов, чем у реляционных БД |
Распространенные ошибки
Ошибка 1: Использование графовой БД для табличных данных
Попытка хранить в Neo4j таблицу “пользователи” без связей.
Как исправить: Если у вас нет сложных связей, используйте реляционную или документную БД.
Ошибка 2: Полносвязный граф
// Плохо: каждый человек связан с каждым
MATCH (a:Person), (b:Person)
WHERE a <> b
CREATE (a)-[:KNOWS]->(b)
// Для 1000 человек → ~1 000 000 ребер
// Для 10000 человек → ~100 000 000 реберКак исправить: Ограничивайте степень вершин. Если нужны полносвязные графы, возможно, вы выбрали не ту модель данных.
Ошибка 3: Игнорирование индексов
// Плохо: нет индекса на Person.name
MATCH (p:Person {name: "Иван"}) RETURN p
// Без индекса — полное сканирование всех вершинКак исправить: Всегда создавайте индексы на свойства, по которым ищете стартовые вершины.
Ошибка 4: Очень глубокие обходы без ограничений
// Потенциально бесконечный обход
MATCH (ivan:Person {name: "Иван"})-[:FRIEND*]-(friend)
RETURN friendКак исправить: Всегда ограничивайте глубину обхода (*1..5) или используйте shortestPath.
Ошибка 5: Хранение больших бинарных данных в свойствах
// Плохо: изображение в свойстве
CREATE (user:User {name: "Иван", avatar: BLOB("...")})Как исправить: Храните большие объекты в S3/Blob Store, в графе — только ссылки.
Резюме для системного аналитика
Графовые базы данных хранят вершины (сущности) и ребра (связи). Связи — первоклассные граждане, такие же важные, как и данные. Это позволяет эффективно отвечать на вопросы “как связаны X и Y?”.
Главное преимущество — производительность обхода связей. В реляционных БД глубина связей означает количество JOIN. В графовых — это локальный обход от вершины к вершине, который остается быстрым даже на глубине 5-10 уровней.
Neo4j — самая популярная графовая БД. Использует Cypher (декларативный язык), поддерживает ACID, индексы, кластеризацию.
Основные паттерны запросов: поиск соседей, поиск путей (кратчайший путь), агрегации на графах, рекомендации, обнаружение циклов.
Графовые БД не заменяют реляционные. Они хороши для связей, но не для аналитики на больших данных, не для простого CRUD, не для кеширования.
Проектирование графа требует выбора правильных типов вершин и ребер. Конкретные типы лучше общих. Избегайте полносвязных графов.