PostgreSQL에서 SQL/PGQ를 활용한 그래프 데이터 모델링과 쿼리
John Nevin
2025년 2월 18일
PostgreSQL에서 그래프 데이터를 모델링하고 탐색하는 방법은 재귀적 공통 테이블 표현식(Recursive Common Table Expression, Recursive CTE)에만 국한되지 않습니다. 이 글에서는 SQL 속성 그래프 쿼리(SQL Property Graph Queries, SQL/PGQ)를 PostgreSQL에 적용하고, 속성 그래프를 생성한 뒤 GRAPH_TABLE로 관계를 조회하는 과정을 살펴봅니다.
토르킨(Tolkien)과 그래프 이론을 좋아하는 저에게 EDB 동료들이 공유한 훌륭한 블로그 글은 무척 흥미로웠습니다.
해당 글의 작성자는 PostgreSQL에서 그래프를 모델링하는 방법과 재귀적 CTE를 활용해 그래프 탐색 쿼리를 실행하는 방법을 설명하며, 이를 PostgreSQL에서 그래프 데이터를 표현하고 유연하게 쿼리하는 한 가지 방법이라고 정리했습니다. 또한 다른 기법이나 개선 방안이 있다면 공유해 달라는 의견을 남겼습니다.
실제로 PostgreSQL에서 그래프를 보다 효과적으로 다룰 수 있도록 하는 새로운 기술이 개발되고 있습니다.
SQL 속성 그래프 쿼리(SQL/PGQ)란?
SQL 속성 그래프 쿼리(SQL/PGQ)는 최근 SQL:2023 ISO 표준에 포함되었으며, 별도의 그래프 데이터베이스 관리 시스템(예: Neo4j) 없이도 기존 관계형 데이터를 그래프로 표현하고 효율적으로 쿼리할 수 있도록 지원합니다.
노드 간의 관계를 연결된 엣지(Edge)로 표현하는 방식은 직관적이며, 특히 소셜 네트워크의 친구 관계, 교통망, 컴퓨터 네트워크 등 다양한 네트워크를 모델링하는 데 적합합니다.
PostgreSQL에 SQL/PGQ 기능 추가하기
PostgreSQL에 그래프 데이터베이스 기능을 추가하는 Apache AGE™와 같은 서드파티 확장이 존재하지만, PostgreSQL에 SQL/PGQ를 도입하기 위한 논의와 작업도 pgsql-hackers 메일링 리스트에서 꾸준히 진행되고 있습니다.
아직 정식 출시 일정은 정해지지 않았지만, 현재 기능적으로 동작하며 PostgreSQL에 패치를 적용하면 직접 실험해 볼 수 있습니다.
세 단계로 PostgreSQL SQL/PGQ 패치 적용하기
이 패치를 적용하려면 PostgreSQL을 소스 코드에서 빌드하고 설치해야 합니다. PostgreSQL 공식 문서에서 이에 관한 상세한 가이드를 제공하고 있습니다.
1. 패치 파일 다운로드
먼저 pgsql-hackers 스레드에서 모든 .patch 파일을 다운로드합니다.
2. PostgreSQL 소스 코드 다운로드
PostgreSQL 소스 코드를 가져온 후 해당 디렉터리로 이동하여 설치 절차를 진행합니다. 자세한 다운로드 방법은 PostgreSQL 소스 코드 다운로드 공식 문서에서 확인할 수 있습니다.
3. 패치 파일 적용
다운로드한 각 .patch 파일에 대해 다음 명령어를 실행합니다. 아래는 Debian 12에서 패치 파일을 Downloads 디렉터리에 다운로드한 경우의 예입니다.
patch -p1 < ~/Downloads/v10-00NN-x-y-z.patch
※ NN-x-y-z 부분을 적절한 패치 파일명으로 변경하세요.
모든 패치를 적용하면 SQL/PGQ가 적용된 PostgreSQL을 빌드하고 설치할 준비가 완료됩니다.
패치된 PostgreSQL 빌드 및 설치하기
⚠️ 참고: PostgreSQL을 빌드하기 전에 필수 도구가 설치되어 있는지 확인하세요. Debian 12 기준으로 libicu-dev, bison, flex 등의 패지를 추가로 설치해야 했습니다.
PostgreSQL을 빌드하고 설치하는 기본 절차는 간단하지만, 필요에 따라 빌드 설정을 조정할 수도 있습니다.
./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 testPostgreSQL SQL/PGQ 그래프 활용 예제
이 글에 영감을 준 블로그 게시물에서는 톨킨(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의 쿼리 문법에서는 화살표를 사용해 두 노드 간 관계의 방향을 표현합니다.
- 화살표의 머리(→ 방향)는 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'(친구) 관계를 엣지 테이블에 추가하세요.
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 GandalfSQL/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)PostgreSQL SQL/PGQ 활용 정리
SQL/PGQ는 PostgreSQL에서 그래프 데이터를 직접 다룰 수 있는 강력하고 유연하며 직관적인 인터페이스를 제공합니다.
이번 예제에서는 기본적인 개념만 다루었지만, 참고한 블로그 게시물과 동일한 결과를 도출하며 PostgreSQL에서 그래프를 활용하는 또 다른 방법을 보여줍니다.
아직 공식 출시 일정은 정해지지 않았으며, 이 글에서 소개한 기능도 개발 중인 상태입니다. 앞으로 PostgreSQL 정식 버전에서 이 기능이 완전히 지원되기를 기대합니다.
마지막으로 이 기술을 가능하게 해준 pgsql-hackers 커뮤니티의 모든 개발자분들께 감사드립니다. 😊
PostgreSQL SQL/PGQ FAQ
SQL/PGQ란 무엇인가요?
SQL 속성 그래프 쿼리(SQL/PGQ)는 SQL:2023 ISO 표준에 포함된 그래프 쿼리 기능입니다. 기존 관계형 데이터를 속성 그래프로 표현하고 SQL에서 그래프 패턴을 조회할 수 있도록 지원합니다.
PostgreSQL에서 SQL/PGQ를 바로 사용할 수 있나요?
이 글에서 다루는 PostgreSQL용 SQL/PGQ 기능은 개발 중이며 공식 출시 일정이 정해지지 않았습니다. 현재는 관련 패치를 PostgreSQL 소스 코드에 적용한 후 직접 빌드하고 설치하여 실험할 수 있습니다.
SQL/PGQ와 재귀적 CTE의 차이점은 무엇인가요?
재귀적 CTE와 SQL/PGQ 모두 관계형 데이터의 연결 관계를 탐색할 수 있습니다. SQL/PGQ는 MATCH 문과 화살표를 사용해 노드와 엣지의 탐색 경로를 그래프 형태로 보다 직관적으로 표현합니다.
SQL/PGQ 속성 그래프에서 SOURCE KEY와 DESTINATION KEY가 중요한 이유는 무엇인가요?
SOURCE KEY와 DESTINATION KEY는 엣지의 출발 노드와 도착 노드를 정의합니다. SQL/PGQ의 화살표 방향이 이 키를 기준으로 관계의 탐색 방향을 나타내므로 올바르게 설정해야 합니다.
PostgreSQL에서 SQL/PGQ 패치를 테스트하려면 무엇이 필요한가요?
pgsql-hackers 스레드에서 패치 파일을 내려받고 PostgreSQL 소스 코드에 적용해야 합니다. 이후 필요한 빌드 도구를 설치한 환경에서 PostgreSQL을 소스 코드로 빌드하고 설치해야 합니다.