Перейти к содержимому

Графовые

Графовые базы данных — хранение вершин (сущностей) и рёбер (связей), где связи являются первоклассными гражданами. Эффективный обход графа вместо 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

Что происходит под капотом:

  1. Найти вершину “Иван” по индексу (быстро)
  2. Пройти по ребрам типа FRIEND на 2 уровня (локально, без JOIN)
  3. Собрать уникальные вершины

Сложность: 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 ivan

Gremlin (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Нет преимущества перед любой другой БД

Сравнение графовых БД

ХарактеристикаNeo4jJanusGraphAmazon NeptuneArangoDB
МодельProperty graphProperty graphProperty graph + RDFMulti-model
ЯзыкCypherGremlinCypher/Gremlin/SPARQLAQL
ACIDДаНет (зависит от бэкенда)ДаДа
МасштабированиеКластер (causal)Горизонтальное (до сотен узлов)ГоризонтальноеГоризонтальное
ИндексыB-Tree, full-text, spatialElasticsearch, SolrДаДа
ВизуализацияNeo4j BrowserGephi, CytoscapeNeptune WorkbenchArangoDB Web UI
ЛицензияGPLv3 (Community)Apache 2ПроприетарнаяApache 2
Управляемая версияNeo4j AuraНетAWS NeptuneArangoDB 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, в графе — только ссылки.

Резюме для системного аналитика

  1. Графовые базы данных хранят вершины (сущности) и ребра (связи). Связи — первоклассные граждане, такие же важные, как и данные. Это позволяет эффективно отвечать на вопросы “как связаны X и Y?”.

  2. Главное преимущество — производительность обхода связей. В реляционных БД глубина связей означает количество JOIN. В графовых — это локальный обход от вершины к вершине, который остается быстрым даже на глубине 5-10 уровней.

  3. Neo4j — самая популярная графовая БД. Использует Cypher (декларативный язык), поддерживает ACID, индексы, кластеризацию.

  4. Основные паттерны запросов: поиск соседей, поиск путей (кратчайший путь), агрегации на графах, рекомендации, обнаружение циклов.

  5. Графовые БД не заменяют реляционные. Они хороши для связей, но не для аналитики на больших данных, не для простого CRUD, не для кеширования.

  6. Проектирование графа требует выбора правильных типов вершин и ребер. Конкретные типы лучше общих. Избегайте полносвязных графов.

Проверка знаний

Вопрос 1 из 4
Что такое графовая БД?
Для каких задач графовые БД особенно хороши?
Чем графовая модель отличается от табличной?
Что важно помнить о графовых БД?

Вопросы, где были ошибки