Запросы к графам свойств SQL/PGQ в PostgreSQL 19

от автора

Примечание: вышла PostgreSQL 19 Beta 1

PostgreSQL 19 Beta 1 вышла 4 июня 2026 года, поэтому запросы к графам свойств SQL/PGQ уже можно попробовать до финального релиза, ожидаемого позднее в 2026 году. В Beta 1 добавлена поддержка запросов к графам свойств в стандартном синтаксисе SQL поверх существующих реляционных таблиц — без нового движка хранения и расширений.

Кратко: PostgreSQL 19 добавляет SQL/PGQ (Property Graph Queries, запросы к графам свойств) на основе стандарта SQL:2023. Графовые структуры можно определить поверх уже существующих реляционных таблиц и запрашивать их синтаксисом сопоставления с образцом. Не нужны новый движок хранения, расширения или миграция данных.

Введение в SQL/PGQ

Графовые БД, например Neo4j, удобны для запросов к связям: «найти друзей друзей» или «обнаружить циклические зависимости» естественно выражаются на графовом языке запросов. Но отдельная графовая БД рядом с PostgreSQL означает дублирование данных, сложность синхронизации и ещё одну систему, которую нужно сопровождать.

PostgreSQL 19 добавляет графовые запросы непосредственно в SQL через стандарт SQL/PGQ (ISO/IEC 9075-16:2023). Ключевое решение здесь такое: граф свойств задаётся как представление над существующими реляционными таблицами. Данные остаются на месте; PostgreSQL нужно лишь указать, какие таблицы соответствуют вершинам, а какие — рёбрам.

Определение графа свойств

Граф свойств состоит из двух типов элементов: таблиц вершин (узлов) и таблиц рёбер (связей). Определение графа сопоставляет их с уже существующими таблицами.

Пример таблиц

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

-- Обычные таблицы PostgreSQLCREATE TABLE users (    id SERIAL PRIMARY KEY,    name TEXT NOT NULL,    email TEXT,    joined_at DATE DEFAULT CURRENT_DATE);CREATE TABLE follows (    id SERIAL PRIMARY KEY,    follower_id INT NOT NULL REFERENCES users(id),    followed_id INT NOT NULL REFERENCES users(id),    created_at TIMESTAMP DEFAULT now());CREATE TABLE posts (    id SERIAL PRIMARY KEY,    author_id INT NOT NULL REFERENCES users(id),    title TEXT NOT NULL,    created_at TIMESTAMP DEFAULT now());CREATE TABLE likes (    id SERIAL PRIMARY KEY,    user_id INT NOT NULL REFERENCES users(id),    post_id INT NOT NULL REFERENCES posts(id),    created_at TIMESTAMP DEFAULT now());-- Добавим данныеINSERT INTO users (name, email) VALUES    ('Alice', 'alice@example.com'),    ('Bob', 'bob@example.com'),    ('Charlie', 'charlie@example.com'),    ('Diana', 'diana@example.com');INSERT INTO follows (follower_id, followed_id) VALUES    (1, 2), (1, 3), (2, 3), (3, 4), (4, 1);INSERT INTO posts (author_id, title) VALUES    (1, 'Getting Started with PostgreSQL'),    (2, 'Graph Queries in SQL'),    (3, 'Why I Switched from Neo4j');INSERT INTO likes (user_id, post_id) VALUES    (2, 1), (3, 1), (4, 2), (1, 3), (2, 3);

Создание графа

Определение графа сопоставляет каждой таблице метку вершины или ребра. Таблицы вершин становятся узлами графа; для таблиц рёбер указываются столбцы, которые ссылаются на исходную и конечную вершины.

CREATE PROPERTY GRAPH social_graph  VERTEX TABLES (    users LABEL person      PROPERTIES (id, name, email, joined_at),    posts LABEL post      PROPERTIES (id, title, created_at)  )  EDGE TABLES (    follows      SOURCE KEY (follower_id) REFERENCES users (id)      DESTINATION KEY (followed_id) REFERENCES users (id)      LABEL follows      PROPERTIES (created_at),    posts AS post_edges      SOURCE KEY (author_id) REFERENCES users (id)      DESTINATION KEY (id) REFERENCES posts (id)      LABEL wrote,    likes      SOURCE KEY (user_id) REFERENCES users (id)      DESTINATION KEY (post_id) REFERENCES posts (id)      LABEL liked      PROPERTIES (created_at)  );

Данные при этом не копируются. Создаётся определение графа, похожее на представление: PostgreSQL использует его, чтобы преобразовать графовый запрос в реляционные операции над существующими таблицами. Таблица posts участвует в графе дважды: как таблица вершин post и как источник рёбер wrote, связывающих автора с публикацией. Псевдоним post_edges нужен, чтобы различить эти два использования одной таблицы в определении графа. Это не дублирование строк, а два разных представления одной таблицы в модели графа.

Запросы с GRAPH_TABLE и MATCH

Функция GRAPH_TABLE принимает имя графа и шаблон MATCH, а возвращает обычный набор строк:

На кого подписана Alice

Это обход на один переход: начинаем с Alice, идём по исходящим рёбрам follows и выводим имя пользователя в конечной вершине.

SELECT * FROM GRAPH_TABLE (social_graph    MATCH (a IS person WHERE a.name = 'Alice')          -[f IS follows]->(b IS person)    COLUMNS (b.name AS followed_name));
 followed_name--------------- Bob Charlie

Стрелка -[f IS follows]-> обозначает направленное ребро типа follows. Выражение (a IS person) сопоставляется с вершинами, имеющими метку person.

Друзья друзей

Соедините в шаблоне два ребра follows, чтобы выполнить обход на два перехода. Переменные b и c привязываются соответственно к промежуточной и конечной вершинам и затем выводятся как именованные столбцы.

SELECT * FROM GRAPH_TABLE (social_graph    MATCH (a IS person WHERE a.name = 'Alice')          -[IS follows]->(b IS person)          -[IS follows]->(c IS person)    COLUMNS (        b.name AS friend,        c.name AS friend_of_friend    ));
 friend  | friend_of_friend---------+------------------ Bob     | Charlie Charlie | Diana

Шаблон с несколькими переходами записывается цепочкой рёбер. Каждая стрелка означает ровно один переход.

Замечание переводчика: о глубине обхода. В этом запросе глубина обхода равна двум, потому что в шаблоне указаны две стрелки. PostgreSQL 19 пока не умеет записать «повторяй переход, пока есть связь» одним графовым шаблоном. Если нужны связи ровно через три перехода, добавьте третью стрелку; если глубина заранее неизвестна, используйте рекурсивный CTE. Это ограничение не мешает запросам с известной глубиной, но важно при поиске всех зависимостей или всех путей между двумя вершинами.

Кто поставил «нравится» публикациям Alice

В определении social_graph выше таблица posts также задаёт ребро wrote: от автора к его публикации. Поэтому можно сначала найти Alice, перейти к её публикациям по wrote, а затем пройти по входящим рёбрам liked к пользователям, поставившим отметку «нравится».

SELECT * FROM GRAPH_TABLE (social_graph    MATCH (author IS person WHERE author.name = 'Alice')          -[authorship IS wrote]->(p IS post)          <-[IS liked]-(liker IS person)    COLUMNS (        liker.name AS liked_by,        p.title AS post_title    ));
liked_by |            post_title----------+----------------------------------- Bob      | Getting Started with PostgreSQL Charlie  | Getting Started with PostgreSQL

Синтаксис <-[IS liked]- разворачивает направление ребра: он сопоставляет входящие связи. Вся цепочка читается слева направо: «Alice — написала → публикация ← понравилась — пользователь».

Разберём шаблон по шагам:

  1. (author IS person WHERE author.name = 'Alice') находит вершину пользователя Alice.

  2. -[authorship IS wrote]->(p IS post) переходит к публикациям, для которых Alice — автор.

  3. <-[IS liked]-(liker IS person) идёт от каждой такой публикации к пользователям, поставившим «нравится».

Замечание переводчика: как читать направление. Направление ребра задано в CREATE PROPERTY GRAPH, а не тем, как удобнее прочесть запрос. Для likes исходная вершина — пользователь (user_id), конечная — публикация (post_id). Поэтому, двигаясь от публикации к пользователю, в MATCH нужна стрелка влево: <-[IS liked]-. Ошибка в направлении особенно легко возникает, когда одна и та же таблица участвует в нескольких связях.

Совмещение графовых запросов с обычным SQL

GRAPH_TABLE возвращает обычный набор строк, поэтому его можно использовать везде, где допустимы подзапрос или таблица:

-- Найти пользователей, на которых подписано более двух человекSELECT followed_name, count(*) AS follower_countFROM GRAPH_TABLE (social_graph    MATCH (a IS person)-[IS follows]->(b IS person)    COLUMNS (b.name AS followed_name))GROUP BY followed_nameHAVING count(*) > 1ORDER BY follower_count DESC;

Это одно из главных преимуществ перед отдельной графовой БД: графовые запросы естественно сочетаются с агрегациями, оконными функциями, CTE и остальными возможностями SQL.

Что происходит внутри

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

Проверить это можно с помощью EXPLAIN:

EXPLAIN SELECT * FROM GRAPH_TABLE (social_graph    MATCH (a IS person WHERE a.name = 'Alice')          -[IS follows]->(b IS person)    COLUMNS (b.name AS followed_name));

В плане будут соединения таблиц users и follows, использующие доступные индексы. Отдельного движка выполнения графовых запросов здесь нет.

Управление графами свойств

Графы свойств — объекты схемы, как представления. Их можно вывести, изменить и удалить стандартными командами.

Просмотр списка графов

В psql используйте метакоманду \dG. Если вывод нужен в скрипте, обращайтесь непосредственно к системным каталогам графов свойств.

-- В psql\dG-- Метаданные графов свойств размещены в нескольких системных каталогах.-- Обычно достаточно этих каталогов:SELECT * FROM pg_propgraph_element;       -- таблицы вершин и рёберSELECT * FROM pg_propgraph_label;         -- меткиSELECT * FROM pg_propgraph_label_property; -- соответствие «метка → свойство»SELECT * FROM pg_propgraph_property;      -- выражения свойств

Изменение графа

ALTER PROPERTY GRAPH позволяет добавлять, удалять и изменять таблицы вершин и рёбер без пересоздания графа.

-- Добавить новый тип ребраALTER PROPERTY GRAPH social_graph  ADD EDGE TABLE messages    SOURCE KEY (sender_id) REFERENCES users (id)    DESTINATION KEY (receiver_id) REFERENCES users (id)    LABEL sent_message;

Удаление графа

DROP PROPERTY GRAPH удаляет только определение графа.

DROP PROPERTY GRAPH social_graph;

Удаление графа не затрагивает таблицы, на которых он основан.

Практические сценарии

Ниже — несколько случаев, где графовое представление существующих таблиц позволяет выразить запрос понятнее, чем многоуровневые самосоединения.

Отслеживание зависимостей

Представьте пакеты вершинами, а таблицу связей depends_on — рёбрами. Так удобно искать прямые зависимости и, если заранее известна глубина, зависимости в несколько переходов. Для зависимостей произвольной глубины в PostgreSQL 19 по-прежнему нужен рекурсивный CTE.

CREATE TABLE packages (    id SERIAL PRIMARY KEY,    name TEXT NOT NULL,    version TEXT);CREATE TABLE depends_on (    id SERIAL PRIMARY KEY,    package_id INT REFERENCES packages(id),    dependency_id INT REFERENCES packages(id));CREATE PROPERTY GRAPH dep_graph  VERTEX TABLES (packages LABEL pkg PROPERTIES (id, name, version))  EDGE TABLES (    depends_on      SOURCE KEY (package_id) REFERENCES packages(id)      DESTINATION KEY (dependency_id) REFERENCES packages(id)      LABEL depends  );-- Найти прямые зависимостиSELECT * FROM GRAPH_TABLE (dep_graph    MATCH (a IS pkg WHERE a.name = 'my-app')          -[IS depends]->(b IS pkg)    COLUMNS (b.name AS dependency, b.version));

Иерархия в организации

Связи подчинённости образуют ориентированный граф над одной таблицей сотрудников. Шаблон из двух переходов возвращает сотрудника, его руководителя и руководителя руководителя одним запросом.

-- Найти цепочки подчинённости из двух уровнейSELECT * FROM GRAPH_TABLE (org_graph    MATCH (emp IS employee)          -[IS reports_to]->(mgr IS employee)          -[IS reports_to]->(dir IS employee)    COLUMNS (        emp.name AS employee,        mgr.name AS manager,        dir.name AS director    ));

Выявление мошенничества

Обходы по общим атрибутам помогают находить учётные записи, которые используют те же контактные данные, что и уже помеченная запись. Шаблон «помеченная учётная запись → телефон → другая учётная запись» находит такие связи, которые неудобно записывать обычными JOIN.

-- Найти учётные записи с тем же номером телефона, что у помеченной записиSELECT * FROM GRAPH_TABLE (fraud_graph    MATCH (flagged IS account WHERE flagged.status = 'flagged')          -[IS uses]->(phone IS phone_number)          <-[IS uses]-(other IS account)    COLUMNS (        flagged.id AS flagged_account,        phone.number,        other.id AS connected_account    ));

Текущие ограничения

В первой реализации SQL/PGQ в PostgreSQL 19 есть несколько намеренных ограничений:

Нет путей переменной длины: нельзя записать шаблон наподобие -[IS follows]->+ (один или более переходов) или -[IS follows]->{2,5} (от двух до пяти переходов). Каждый переход нужно указать явно. Поэтому рекурсивные обходы, например поиск кратчайшего пути или транзитивного замыкания, по-прежнему требуют рекурсивных CTE.

Нет квантифицированных шаблонов путей: квантификаторы *, + и {m,n} планируются в последующем патче, но в первый релиз не вошли.

Только фиксированная глубина: запросы вида «найти все пути между A и B независимо от длины» средствами одного лишь SQL/PGQ в PostgreSQL 19 невозможны. Для них используйте рекурсивные CTE.

Замечание переводчика: когда выбирать рекурсивный CTE. Фиксированная глубина означает, что число переходов известно при написании запроса: например, «руководитель руководителя» — это два перехода. Переменная глубина означает, что нужно идти по связям до конца цепочки или пока не найден нужный узел: например, «все зависимости пакета». Во втором случае в PostgreSQL 19 используйте WITH RECURSIVE; SQL/PGQ здесь пока не заменяет рекурсию.

Для запросов со связями фиксированной глубины — «друзья друзей», шаблоны на 2–3 перехода, поиск прямых зависимостей — SQL/PGQ уже подходит. Поддержка путей переменной длины ожидается в одном из будущих выпусков PostgreSQL.

SQL/PGQ и другие решения для графов

SQL/PGQ (PG 19)

Apache AGE

Neo4j

Язык

Стандарт SQL (ISO 2023)

Cypher через расширение

Cypher

Хранение

Существующие таблицы

Хранилище расширения

Собственная графовая БД

Пути переменной длины

Пока нет

Да

Да

Кратчайший путь

Пока нет

Ограниченно

Да

Установка

Встроено

Нужно расширение

Отдельная БД

Основано на стандарте

Да (SQL:2023)

Нет

Нет

Индексы

Использует существующие индексы PostgreSQL

Собственные индексы

Собственные индексы

Полная поддержка SQL

Да: JOIN, CTE, оконные функции

Ограниченная интеграция

Нет SQL

Главное преимущество SQL/PGQ: он работает с уже существующими таблицами и индексами. Не требуются миграция данных, установка расширения и сопровождение отдельной системы.

Итоги

SQL/PGQ добавляет в PostgreSQL стандартизированные графовые запросы без миграции данных, расширений и отдельной БД. Первая реализация поддерживает сопоставление шаблонов фиксированной глубины, чего достаточно для многих распространённых графовых запросов. Поддержка путей переменной длины ожидается в будущих выпусках.

Источники

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