PostgreSQL에서 SQL/PGQ를 활용한 그래프 표현
John Nevin
2025년 2월 18일
토르킨(Tolkien)과 그래프 이론을 좋아하는 저에게, EDB 동료들이 공유한 이 훌륭한 블로그 글은 무척 흥미로웠습니다.
작성자는 PostgreSQL에서 그래프를 모델링하는 방법과 재귀적 CTE를 활용하여 그래프 탐색 쿼리를 실행하는 방법을 설명하며 다음과 같이 결론을 내립니다.
PostgreSQL에서 그래프 데이터를 표현하고 이를 유연하게 쿼리하는 한 가지 방법입니다.
또한, 다른 기법이나 개선 방안이 있다면 공유해 달라는 의견을 남겼습니다.
실제로 PostgreSQL에서 그래프를 보다 효과적으로 다룰 수 있도록 하는 새로운 기술이 개발되고 있습니다.
SQL 속성 그래프 쿼리 (SQL Property Graph Queries)
SQL 속성 그래프 쿼리(SQL/PGQ)는 최근 SQL:2023 ISO 표준에 포함되었으며, 별도의 그래프 데이터베이스 관리 시스템(예: Neo4j) 없이도 기존 관계형 데이터를 그래프로 표현하고 효율적으로 쿼리할 수 있도록 지원합니다.
노드 간의 관계를 연결된 엣지(Edge)로 표현하는 것은 매우 직관적이고 유용한 기능으로, 특히 소셜 네트워크(친구의 친구 관계), 교통망, 컴퓨터 네트워크 등 다양한 네트워크를 모델링하는 데 적합합니다.
Postgres에 이 기능 추가하기
Postgres에 그래프 데이터베이스 기능을 추가하는 Apache AGE™와 같은 서드파티 확장이 존재하지만, PostgreSQL에 SQL/PGQ를 도입하기 위한 논의와 작업이 pgsql-hackers 메일링 리스트에서 꾸준히 진행되고 있습니다.
아직 정식 출시 일정은 정해지지 않았지만, 현재 기능적으로 동작하며 Postgres에 패치를 적용하면 직접 실험해볼 수 있습니다.
세 단계로 간편하게 패치 적용하기
이 패치를 적용하려면 Postgres를 소스 코드에서 빌드 및 설치해야 합니다. Postgres 공식 문서에서 이에 대한 상세한 가이드를 제공하고 있습니다.
1. 패치 파일 다운로드
먼저, pgsql-hackers
스레드에서 모든 .patch
파일을 다운로드합니다.
2. Postgres 소스 코드 다운로드
Postgres 소스 코드를 가져온 후, 해당 디렉터리로 이동하여 설치 절차를 진행합니다.
(Postgres 소스 코드 다운로드 방법: 공식 문서)
3. 패치 파일 적용
다운로드한 각 .patch
파일에 대해 다음 명령어를 실행합니다.
(예: Debian 12에서 Downloads
디렉터리에 다운로드된 경우)
patch -p1 < ~/Downloads/v10-00NN-x-y-z.patch
※ NN-x-y-z
부분을 적절한 패치 파일명으로 변경하세요.
모든 패치를 적용한 후, 이제 SQL/PGQ가 적용된 Postgres를 빌드 및 설치할 준비가 완료되었습니다!
빌드 및 설치하기
⚠️ 참고:
Postgres를 빌드하기 전에 필수 도구들이 설치되어 있는지 확인하세요.
(Debian 12 기준, libicu-dev
, bison
, flex
등의 패키지를 추가로 설치해야 했습니다.)
Postgres를 빌드하고 설치하는 기본적인 절차는 간단하지만, 필요에 따라 빌드 설정을 조정할 수도 있습니다.
./configure
make
su
make install
adduser postgres
mkdir -p /usr/local/pgsql/data
chown postgres /usr/local/pgsql/data
su - postgres
/usr/local/pgsql/bin/initdb -D /usr/local/pgsql/data
/usr/local/pgsql/bin/pg_ctl -D /usr/local/pgsql/data -l logfile start
/usr/local/pgsql/bin/createdb test
/usr/local/pgsql/bin/psql test
실제 그래프 활용 예시!
이 글의 영감을 준 블로그 게시물에서는 톨킨(Tolkien) 세계관의 캐릭터를 예제 데이터로 사용하고 있습니다.
여기서도 동일한 데이터를 생성한 후, SQL/PGQ 방식으로 동일한 쿼리 결과를 얻을 수 있는지 확인해 보겠습니다.
테이블 생성하기
먼저, psql
을 실행한 후 두 개의 테이블을 생성합니다.
하나는 **캐릭터(노드, Nodes)**를 위한 테이블 다른 하나는 **관계(엣지, Edges)**를 위한 테이블입니다
CREATE TABLE nodes (
id SERIAL PRIMARY KEY,
name TEXT,
details JSONB
);
CREATE TABLE edges (
id SERIAL PRIMARY KEY,
type TEXT,
from_id INTEGER REFERENCES nodes(id),
to_id INTEGER REFERENCES nodes(id),
details JSONB
);
데이터 생성하기
이제 해당 테이블에 데이터를 입력해 보겠습니다.
INSERT INTO nodes (name, details) VALUES
('Frodo Baggins', '{"species": "Hobbit"}'), -- 1
('Bilbo Baggins', '{"species": "Hobbit"}'), -- 2
('Samwise Gamgee', '{"species": "Hobbit"}'), -- 3
('Hamfast Gamgee', '{"species": "Hobbit"}'), -- 4
('Gandalf', '{"species": "Wizard"}'), -- 5
('Aragorn', '{"species": "Human"}'), -- 6
('Arathorn', '{"species": "Human"}'), -- 7
('Legolas', '{"species": "Elf"}'), -- 8
('Thranduil', '{"species": "Elf"}'), -- 9
('Gimli', '{"species": "Dwarf"}'), -- 10
('Gloin', '{"species": "Dwarf"}'); -- 11
-- Parents
INSERT INTO edges (type, from_id, to_id, details) VALUES
('parent', 2, 1, '{}'),
('parent', 4, 3, '{}'),
('parent', 7, 6, '{}'),
('parent', 9, 8, '{}'),
('parent', 11, 10, '{}');
속성 그래프(Property Graph) 생성하기
SQL/PGQ를 사용하면 기존의 관계형 테이블 위에 **속성 그래프(Property Graph)**를 만들고, 이를 GRAPH_TABLE 연산자를 통해 직접 SQL에서 쿼리할 수 있습니다.
GRAPH_TABLE은 SQL에 완전히 통합된 그래프 패턴 매칭 언어를 제공하여, 보다 직관적인 방식으로 그래프 데이터를 조회할 수 있도록 해줍니다.
🔹 참고:
현재 PostgreSQL에서 SQL/PGQ는 개발 진행 중이라 공식 문서가 충분하지 않습니다.
따라서 아래에서는 OracleDB의 PGQL(Property Graph Query Language) 문서를 참고합니다.
PGQL은 SQL:2023 표준을 기반으로 하며, PostgreSQL과는 다른 DBMS지만 개념을 이해하는 데 도움이 됩니다.
속성 그래프에서는 VERTEX TABLE(정점 테이블)을 사용해 노드(Node)를 정의하고, EDGE TABLE(엣지 테이블)을 사용해 노드 간의 관계(Relationship)를 표현합니다.
이 예제에서는:
- 캐릭터(노드)를 저장하는 VERTEX TABLE, 캐릭터 간의 관계(엣지)를 저장하는 EDGE TABLE
을 생성합니다.
관계(엣지)에는 LABEL(레이블) “relationship”을 지정하고, 관계 유형은 'parent'
(부모) 또는 'friend'
(친구) 중 하나가 될 수 있습니다.
SOURCE KEY(출발 노드)와 DESTINATION KEY(도착 노드)를 올바르게 설정해야 합니다.그래프 탐색에서는 방향성이 중요하기 때문입니다. (이 부분은 이후 쿼리에서 확인할 수 있습니다.)
CREATE PROPERTY GRAPH characters
VERTEX TABLES (
nodes LABEL node PROPERTIES ( id, name, details )
)
EDGE TABLES (
edges
SOURCE KEY ( from_id ) REFERENCES nodes ( id )
DESTINATION KEY ( to_id ) REFERENCES nodes ( id )
LABEL relationship PROPERTIES (type)
);
간단한 쿼리 – 부모와 자녀 찾기
속성 그래프에서 관계(Relationship) 레이블이 ‘parent’인 엣지(Edge)를 따라 그래프를 탐색하면 특정 캐릭터의 부모를 찾을 수 있습니다.
예를 들어, 샘와이즈 갬지(Samwise Gamgee)의 부모를 찾아보겠습니다.
재귀적 CTE(Recursive CTE) 활용
WITH child AS (SELECT id FROM nodes WHERE name = 'Samwise Gamgee')
SELECT parent.name FROM child
JOIN edges ON edges.to_id = child.id
JOIN nodes parent ON edges.from_id = parent.id;
SQL/PGQ
SELECT name FROM GRAPH_TABLE (characters
MATCH (a IS node WHERE a.name='Samwise Gamgee') <-[e IS relationship WHERE e.type='parent']- (b IS node)
COLUMNS (b.name AS name)
);
예상대로, Hamfast Gamgee가 반환되었습니다.
속성 그래프를 생성할 때 SOURCE KEY(출발 노드)와 DESTINATION KEY(도착 노드)의 방향성에 대해 언급한 바 있습니다.
여기에서 확인할 수 있듯이, SQL/PGQ의 쿼리 문법에서는 ←[e]→
형태의 화살표를 사용하여 두 노드 간의 관계를 표현합니다.
- 화살표의 **머리(→ 방향)**가 **DESTINATION KEY(도착 노드)**를 가리키고,
- 화살표의 **꼬리(← 방향)**가 **SOURCE KEY(출발 노드)**를 나타냅니다.
즉, 이번 쿼리는 SOURCE KEY가 “parent”(부모) 관계를 맺고 있는 DESTINATION KEY를 찾는 구조입니다.
a e b
DESTINATION KEY <-[relationship.type = parent]- SOURCE KEY
to_id from_id
Samwise Gamgee Hamfast Gamgee
부모를 찾는 쿼리는 자식(DESTINATION KEY)에서 부모(SOURCE KEY) 방향으로 이동하는 구조이므로, 화살표 방향이 왼쪽(←), 즉 **”위쪽”**을 가리키는 형태가 됩니다.
a e b
SOURCE KEY -[relationship.type = parent]-> DESTINATION KEY
from_id to_id
Samwise Gamgee NO MATCH
화살표 방향을 올바르게 유지하는 쉬운 방법은 다음과 같이 표현하는 것입니다:
"SOURCE는 DESTINATION에 대한 RELATIONSHIP이다."
더 복잡한 쿼리 – 친구의 친구 찾기
모든 캐릭터가 서로 직접 아는 것은 아닙니다.
따라서 우정(Friendship) 그래프는 조금 더 복잡한 구조를 가집니다.
다음 쿼리를 실행하여 “friend”(친구) 관계를 엣지(Edges) 테이블에 추가하세요:
SERT INTO edges (type, from_id, to_id, details) VALUES
-- Everyone in the fellowship is friends with everyone else
('friend', 1, 3, '{}'), -- Frodo and Sam
('friend', 1, 5, '{}'), -- Frodo and Gandalf
('friend', 1, 6, '{}'), -- Frodo and Aragorn
('friend', 1, 8, '{}'), -- Frodo and Legolas
('friend', 1, 10, '{}'), -- Frodo and Gimli
('friend', 3, 1, '{}'), -- Sam and Frodo
('friend', 3, 5, '{}'), -- Sam and Gandalf
('friend', 3, 6, '{}'), -- Sam and Aragorn
('friend', 3, 8, '{}'), -- Sam and Legolas
('friend', 3, 10, '{}'), -- Sam and Gimli
('friend', 5, 1, '{}'), -- Gandalf and Frodo
('friend', 5, 3, '{}'), -- Gandalf and Sam
('friend', 5, 6, '{}'), -- Gandalf and Aragorn
('friend', 5, 8, '{}'), -- Gandalf and Legolas
('friend', 5, 10, '{}'), -- Gandalf and Gimli
('friend', 6, 1, '{}'), -- Aragorn and Frodo
('friend', 6, 3, '{}'), -- Aragorn and Sam
('friend', 6, 5, '{}'), -- Aragorn and Gandalf
('friend', 6, 8, '{}'), -- Aragorn and Legolas
('friend', 6, 10, '{}'), -- Aragorn and Gimli
('friend', 8, 1, '{}'), -- Legolas and Frodo
('friend', 8, 3, '{}'), -- Legolas and Sam
('friend', 8, 5, '{}'), -- Legolas and Gandalf
('friend', 8, 6, '{}'), -- Legolas and Aragorn
('friend', 8, 10, '{}'), -- Legolas and Gimli
('friend', 10, 1, '{}'), -- Gimli and Frodo
('friend', 10, 3, '{}'), -- Gimli and Sam
('friend', 10, 5, '{}'), -- Gimli and Gandalf
('friend', 10, 6, '{}'), -- Gimli and Aragorn
('friend', 10, 8, '{}'), -- Gimli and Legolas
-- Bilbo was friends with Hamfast and Gandalf
('friend', 2, 4, '{}'), -- Bilbo and Hamfast
('friend', 2, 5, '{}'), -- Bilbo and Gandalf
-- And vice versa
('friend', 4, 2, '{}'), -- Hamfast and Bilbo
('friend', 5, 2, '{}'), -- Gandalf and Bilbo
-- Gandalf was friends with Bilbo, Hamfast and Thranduil, but for the sake of
-- argument let's say he didn't know Gloin or Arathorn
('friend', 5, 2, '{}'), -- Gandalf and Bilbo
('friend', 5, 4, '{}'), -- Gandalf and Hamfast
('friend', 5, 9, '{}'), -- Gandalf and Thranduil
-- And vice versa
('friend', 2, 5, '{}'), -- Bilbo and Gandalf
('friend', 4, 5, '{}'), -- Hamfast and Gandalf
('friend', 9, 5, '{}'); -- Thranduil and Gandalf
SQL/PGQ를 사용하면 친구의 친구 부모를 찾는 쿼리의 문법이 재귀 CTE와 비교했을 때 훨씬 간단해집니다.
재귀적 CTE(Recursive CTE) 활용
WITH RECURSIVE
root(id) AS (SELECT id FROM nodes WHERE name = 'Samwise Gamgee'),
paths(path) AS (VALUES ('{friend,friend,parent}'::text[])),
results(id) AS (
SELECT root.id, 1 as path_index from root
UNION
SELECT edges.from_id, path_index + 1 AS path_index FROM results
JOIN edges ON edges.to_id = results.id
JOIN paths ON edges.type = paths.path[path_index]
)
SELECT * FROM results
JOIN nodes ON nodes.id = results.id
JOIN paths ON cardinality(paths.path) + 1 = results.path_index;
SQL/PGQ
SELECT DISTINCT id, fof_parents, details FROM GRAPH_TABLE(characters
MATCH (a IS node WHERE a.name='Samwise Gamgee')-[x IS relationship WHERE x.type='friend']->(b IS node)-[y IS relationship WHERE y.type='friend']->(c IS node)<-[z IS relationship WHERE z.type='parent']-(d IS node)
COLUMNS (d.id, d.name as fof_parents, d.details as details)
);
MATCH 문에서 화살표의 형태는 그래프 탐색 과정을 직관적으로 표현해 줍니다.
(Samwise Gamgee) -[friendship]-> (friend) -[friendship]-> (friend-of-friend) <-[parent]-(parent-of-friend-of-friend)
우리는 샘와이즈 갬지(Samwise Gamgee)와 연결된 ‘friend'(친구) 관계의 엣지를 따라 두 단계 이동하여 친구의 친구 노드를 찾습니다.
그리고 처음 예제에서처럼 왼쪽 화살표(←)를 사용하여 각 친구의 친구 노드의 부모를 조회합니다.
쿼리 결과는 원래 블로그 게시물과 동일하게 반환됩니다.
id | fof_parents | details
----+----------------+-----------------------
2 | Bilbo Baggins | {"species": "Hobbit"}
4 | Hamfast Gamgee | {"species": "Hobbit"}
7 | Arathorn | {"species": "Human"}
9 | Thranduil | {"species": "Elf"}
11 | Gloin | {"species": "Dwarf"}
(5 rows)
결론
SQL/PGQ는 PostgreSQL에서 그래프 데이터를 직접 다룰 수 있는 강력하고 유연하며, 직관적인 인터페이스를 제공합니다!
이번 예제들은 기본적인 개념만 다루었지만, 참고한 블로그 게시물과 동일한 결과를 도출하며, Postgres에서 그래프를 활용하는 또 다른 방법을 보여줍니다.
아직 공식 출시 일정이 정해지지는 않았으며, 이 글에서 소개한 기능도 개발 중인 상태입니다.
그러나 앞으로 PostgreSQL의 정식 버전에서 이 흥미로운 기능이 완전히 지원되기를 기대합니다!
마지막으로, 이 기술을 가능하게 해준 pgsql-hackers 커뮤니티의 모든 개발자분들께 감사드립니다. 😊