Графовые данные можно технически хранить 3 известными мне способами:
1. В реалиционных базах:
* Данные хранятся в плоских таблицах
* Связи между сущностями осуществляются через индексированные внешие ключи
2. Документные базы:
* Связанные данные лежат прямо в колонках основной таблицы
К примеру, информация о друзьях аккаунта положена в json поле таблицы
3. Графовая база
Остановимся подробней на примере бд Neo4j:
* Отдельно хранятся данные о нодах, ребрах и значениях в ноде в разных файлах
* Нода (Node) имеет фиксированный размер, что позволяет переходить к ней в файле по смещению за константное время: ID * fixed size
[ 1 байт - активность | 4 байт - указатель на файл связи | 5 байт - указатель на файл свойств ]
* файл соединение (Relationship - ребра) состоит:
[ 5 байт - файл 1 ноды | 5 байт - файл 2 ноды | тип соединение | указатель на следующий relation | указатель на предыдущий relation ]
первый 10 байт - это обратные ссылки на ноды, что в сочетании с сылками в нодах дает двунаправленный список
* Связанный список свойств
повторяющиеся значения сжимаются в словарь, а в самом property хранится ссылка на словарь
если значение достаточно маленькое, то оно хранится прямо в property файл

Графически это можно представить следующим образом:

Какие же преимущества дает такой тип хранения данных:
| Показатель | Графовая | Реалиционная | Документная |
| Простота выборки |
Синтаксис проще и короче релиционной схемы:
MATCH (john:Person { name:"John" })-
[:friend *1..2]->(friend: Person)
RETURN friend.name, friend.age |
Достаточно сложный синтаксис:
WITH RECURSIVE r AS (
SELECT id, parent_id, name, 1 AS level
FROM geo
WHERE id = 4
UNION ALL
SELECT geo.id, geo.parent_id, geo.name,
r.level + 1 AS level
FROM geo
JOIN r
ON geo.parent_id = r.id
)
SELECT * FROM r
|
Достаем готовые документ по ключу:
val theget= new Get(Bytes.toBytes("rowkey1"))
val result=table.get(theget)
val value=result.value()
println(Bytes.toString(value))
|
| Скорость доступа к следующей записи графа | Константное время перехода к следуюещй записи в графе, т.к. смещения хранятся в самом relation | Для доступа к следующей записи нужно найти запись в индексе и перейти к таблице
минимум 3-4 одноблочных чтения |
Связанные записи хранятся в самом объекте |
| Досту к связям связей | Аналогичное константное время через relation | Еще 1 проход по внешнему ключу (+3-4 одноблочных чтения) | Дорогая операция, т.к. связанный объект нужно распарсить |
| Возможность обратного обхода (Наити людей, у кого ты в друзьях) | Константное время как вперед, так и назад | Чаще всего нет возможности пойти в обратную сторону, если это не предусмотрено в схеме дополнительными объектами | Такой возможности нет, т.к. нужно будет просмотреть и распарсить все документы |
| Связи многие ко многим | Аналогичное константное время через relation | Такая возможность есть через таблицы связки. Дополнительные минимум 3-4 одноблочных чтения к основным |
Такой возможнсоти нет, связь всегда 1 к N |
| Ограничения на размер свойств | Свойства хранятся отдельно от связей, что уменьшает размер чтений при проходе по графу | Дополнительные поля таблицы хранятся в одном месте со связями, что увеличивает размер чтений при проходе по графу | Размер свойств не имеет значения |
Еще чуть информации об внутреннем устройстве Neo4J:
* для данных используется LFU кэш (least freaq use) - аналогично Oracle
* есть возможность горизонтального масштабирования черзе шардирования по одному из ключей графа
* Параллелизация запроса возможна через парадигму map-reduce, если задача разбивается на подзадачи
** собирается массив параметров, по которому будем параллелить
** запускаем запрос в N потоков, передавая порцию данных
** собираем результат от потоков и выполняется доагрегация в reduce
Пример:
#параметры
with collect(distinct a) as addresses
# 1 параметр - код, для выполнения в потоке
# 2 - параметры параллелизации и шага
# 3 - параметры для передачи в поток
# 4 - спит параметров на число партиций
CALL apoc.cypher.mapParallel2("
optional match (_)<-[:LOCKED_BY]-(o: Output)
where not (o)-[:UNLOCKED_BY]->()
return _.address as address,
round(sum(o.bitcoinValue)*100000000)/100000000 as balance"
, {parallel:True, batchSize:1000, concurrency:20}, addresses, 20) yield value
Комментариев нет:
Отправить комментарий