Dev & EngARTIGO

Extensão pgColumnar chega à versão 1.0-alpha4 com clustering Hilbert e filtro de join no Postgres

A quarta alpha do table access method colunar para PostgreSQL adiciona ordenação por curva de Hilbert e um filtro de runtime para joins em esquema estrela, além de corrigir um bug que podia devolver bytes errados em chunks grandes.

O PostgreSQLPostgreSQL11 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 não tem armazenamento colunar nativo, mas desde a versão 12 tem uma API estável para table access methods customizados, o mesmo mecanismo que permite trocar heap por qualquer outra forma de guardar linhas em disco. É nessa API que Joshua D. Drake constrói o pgColumnar: uma extensão que registra um TAM colunar dentro do Postgres, sem fork do banco e sem camada de middleware na frente das queries. Ele publicou no Planet PostgreSQL as notas da versão 1.0-alpha4, lançada em 17 de setembro de 2026, e o tema desta rodada é bem específico: layout físico dos dados e capacidade do otimizador de pular trabalho que não precisa fazer.

Para quem lida com analytics em cima de um Postgres que já está em produção, isso é relevante por um motivo direto: normalmente a saída para OLAP nesse cenário é replicar dados para um Citus, um ClickHouse, um DuckDB via FDW, ou aceitar o custo de scans sequenciais em tabelas heap gigantes. O pgColumnar propõe resolver isso sem sair do banco relacional, com CREATE TABLE ... USING pgcolumnar. O formato em disco, o PGCN v1, permanece inalterado nesta release, o que significa que tabelas existentes continuam sendo lidas e escritas exatamente como antes: quem já está em produção com a extensão não precisa converter nada.

Hilbert versus Z-order: o que muda no plano de execução

A novidade mais estrutural é o clustering por curva de Hilbert. Até aqui o pgColumnar organizava dados em ordem Z (Morton order), técnica comum em índices multidimensionais por ser barata de calcular. O problema do Z-order é que ele tem saltos em fronteiras de bit: duas chaves numericamente próximas podem cair em posições distantes na ordenação física, o que obriga o executor a varrer mais chunk groups do que deveria num filtro de intervalo.

A curva de Hilbert não tem esses saltos, e a extensão expõe isso via duas funções novas: pgcolumnar.cluster_hilbert(table, VARIADIC columns), que reescreve a tabela sob AccessExclusiveLock (o mesmo custo de um CLUSTER ou VACUUM FULL), e pgcolumnar.recluster_hilbert(table, VARIADIC columns), que faz o mesmo trabalho de forma online sob ShareUpdateExclusiveLock, mantendo leituras e escritas correndo durante a reorganização. Drake reporta que, num teste com 200 mil linhas em duas colunas, a ordenação por Hilbert leu de 1,24x a 2,04x menos chunk groups do que Z-order para o mesmo filtro de range.

A recomendação prática que vem junto com o número é sóbria, e é a que qualquer DBA experiente daria: Z-order continua bom para busca por ponto, Hilbert ganha em filtros de range sobre várias colunas, e a vantagem diminui conforme a caixa de busca do filtro cresce. Ou seja, não é migração automática. Antes de rodar recluster_hilbert em produção vale medir o próprio corpus de dados, e a extensão ajuda com isso: pgcolumnar.sort_status informa quanto de uma tabela está de fato em ordem física.

Vale notar um detalhe de design da API: por que dois verbos novos em vez de um parâmetro na função cluster existente? Porque o PostgreSQL não permite estender a assinatura cluster(regclass, VARIADIC name[]) com um parâmetro opcional antes de um VARIADIC, em nenhuma das duas direções. É uma limitação da linguagem PL, não uma escolha arbitrária do projeto.

Join de esquema estrela ganha filtro automático

A segunda mudança de peso mira o padrão mais comum em analytics: hash join entre uma tabela fato colunar grande e dimensões menores. A partir desta versão, um Hash Join interno serial constrói, a partir das próprias chaves de join que já hasheou no lado de build, um filtro que faz duas coisas ao mesmo tempo: usa um intervalo de chave para pular grupos inteiros de chunks da tabela fato, e usa um Bloom filter para rejeitar linhas que certamente não vão casar.

As notas de release trazem três cenários medidos que explicam por que o parâmetro pgcolumnar.enable_join_runtime_filter vem ligado por padrão para todo mundo:

  • Fato clusterizada pela chave de join: 19 de 20 chunk groups eliminados, apenas 1 lido.
  • Fato espalhada (sem clustering pela chave): 0 grupos eliminados, mas o Bloom filter ainda rejeita mais de 15.000 de 19.800 não-correspondências.
  • Lado de build grande demais: o Bloom filter se desativa sozinho.

Esse terceiro caso é o argumento de segurança: um filtro que não ajuda em nada se desliga sozinho, em vez de virar overhead. Mas a eliminação de grupos inteiros só acontece se a tabela fato estiver clusterizada pela chave de join, o que reconecta diretamente com a decisão de usar cluster_hilbert ou recluster na coluna certa. Vale registrar as limitações: o filtro só se aplica a Hash Join interno serial sobre scan colunar direto, e não cobre LEFT, SEMI, ANTI, CROSS, scans paralelos ou scans com projeção. Um agregado vetorizado sem agrupamento também continua rodando por cima de um Hash Join interno com chave única no lado de dimensão, já que uma dimensão com chave única funciona como filtro puro da fato; dimensões com chave duplicada e joins LEFT ficam no plano tradicional.

Correções que interessam a quem confia dados críticos à extensão

Além das duas features grandes, a lista de correções de bug desta alpha merece atenção de quem avalia se coloca pgColumnar perto de dados que importam. A mais séria: um chunk de coluna cujo comprimento registrado excedia 4 GB estava sendo convertido para 32 bits no caminho de fetch por índice. Na prática, isso podia fazer o fetch ler os bytes errados e reportá-los como dado válido, silenciosamente. Agora ambos os pontos de cast levantam o erro XX001 em vez de devolver lixo. É o tipo de bug que só aparece em tabelas grandes o suficiente para ter chunks nessa faixa, mas que justifica não tratar uma extensão em fase alpha como pronta para dado que não pode estar errado.

Outras correções da mesma rodada: a cópia do bitmap de validade num fetch coalescido agora respeita o limite do chunk antes de rodar, fechando uma leitura potencial além do buffer. Projeções agora sobrevivem a DDL: um rewrite regrava suas projeções, RENAME COLUMN propaga o novo nome, DROP COLUMN é recusado quando uma projeção depende da coluna, e TRUNCATE in-place limpa o storage de cada projeção junto com a base. O codec de bloco também parou de vazar buffer nos dois caminhos que o abandonavam sem liberar.

Planejador e paralelismo: ajustes que economizam I/O real

Três correções menores, mas com efeito prático direto em custo de execução: leituras de colunas adjacentes num mesmo grupo de linhas, no caminho de fetch por índice, agora são coalescidas numa única leitura de I/O em vez de uma por coluna. A construção paralela de índice passou a distribuir de fato o trabalho entre os workers lançados, algo que antes não acontecia: um backendBack-end49 conteúdosIntegração front-end com backend: 7 decisões que evitam caos entre APIs, BFF e GraphQLDev (Back & Front) · abr 2026Como criar uma FAKE API REST para testes — JSONPlaceholderDev (Back & Front) · set 2025Construindo um aplicativo de bate-papo de IA simples com Spring AI e AngularDev (Back & Front) · jul 2025Ver tudo em Dev (Back & Front) lia a tabela inteira sozinho enquanto os workers paralelos ficavam ociosos, um desperdício de recursos alocados que qualquer DBA reconheceria olhando o plano. E o custo de scan paralelo no otimizador parou de dividir o I/O pelo número de workers, já que o PostgreSQL divide CPU entre workers mas deixa o trabalho de disco inteiro, agora o custo colunar reflete isso corretamente. Como consequência, o planejador deixou de preferir um index scan com fetch que fazia muito mais trabalho: um range correlacionado de 50 mil linhas passou a escolher o scan colunar puro em vez do index scan, porque a penalidade de fetch agora cobra um termo por linha, limitado a metade de um chunk group.

O que fica em aberto

Dois problemas conhecidos seguem documentados como tal, sem correção nesta versão: ajustar o codec de compressão pode aumentar o tamanho de uma tabela em texto de alta entropia (o writer só mantém FSST quando ele supera a alternativa por uma margem configurável, e nunca compara armazenar os códigos FSST sem compressão adicional); em teste com 200 mil linhas de texto hex aleatório, zstd escreveu 1,777% a mais do que sem compressão nenhuma. E o codec de bloco comprime uma região que depois descarta, custando cerca de 25% mais CPU de escrita em dados incompressíveis, sem afetar o que é lido ou armazenado.

Upgrade e o que considerar antes de adotar

A atualização exige rodar ALTER EXTENSION pgcolumnar UPDATE; em cada banco que usa a extensão. É o menor upgrade da série até agora: cria apenas as duas funções de clustering Hilbert, sem converter dado existente, sem substituir função nenhuma e sem alterar SQL já escrito. Vale notar uma curiosidade de versionamento: no PGXN esta release aparece como 1.0.0-alpha.4 (versionamento semântico exige três componentes inteiros), enquanto CREATE EXTENSION reporta 1.0-alpha4, o formato de dois componentes que a própria extensão usa internamente. São o mesmo release com dois rótulos.

Para quem está avaliando pgColumnar como caminho de OLAP dentro de um stack Postgres já existente, o ponto que merece cautela é o de sempre com software em fase alpha: as interfaces ainda podem mudar antes do 1.0, e o clustering Hilbert ordena grupos de linhas inteiros no rewrite, o que significa que uma tabela sob escrita contínua degrada essa ordem até a próxima reorganização. Isso não invalida o projeto, mas define onde ele serve hoje: cargas analíticas com janelas de manutenção previsíveis, não sistemas que exigem ordenação perfeita o tempo todo sem intervenção. Antes de apostar em produção, o caminho sensato continua sendo o de sempre, olhar o EXPLAIN real da carga própria, comparar com enable_join_runtime_filter ligado e desligado, e decidir pela curva de clustering com base no sort_status da tabela, não pela expectativa gerada pelos números do changelog.

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.

Ver perfil