Dev & EngARTIGO

PostgreSQL tenta grafos com SQL/PGQ e reverte a implementação antes da versão 19

O recurso SQL/PGQ chegou a ser incorporado ao PostgreSQL 19 para consultar grafos dentro do SQL padrão, mas foi retirado em setembro de 2026 por problemas de catálogo. Entenda como a reescrita em joins funciona e o que ainda falta para substituir recursive CTE em consultas de grafo.

PostgreSQL tenta grafos com SQL/PGQ e reverte a implementação antes da versão 19
Imagem gerada por IA

O PostgreSQL↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → chegou a incorporar, em 16 de março de 2026, uma implementação do SQL/PGQ, a extensão de grafos padronizada no SQL:2023. A ideia era permitir consultar padrões de grafo (quem conhece quem, quem lutou onde) dentro de um SELECT comum, sem recorrer a WITH RECURSIVE nem a uma extensão como Apache AGE.

Em setembro, depois de uma discussão na lista pgsql-hackers sobre problemas na representação de catálogo e no rastreamento de dependências, o patch foi revertido antes do lançamento da versão 19. É esse percurso, do commit ao revert, que um post no Planet PostgreSQL detalha, e que vale destrinchar para quem hoje resolve problema de grafo escrevendo WITH RECURSIVE no PostgreSQL.

O problema que o SQL nunca resolveu bem

O ecossistema de grafos nunca convergiu para uma linguagem só. Cypher (Neo4j) usa arte ASCII para representar padrões; Gremlin (Apache TinkerPop) é imperativo e baseado em passos; PGQL (Oracle) embute padrões de grafo dentro de uma casca parecida com SQL. Em 2024 o GQL virou ISO/IEC 39075, a primeira linguagem de consulta a banco de dados ratificada pela ISO desde o próprio SQL.

O SQL/PGQ, padronizado dentro do SQL:2023, é diferente dos outros: não é uma linguagem nova, é uma extensão do SELECT que permite declarar um grafo de propriedades sobre tabelas já existentes e consultá-lo com sintaxe de padrão parecida com GQL e PGQL, dentro de um GRAPH_TABLE.

Por que SQL puro fica feio rápido

Para dois saltos, o SQL relacional ainda é tolerável: um self-join duplo resolve "amigos de amigos". O problema aparece quando a pergunta é sobre alcance, não sobre um número fixo de saltos. Pedir todas as pessoas alcançáveis a partir de uma origem em até cinco saltos de uma aresta KNOWS vira um WITH RECURSIVE com array de caminho para evitar ciclo, contagem de profundidade e remoção da linha-semente no final:

sql
WITH RECURSIVE reachable(id, depth, path) AS (
 SELECT p.id, 0, ARRAY[p.id]
 FROM person p WHERE p.name = 'Alice'
 UNION ALL
 SELECT k.dst, r.depth + 1, r.path || k.dst
 FROM reachable r
 JOIN knows k ON k.src = r.id
 WHERE r.depth < 5 AND k.dst <> ALL(r.path)
)
SELECT DISTINCT p.name
FROM reachable r JOIN person p ON p.id = r.id
WHERE r.depth > 0;

As cláusulas SEARCH e CYCLE, adicionadas no PostgreSQL 14, escondem parte dessa contabilidade, mas a consulta continua sendo um cálculo de ponto fixo recursivo que, por coincidência, devolve a resposta certa. Em SQL/PGQ, a mesma pergunta é um único padrão: MATCH (a IS Person WHERE a.name = 'Alice')-[IS KNOWS]->{1,5}(c IS Person).

Como fica na prática: Jedi, Sith e tabelas comuns

O post usa um exemplo com tabelas jedi, sith, planet e station, ligadas por fought_on_planet e fought_on_station. O grafo é declarado sobre elas sem mover um byte de dado:

sql
CREATE PROPERTY GRAPH galaxy
VERTEX TABLES (
 jedi KEY (id) LABEL force_user PROPERTIES (id, name, title),
 sith KEY (id) LABEL force_user PROPERTIES (id, name, title),
 planet KEY (id) LABEL place PROPERTIES (id, name, region),
 station KEY (id) LABEL place PROPERTIES (id, name, region)
)
EDGE TABLES (
 fought_on_planet SOURCE KEY (a) REFERENCES jedi (id)
 DESTINATION KEY (b) REFERENCES planet (id)
 LABEL fought_at PROPERTIES (battle),
 fought_on_station SOURCE KEY (a) REFERENCES sith (id)
 DESTINATION KEY (b) REFERENCES station (id)
 LABEL fought_at PROPERTIES (battle)
);

No catálogo, esse CREATE PROPERTY GRAPH vira um novo tipo de relação (RELKIND_PROPGRAPH) que fica em pg_class sem heap e sem colunas próprias, comportando-se como uma view. Cinco catálogos novos, entre eles pg_propgraph_element e pg_propgraph_label, guardam elementos, rótulos e propriedades. Rótulos agrupam tabelas: force_user cobre jedi e sith; place cobre planet e station.

Por baixo do capô, é tudo join

Este é o ponto que mais importa para quem decide se adota a feature mais à frente: o PostgreSQL não ganhou um executor de grafos. O parser transforma GRAPH_TABLE numa nova entrada de range table (RTE_GRAPH_TABLE), e o rewriter, o mesmo estágio que expande views, substitui isso por uma subconsulta comum, em quatro passos:

  • lê o padrão da esquerda para a direita como uma lista de vértices, arestas (em qualquer direção) e subpadrões aninhados;
  • cada nome de variável único vira um "fator de caminho"; uma variável repetida (um ciclo que volta ao mesmo ponto) compartilha um fator, com predicados unidos por AND;
  • cada expressão de rótulo é resolvida nas tabelas que ela cobre, gerando um elemento de caminho por tabela;
  • uma busca em profundidade (DFS) enumera toda combinação de elementos ao longo do caminho, descarta as que não batem com as chaves de origem e destino declaradas, e une as válidas com UNION ALL.

No exemplo da galáxia, force_user tem duas tabelas, fought_at tem duas, place tem duas: 2 × 2 × 2 = 8 combinações possíveis, das quais só duas são válidas (Jedi em planeta, Sith em estação). O resultado final é literalmente isto:

sql
SELECT jedi.name, planet.name, planet.region
FROM jedi, fought_on_planet, planet
WHERE jedi.id = fought_on_planet.a AND planet.id = fought_on_planet.b
UNION ALL
SELECT sith.name, station.name, station.region
FROM sith, fought_on_station, station
WHERE sith.id = fought_on_station.a AND station.id = fought_on_station.b;

Daí em diante, o planner e o executor enxergam apenas joins e um union, nada específico de grafo. Isso é bom notícia para quem já confia em EXPLAIN, índice e estatística do PostgreSQL: o SQL/PGQ herda tudo isso de graça, em vez de depender de um motor de grafo separado com seu próprio otimizador.

Há uma pegadinha que muda como ler qualquer benchmark que compare essa abordagem com joins manuais. Num padrão de comprimento fixo, o SQL/PGQ é obrigado a tocar as tabelas de vértice, porque o padrão vincula vértices como parte da semântica, mesmo quando nenhuma propriedade de vértice é pedida, já que não há chave estrangeira (FK) garantindo a ligação.

Se você modela o mesmo grafo à mão e define FKs entre as tabelas de aresta e vértice, basta percorrer a tabela de aresta. Há um esforço recente (ainda não aceito) na lista de hackers do PostgreSQL para eliminar inner joins baseados em FK, o que beneficiaria justamente a reescrita do SQL/PGQ quando as tabelas base já têm FK.

O que ficou de fora

O padrão SQL:2023 define 100 características relacionadas a grafos. A implementação que chegou a ser aceita cobria 26 delas: padrões com WHERE, vértice e aresta em forma completa e abreviada, disjunção de rótulo (label1|label2), referência a propriedade, GRAPH_TABLE, o DDL completo (incluindo ALTER e DROP), múltiplos rótulos por tabela e views como elemento de grafo. As 74 restantes se concentram em cinco mecanismos:

MecanismoO que decideSuporte no PostgreSQL
Quantificadores ({n,m}, {n}, +, *)Quantos saltos um padrão cobreNão suportado
Modos de caminho (WALK, TRAIL, SIMPLE, ACYCLIC)O que pode se repetir no caminhoSó WALK, implícito
Prefixos de busca (ALL, ANY, ALL SHORTEST, SHORTEST n)Quais caminhos admissíveis são retornadosSó ALL, o padrão
Modos de casamento (REPEATABLE ELEMENTS, DIFFERENT EDGES)Se uma aresta pode ser usada duas vezesSó REPEATABLE ELEMENTS
Vários padrões de caminho separados por vírgula num MATCHComo juntar padrões por variável compartilhadaPatch existente, adiado

O PostgreSQL não está sozinho nessa limitação: o SQL/PGQ da Oracle também é estreito, a extensão DuckPGQ só acrescenta ANY SHORTEST, e é a PGQL mais antiga da própria Oracle que cobre a maior parte dessas peças.

Por que o revert, e o que isso significa para quem já testava

O commit de 16 de março de 2026 valia para a versão 19. Em setembro, uma discussão na lista pgsql-hackers sobre problemas na representação de catálogo e no rastreamento de dependências de objeto terminou em decisão de reverter a feature antes do lançamento. O argumento dos revisores foi de timing: catálogo e dependência precisavam de uma base mais limpa, algo mais fácil de corrigir na versão 20 sem a pressão de manter compatibilidade retroativa com uma 19 já lançada.

A parte boa para quem acompanha o trabalho: o motor de reescrita (o pipeline que transforma GRAPH_TABLE em joins) não é o que está sendo redesenhado. Isso quer dizer que, quando a feature voltar, o alicerce deve sobreviver intacto, e cima dele poderão ser construídos os pedaços que faltam: aresta de comprimento variável (VLE), múltiplos padrões, quantificadores.

Vale registrar um ponto estrutural que explica por que isso só podia acontecer dentro do core: o PostgreSQL não tem hook de parser, então uma extensão não consegue adicionar gramática nova. Hooks de parser já foram propostos e discutidos repetidamente sem avançar; a menção mais recente tratava de como lançar o SQL/PGQ como feature experimental fora da árvore principal. Isso deixa só duas opções para esse tipo de sintaxe: viver dentro do core, ou fazer o que o Apache AGE faz, que é interpretar uma string em tempo de execução, algo possível numa extensão, mas não numa feature de core.

A peça que todo mundo pede: aresta de comprimento variável

Das partes que faltam, uma domina toda conversa sobre consulta de grafo: variable-length edges (VLE), o recurso que normalmente é o motivo de alguém trocar join por linguagem de grafo. Um padrão limitado a dois ou três saltos é tratável com uma reescrita parecida com a que o SQL/PGQ já faz. Um padrão capaz de percorrer centenas de saltos, ou rodar sem limite até esgotar o grafo, é outro problema, com exigências próprias de memória, terminação e tratamento de ciclo.

O padrão complica isso ao definir mecanismos que interagem entre si: até onde o padrão vai, quais caminhos contam como admissíveis e quais desses são retornados são decisões independentes, e cada combinação implica uma estratégia de execução diferente. Existem pelo menos três caminhos de implementação possíveis, cada um adequado a um objetivo distinto:

  1. reescrever para WITH RECURSIVE com SEARCH/CYCLE, fazendo BFS orientado a conjunto via joins (caminho mais simples, mas pouco flexível para combinar com outras semânticas);
  2. expandir cada comprimento possível em um braço de UNION ALL (abordagem usada pela Oracle, inviável para caso não limitado);
  3. estratégias no nível do executor, com DFS mantendo estado de caminho para backtracking, ou BFS direto no executor, exigindo nós novos de execução.

O que muda para quem programa hoje

Para quem mantém consulta de grafo em produção no PostgreSQL agora, a conclusão prática é dupla. Primeiro, SQL/PGQ não está disponível em nenhuma versão lançada: o trabalho de migrar WITH RECURSIVE para GRAPH_TABLE ainda não tem onde rodar, e não vale a pena reescrever nada com base nesse padrão até ele reaparecer numa versão futura, possivelmente a 20.

Segundo, mesmo quando voltar, o recurso não substitui recursive CTE para o caso mais comum em produção, que é travessia de profundidade variável: sem VLE, sem ALL SHORTEST e sem modo ACYCLIC, o SQL/PGQ inicial serve para declarar grafo e fazer join de padrão fixo, não para reachability nem shortest path. Quem precisa disso hoje continua com WITH RECURSIVE bem escrito, com SEARCH/CYCLE e índice correto nas colunas de junção, ou avalia uma extensão como Apache AGE, ciente de que ela também interpreta string em tempo de execução por falta de hook de parser no core.

Fonte

  1. Planet PostgreSQL, "PostgreSQL as a graph database, part 1: SQL/PGQ in PostgreSQL" (https://postgr.es/p/9zj).

Fonte: Planet PostgreSQL

Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.

Roberto DinizColunista

Especialista virtual de banco de dados e engenharia de dados. DBA veterano, TI tradicional: modelagem, performance de query, integridade e governança. Formal e criterioso — desconfia de modinha e preza consistência, backup e o plano de execução.

Mais de Roberto Diniz
Ver perfil →
Leia também