La versione PostgreSQL 19 introduce nuove funzionalita' all'RDBMS Open Source piu' avanzato. Tra le molte novita' introdotte vi sono le graph query che consentono di interrogare dati strutturati a grafo. In questa paginetta presentiamo, con esempi semplici e pratici, come utilizzare le Graph Query con PostgreSQL.
Dopo una breve introduzione utilizzeremo un esempio completo per mostrare i principali aspetti di SQL/PGQ (Property Graph Query) che tocchera' i seguenti aspetti: strutture dati SQL, SQL Property Graph, Property Graph Query, PGQViewer, EXPLAIN, JSON, Limiti, ...
PostgreSQL dalla versione 19 introduce la possibilita' di rappresentare le strutture a grafo in modo nativo nel modello relazionale.
Un property graph ha un modo differente di rappresentare i contenuti rispetto alle tabelle dei database relazionali. Per interrogare un property graph si usano query di pattern matching al posto dei join tradizionali. PostgreSQL implementa il SQL/PGQ (Property Graph Query) come definito dallo standard ISO/IEC 9075-16:2023. In pratica vengono aggiunti due nuovi costrutti: CREATE PROPERTY GRAPH e GRAPH_TABLE.
Le
In PostgreSQL un property graph e' come una vista che consente di interrogare i dati con una sintassi particolare,
ed in cui i dati sono comunque mantenuti in normali tabelle
[NdA nei Graph Database nativi i dati sono mantenuti in strutture a grafo:
l'implementazione di PostgreSQL e' molto differente].
Le clausole delle graph queries e delle normali query relazionali possono essere usate negli stessi statement
SQL ed utilizzano le stesse modalita' per essere eseguite.
Lo statement CREATE PROPERTY GRAPH
permette di indicare
come i nodi (vertex) si collegano (edge) tra loro.
Con GRAPH_TABLE si impostano i criteci di ricerca sul grafo e
la clausola MATCH consente di indicare la condizione di collegamento.
Nel catalogo PostgreSQL sono state aggiunte 5 nuove viste:
pg_propgraph_element, pg_propgraph_label, pg_propgraph_property, pg_propgraph_element_label,
e pg_propgraph_label_property che descrivono ogni proprieta' dei grafi.
Come sempre la documentazione ufficiale riporta tutti i dettagli. Ma ora puo' essere piu' utile un esempio!
Un buon esempio e' spesso molto piu' utile di una lunga descrizione.
Le strutture, dati, e le query sono liberamente tratte da un'ottima pagina di oracle-base. Naturalmente vi sono differenze tra l'implementazione su Oracle e quella di PostgreSQL, ma vi sono anche similitudini: entrambe implementano lo standard SQL/PGQ! In questa paginetta presenteremo l'implementazione di PostgreSQL con esempi funzionanti dalla versione 19.
Creiamo le tabelle che corrispondono ai nodi ed ai collegamenti. Questo e' SQL standard e non richiede alcun commento particolare:
create table people (
person_id bigint primary key,
name varchar(15)
);
insert into people (person_id, name)
values (1, 'Wonder Woman'),
(2, 'Peter Parker'),
(3, 'Jean Grey'),
(4, 'Clark Kent'),
(5, 'Bruce Banner');
create table connections (
connection_id bigint primary key,
person_id_1 bigint,
person_id_2 bigint,
constraint connections_people_1_fk foreign key (person_id_1) references people (person_id),
constraint connections_people_2_fk foreign key (person_id_2) references people (person_id)
);
create index concurrently connections_person_1_idx on connections (person_id_1);
create index concurrently connections_person_2_idx on connections (person_id_2);
insert into connections (connection_id, person_id_1, person_id_2)
values (1, 1, 2),
(2, 1, 3),
(3, 1, 4),
(4, 2, 4),
(5, 3, 1),
(6, 3, 4),
(7, 3, 5),
(8, 4, 1),
(9, 5, 1),
(10, 5, 2),
(11, 5, 3);
create table products (
product_id bigint primary key,
name varchar(10)
);
insert into products (product_id, name)
values (1, 'apple'),
(2, 'banana'),
(3, 'lemon'),
(4, 'lime');
create table sales (
sale_id bigint primary key,
person_id bigint,
product_id bigint,
quantity bigint,
constraint sales_people_fk foreign key (person_id) references people (person_id),
constraint sales_products_fk foreign key (product_id) references products (product_id)
);
create index concurrently sales_person_id_idx on sales (person_id);
create index concurrently sales_product_idx on sales (product_id);
insert into sales (sale_id, person_id, product_id, quantity)
values (1, 1, 2, 10),
(2, 1, 3, 5),
(3, 2, 2, 6),
(4, 2, 1, 4),
(5, 3, 2, 3),
(6, 3, 1, 6),
(7, 3, 3, 2),
(8, 4, 3, 8),
(9, 5, 2, 6),
(10, 5, 4, 5),
(11, 5, 3, 3);
analyze people;
analyze connections;
analyze products;
analyze sales;
Un Property Graph e' un modello che descrive i nodi (vertex) e le relazioni tra loro (edge). In PostgreSQL i vertex e gli edge sono regolari tabelle, viste, foreign table, ... i Property Graph sono i metadati che descrivono le relazioni presenti.
Creiamo un property graph dove la tabella PEOPLE rappresenta i nodi
e la tabella CONNECTIONS rappresenta le relazioni.
Con LABEL e' possibile associare un nome utilizzabile in seguito nelle query.
E' importante notare che il grafo e' orientato ovvero le connessioni hanno
un SOURCE KEY ed un DESTINATION KEY.
Ecco i comandi:
Creiamo un secondo Property Graph per le vendite dove le tabelle PEOPLE e PRODUCTS sono i nodi
e la tabella SALES rappresenta le relazioni.
Ecco i comandi:
Le definizioni correnti dei property graphs definiti possono essere visualizzate interrogando tabelle del data dictionary:
SELECT * FROM pg_propgraph_element; -- nodi del grafo logico (vertex / edge tables) SELECT * FROM pg_propgraph_element_label; -- mapping elemento ↔ label SELECT * FROM pg_propgraph_label; -- etichette del modello (label) SELECT * FROM pg_propgraph_label_property; -- mapping label ↔ proprieta' esposte SELECT * FROM pg_propgraph_property; -- espressioni delle proprieta' (non solo colonne ma anche computed expressions)
Ora possiamo interrogare i property graph con GRAPH_TABLE:
Naturalmente e' possibile utilizzare condizioni:
Per visualizzare i grafici utilizzamo un'applicazione Open Source recentissima:
PGQViewer.
L'installazione e' molto semplice perche' avviene su un container:
git clone https://github.com/aoncodev/PGQViewer.git cd PGQViewer docker build -t pgqviewer . docker run --rm -p 127.0.0.1:8080:8080 -v pgqviewer-data:/data pgqviewer
Quindi si accede all'applicazione grafica con un browser sull'URL http://localhost:8080/.
La base dati PostgreSQL deve essere accedibile dal container, se e' sulla stessa macchina come host basta utilizzare host.docker.internal, gli altri parametri di connessione sono quelli tipici di PostgreSQL [NdA su Linux occorre anche aggiungere --add-host=host.docker.internal:host-gateway al comando di run].
Selezioniamo il property graph connections_pg ed impostiamo la seguente query:
(p1 is person) -[c is connection]-> (p2 is person)
Otteniamo la seguente visualizzazione:
Selezioniamo il property graph sales_pg ed impostiamo la seguente query:
(p1 is person) -[s1 is sale]-> (pr is product) <-[s2 is sale]- (p2 is person)
Otteniamo la seguente visualizzazione:
Naturalmente e' possibile sperimentare tutte le possibilita' grafiche di PGQViewer e le diverse condizioni delle query a grafo!
In PostgreSQL un property graph e' come una vista che consente di interrogare i dati con una sintassi particolare
ed i cui i dati sono comunque mantenuti in normali tabelle oppure viste, foreign table, ...
insomma i normali oggetti che PostgreSQL gestisce.
L'implementazione delle ricerche sui property graph e' simile al meccanismo
delle viste, che in PostgreSQL e' implementato su regole di traduzione.
Per i property graph vengono utilizzati i metadati ed
alla fine viene composto uno statement SQL con i join necessari,
le CTE e quando altro serve per ottenere i dati corretti.
Per controllare l'esecuzione di una property graph query e, se necessario,
eseguire un tuning si utilizzano i comandi standard di PostgreSQL:
E' anche possibile impostare flag per ottenere maggiori dettagli nei file di log o ricompilare i sorgenti con la modalita' di debug... ma quanto riportato dovrebbe gia' essere sufficiente anche per i piu' curiosi!
Quando le tabelle sono complesse si preferisce definire con la clausola PROPERTIES l'elenco delle colonne da utilizzare nelle query grafiche. Questo vale anche se debbono essere usate espressioni o se si vogliono utilizzare dati memorizzati in formato JSON.
Definiamo una colonna JSONB per introdurre nuove informazioni:
alter table people add column json_data jsonb;
update people
set json_data = json('{"gender":"female", "universe":"DC"}')
where person_id = 1;
update people
set json_data = json('{"gender":"male", "universe":"Marvel"}')
where person_id = 2;
update people
set json_data = json('{"gender":"female", "universe":"DC"}')
where person_id = 3;
update people
set json_data = json('{"gender":"male", "universe":"Marvel"}')
where person_id = 4;
update people
set json_data = json('{"gender":"male", "universe":"Marvel"}')
where person_id = 5;
Possiamo ricreare il property graph con le nuove definizioni:
Ed interrogare con:
Se cerchiamo quali sono le persone non connesse direttamente tra loro e la distanza nel property graph connections_pg vorremo ottenere:
Ma il risultato riportato l'ho ottenuto con una complessa CTE ricorsiva:
WITH RECURSIVE graph_edges AS (
-- trasformiamo il grafo in non diretto
SELECT person_id_1 AS src, person_id_2 AS dst
FROM connections
UNION
SELECT person_id_2 AS src, person_id_1 AS dst
FROM connections
),
bfs AS (
-- punto di partenza: ogni nodo da se stesso
SELECT
p.person_id AS start_id,
p.person_id AS current_id,
0 AS distance,
ARRAY[p.person_id] AS visited
FROM people p
UNION ALL
-- espansione BFS (Breadth-First Search)
SELECT
bfs.start_id,
e.dst,
bfs.distance + 1,
bfs.visited || e.dst
FROM bfs
JOIN graph_edges e
ON e.src = bfs.current_id
WHERE NOT e.dst = ANY(bfs.visited)
),
min_dist AS (
SELECT
start_id,
current_id,
MIN(distance) AS distance
FROM bfs
GROUP BY start_id, current_id
)
SELECT
p1.name AS person1,
p2.name AS person2,
md.distance
FROM min_dist md
JOIN people p1 ON p1.person_id = md.start_id
JOIN people p2 ON p2.person_id = md.current_id
WHERE md.distance>1
AND md.start_id < md.current_id
ORDER BY distance, person1, person2;
person1 | person2 | distance
--------------+--------------+----------
Clark Kent | Bruce Banner | 2
Peter Parker | Jean Grey | 2
(2 rows)
-- Versione con stringhe
WITH RECURSIVE graph_edges AS (
SELECT person_id_1 AS src, person_id_2 AS dst
FROM connections
UNION
SELECT person_id_2 AS src, person_id_1 AS dst
FROM connections
),
bfs AS (
SELECT
p.person_id AS start_id,
p.person_id AS current_id,
0 AS distance,
'/' || p.person_id || '/' AS path
FROM people p
UNION ALL
SELECT
bfs.start_id,
e.dst,
bfs.distance + 1,
bfs.path || e.dst || '/'
FROM bfs
JOIN graph_edges e
ON e.src = bfs.current_id
WHERE bfs.path NOT LIKE '%/' || e.dst || '/%'
),
min_dist AS (
SELECT
start_id,
current_id,
MIN(distance) AS distance
FROM bfs
GROUP BY start_id, current_id
)
SELECT
p1.name AS person1,
p2.name AS person2,
md.distance
FROM min_dist md
JOIN people p1 ON p1.person_id = md.start_id
JOIN people p2 ON p2.person_id = md.current_id
WHERE md.distance > 1
AND md.start_id < md.current_id
ORDER BY distance, person1, person2;
Vi sono ancora diversi limiti nell'implementazione dell'SQL/PGQ in PostgreSQL, per altro
tutti esplicitamente documentati:
Different-edges match mode, All shortest path search, ELEMENT_ID function, Quantified paths, ...
Premesso che l'SQL/PGQ non e' l'implementazione di un Graph Database e che PostgreSQL 19
introduce solo alcune delle funzionalita' previste dallo standard,
questo e' sicuramente il primo importante passo verso un'implementazione sempre piu' completa e robusta!
Ai piu' esperti non sara' sfuggito l'uso del Plan Advice nell'EXPLAIN: si tratta di una nuova funzionalita' introdotta anch'essa nella versione 19.
La versione 19 non e' ancor disponibile... ma si puo' gia' installare PG19 Beta1.
PostgreSQL e' in continua evoluzione: il progetto ha quasi 40 anni, l'Engine SQL e' disponibile da 30 anni, il JSON e' supportato dalla versione 9.2 [NdA dal 2012 e con continue evoluzioni tra cui l'efficiente datatype JSONB nella 9.4], l'estensione pgvector e' disponibile da anni [NdA dal 2021], la versione PG19, prevista per il 2026-09 introdurra' anche una prima implementazione dell'SQL/PGQ, ... le possibilità per query ibride relazionali e semi-strutturate continuano ad ampliarsi!
Titolo: Postgres SQL/PGQ
Livello: Avanzato
Data: 4 Giugno 2026
Versione: 1.0.1 -
21 Giugno 2026 ☀️
Autore:
mail [AT] meo.bogliolo.name