Postgres SQL/PGQ

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, ...

Introduzione

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!

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.

Strutture dati SQL

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;

SQL Property Graph

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:

create property graph connections_pg vertex tables ( people key (person_id) label person properties all columns ) edge tables ( connections key (connection_id) source key (person_id_1) references people (person_id) destination key (person_id_2) references people (person_id) label connection properties all columns );

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:

create property graph sales_pg vertex tables ( people key (person_id) label person properties all columns, products key (product_id) label product properties all columns ) edge tables ( sales key (sale_id) source key (person_id) references people (person_id) destination key (product_id) references products (product_id) label sale properties all columns );

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)

Property Graph Query

Ora possiamo interrogare i property graph con GRAPH_TABLE:

select person1, person2 from graph_table (connections_pg match (p1 is person) -[c is connection]-> (p2 is person) columns (p1.name as person1, p2.name as person2) ) order by 1; person1 | person2 --------------+-------------- Bruce Banner | Jean Grey Bruce Banner | Peter Parker Bruce Banner | Wonder Woman Clark Kent | Wonder Woman Jean Grey | Clark Kent Jean Grey | Wonder Woman Jean Grey | Bruce Banner Peter Parker | Clark Kent Wonder Woman | Peter Parker Wonder Woman | Clark Kent Wonder Woman | Jean Grey (11 rows)

Naturalmente e' possibile utilizzare condizioni: PGQViewer

select person1, person2 from graph_table (connections_pg match (p1 is person where p1.name = 'Jean Grey') -[c is connection]-> (p2 is person) columns (p1.name as person1, p2.name as person2) ) order by 1; person1 | person2 -----------+-------------- Jean Grey | Wonder Woman Jean Grey | Clark Kent Jean Grey | Bruce Banner (3 rows) select person1, person2 from graph_table (connections_pg match (p1 is person where p1.name = 'Jean Grey') -[c is connection]-> (p2 is person) columns (p1.name as person1, p2.name as person2) ) where person2 = 'Clark Kent' order by 1; person1 | person2 -----------+------------ Jean Grey | Clark Kent (1 row) select person1, product, person2 from graph_table (sales_pg match (p1 is person) -[s1 is sale]-> (pr is product) <-[s2 is sale]- (p2 is person) columns (p1.name as person1, p2.name as person2, pr.name as product) ) where person1 != person2 order by 1; person1 | product | person2 --------------+---------+-------------- Bruce Banner | banana | Jean Grey Bruce Banner | lemon | Wonder Woman Bruce Banner | lemon | Jean Grey Bruce Banner | lemon | Clark Kent Bruce Banner | banana | Wonder Woman Bruce Banner | banana | Peter Parker Clark Kent | lemon | Jean Grey Clark Kent | lemon | Wonder Woman Clark Kent | lemon | Bruce Banner Jean Grey | apple | Peter Parker Jean Grey | banana | Bruce Banner Jean Grey | banana | Peter Parker Jean Grey | banana | Wonder Woman Jean Grey | lemon | Bruce Banner Jean Grey | lemon | Clark Kent Jean Grey | lemon | Wonder Woman Peter Parker | banana | Wonder Woman Peter Parker | banana | Jean Grey Peter Parker | banana | Bruce Banner Peter Parker | apple | Jean Grey Wonder Woman | lemon | Bruce Banner Wonder Woman | banana | Jean Grey Wonder Woman | banana | Peter Parker Wonder Woman | lemon | Jean Grey Wonder Woman | lemon | Clark Kent Wonder Woman | banana | Bruce Banner (26 rows)

PGQViewer

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: PGQViewer

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: PGQViewer

Naturalmente e' possibile sperimentare tutte le possibilita' grafiche di PGQViewer e le diverse condizioni delle query a grafo!

Query Transformation

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: SQL/PGQ PEV2

EXPLAIN (COSTS OFF, PLAN_ADVICE) select person1, product, person2 from graph_table (sales_pg match (p1 is person) -[s1 is sale]-> (pr is product) <-[s2 is sale]- (p2 is person) columns (p1.name as person1, p2.name as person2, pr.name as product) ) where person1 != person2 order by 1; QUERY PLAN --------------------------------------------------------------------------- Sort Sort Key: people.name -> Hash Join Hash Cond: (sales_1.person_id = people_1.person_id) Join Filter: (people.name <> people_1.name) -> Hash Join Hash Cond: (products.product_id = sales_1.product_id) -> Hash Join Hash Cond: (sales.product_id = products.product_id) -> Hash Join Hash Cond: (sales.person_id = people.person_id) -> Seq Scan on sales -> Hash -> Seq Scan on people -> Hash -> Seq Scan on products -> Hash -> Seq Scan on sales sales_1 -> Hash -> Seq Scan on people people_1 Generated Plan Advice: JOIN_ORDER(sales people products sales#2 people#2) HASH_JOIN(people products sales#2 people#2) SEQ_SCAN(sales people products sales#2 people#2) NO_GATHER(people sales products sales#2 people#2) (25 rows)

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!

JSON

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:

drop property graph connections_pg; create property graph connections_pg vertex tables ( people key (person_id) label person properties (name, json_data->>'gender' AS gender, json_data->>'universe' AS universe) ) edge tables ( connections key (connection_id) source key (person_id_1) references people (person_id) destination key (person_id_2) references people (person_id) label connection properties (person_id_1, person_id_2) );

Ed interrogare con:

select person1, gender1, universe1, person2, gender2, universe2 from graph_table (connections_pg match (p1 is person) -[c is connection]-> (p2 is person) columns (p1.name as person1, p1.gender as gender1, p1.universe as universe1, p2.name as person2, p2.gender as gender2, p2.universe as universe2) ) order by 1; person1 | gender1 | universe1 | person2 | gender2 | universe2 --------------+---------+-----------+--------------+---------+----------- Bruce Banner | male | Marvel | Jean Grey | female | DC Bruce Banner | male | Marvel | Peter Parker | male | Marvel Bruce Banner | male | Marvel | Wonder Woman | female | DC Clark Kent | male | Marvel | Wonder Woman | female | DC Jean Grey | female | DC | Clark Kent | male | Marvel Jean Grey | female | DC | Wonder Woman | female | DC Jean Grey | female | DC | Bruce Banner | male | Marvel Peter Parker | male | Marvel | Clark Kent | male | Marvel Wonder Woman | female | DC | Peter Parker | male | Marvel Wonder Woman | female | DC | Clark Kent | male | Marvel Wonder Woman | female | DC | Jean Grey | female | DC (11 rows)

Limiti

Se cerchiamo quali sono le persone non connesse direttamente tra loro e la distanza nel property graph connections_pg vorremo ottenere:

... ORDER BY distance, person1, person2; person1 | person2 | distance --------------+--------------+---------- Clark Kent | Bruce Banner | 2 Peter Parker | Jean Grey | 2 (2 rows)

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!

Varie ed eventuali

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 (3/5)
Data: 4 Giugno 2026
Versione: 1.0.1 - 21 Giugno 2026 ☀️
Autore: mail [AT] meo.bogliolo.name