Postgres: графовая база данных, о которой вы не подозревали

от автора

PostgreSQL (Postgres) — это мощная реляционная база данных, способная хранить широкий спектр типов и структур данных. Когда нам нужно хранить графовые структуры данных, мы часто обращаемся к базам данных, позиционируемым как подходящее для этого решение, например, к Neo4J или Dgraph. Но не торопитесь! Хотя при работе с графовыми структурами данных о Postgres обычно не вспоминают, она идеально справляется с эффективным хранением графовых данных и запросами к ним.

Разбираемся с графовыми структурами данных

Прежде чем приступать к объяснению Postgres как графовой базы данных, нам нужно понять, что же такое графовая структура данных. Граф, или графовая структура данных — это набор узлов и рёбер, где каждый узел обозначает сущность или объект, а каждое ребро обозначает связь между двумя узлами.

Визуальный пример графовой структуры данных.

Чтобы начать воспринимать графы в рамках кода, можно написать следующий TypeScript:

class Node {   edges: Edge[] = [];   data: string; }  class Edge {   previousNode: Node;   nextNode?: Node; }

Каждый узел (node) содержит список своих рёбер, а каждое ребро (edge) содержит ссылку на следующий/предыдущий узел. Как мы ниже узнаем из SQL, узлы не всегда обязаны знать о своих рёбрах.

Facebook — популярная социальная сеть, использующая для описания людей и их связей граф. У человека могут быть друзья, и у этих друзей тоже есть свой список друзей. Каждый человек представлен узлом, а каждую дружбу можно задать ребром. Графы используются для моделирования множества различных систем, например npm-зависимостей, рабочих процессов, транспортных систем, производственных линий и многого другого!

Хранение графовых структур данных в Postgres

Для хранения графа в Postgres нам достаточно создать две таблицы: nodes и edges. Таблица nodes будет хранить информацию о каждой сущности, а в таблице edges будет храниться информация о связях между этими сущностями.

Давайте начнём с создания таблицы nodes:

CREATE TABLE nodes (   id SERIAL PRIMARY KEY,   data VARCHAR(255) );

Определённая нами таблица nodes имеет два столбца: id и data. Столбец id содержит целочисленные значения с автоматическим инкрементом, служащие первичным ключом таблицы. Столбец data — это строки, хранящие дополнительные данные, связанные с узлом. В этом примере мы не будем усложнять и сохраним только строковый столбец, однако в реальном мире эта таблица могла бы содержать что угодно и иметь любое количество столбцов.

Самой важной таблицей при создании графовой структуры данных является таблица edges:

CREATE TABLE edges (   previous_node INTEGER REFERENCES nodes(id),   next_node INTEGER REFERENCES nodes(id),   PRIMARY KEY (previous_node, next_node) );

Мы создали два столбца, previous_node и next_node, обозначающие взаимосвязи между узлами. Каждый из этих столбцов хранит внешний ключ для узла. Важный вывод заключается в том, что таблица edges ссылается на две строки одной таблицы. Ребро может иметь только по одной паре previous_node и next_node, поэтому мы используем составной первичный ключ, чтобы каждое ребро было уникальным и не могло ссылаться на само себя.

Создав таблицы, мы можем вставить в них данные.

INSERT INTO nodes (data) VALUES ('Bob'); INSERT INTO nodes (data) VALUES ('Hank'); INSERT INTO nodes (data) VALUES ('Jeff');

А теперь соединим узлы рёбрами:

INSERT INTO edges (previous_node, next_node) VALUES (1, 2); INSERT INTO edges (previous_node, next_node) VALUES (1, 3);

nodes
id data
1 Bob
2 Hank
3 Jeff

edges
previous_node next_node
1 2
1 3

Если бы мы визуализировали этот граф, то он бы выглядел так:

Запросы к графовым структурам данных в Postgres

Создав графовую структуру данных, мы можем начать выполнять запросы к ней при помощи известного нам и любимого SQL!

Хотите знать, с кем дружит Боб?

SELECT id, data FROM nodes JOIN edges ON nodes.id = edges.next_node WHERE edges.previous_node = 1;

Находим все nodes, связанные с узлом, имеющим id 1 (id Боба).

Похоже, Боб популярен! Но что, если мы захотим узнать, с кем дружат друзья Боба?

Давайте вставим ещё несколько узлов и рёбер, чтобы показать это:

INSERT INTO nodes (data) VALUES ('Sally'); INSERT INTO nodes (data) VALUES ('Sue'); INSERT INTO nodes (data) VALUES ('Sam');  INSERT INTO edges (previous_node, next_node) VALUES (2, 4); INSERT INTO edges (previous_node, next_node) VALUES (3, 4); INSERT INTO edges (previous_node, next_node) VALUES (4, 5);

nodes
id data
1 Bob
2 Hank
3 Jeff
4 Sally
5 Sue
6 Sam

edges
previous_node next_node
1 2
1 3
2 4
3 4
4 5

Чтобы запросить всех друзей друзей Боба, мы можем расширить предыдущий запрос, снова выполнив в нём join таблицы edges, но тогда поддержка базы данных превратится в кошмар, ведь нам пришлось бы выполнять join для каждого «уровня» графа.

Postgres имеет встроенную фичу, позволяющую запрашивать графовые данные, не зная точно, сколько join нам нужно: рекурсивные запросы. Рекурсивные запросы позволяют обойти граф, начиная с заданного узла и двигаясь по его рёбрам до какой-то заданной конечной точки.

Чтобы создать рекурсивный запрос для поиска всех друзей Боба и их друзей, нам нужно написать следующий SQL:

WITH RECURSIVE friend_of_friend AS (   SELECT edges.next_node   FROM edges   WHERE edges.previous_node = 1   UNION   SELECT edges.next_node   FROM edges   JOIN friend_of_friend ON edges.previous_node = friend_of_friend.next_node ) SELECT nodes.data FROM nodes JOIN friend_of_friend ON nodes.id = friend_of_friend.next_node;

Поначалу это может показаться непонятным, поэтому разберём команды. Рекурсивный запрос состоит из двух частей: базовый случай и рекурсивный случай. Базовый случай — это то, с чего мы хотим начать запрос. Рекурсивный случай — это «цикл», который продолжает выполняться, пока не будет достигнута какая-то конечная точка.

WITH RECURSIVE {name} AS (   {base case}   UNION   {recursive case} )

Выше показана простейшая структура SQL рекурсивного запроса.

В нашем примере нужно начать запрос с друзей Боба, чтобы найти рёбра, в которых Боб (id: 1) является previous_node. Затем в рекурсивном случае мы непрерывно выполняем join таблицы edges с самой собой, пока не достигнем конца графа Боба (то есть пока не достигнем friend_of_friend.next_node = NULL). Вне рекурсивного случая мы объединяем всё это вместе. Нам нужно запросить nodes, связанные с рёбрами из рекурсивного запроса, чтобы можно было получить имена всех друзей Боба.

data
Hank
Jeff
Sally

В заключение

При помощи встроенных функций Postgres можно сохранять графовые структуры данных и выполнять к ним запросы. Мы использовали похожий подход в моей предыдущей работе для динамической генерации рабочих инструкций на производственной линии. На основании заданных параметров и определённых в каждом ребре правил можно сгенерировать корректный документ при помощи обхода графа, целиком хранящегося в Postgres. Если вы уже используете Postgres для работы с реляционными данными, то можете интегрировать графовые структуры данных в имеющуюся базу данных добавления лишних систем!


ссылка на оригинал статьи https://habr.com/ru/articles/726598/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *