O PostgreSQL 18 chegou em setembro de 2025 e não é exagero dizer que essa é a release mais significativa dos últimos anos. Depois de duas décadas usando o mesmo protocolo de comunicação, de anos dependendo de I/O síncrono e de gambiarras para gerar UUIDs ordenáveis, essa versão finalmente resolve dores reais que qualquer desenvolvedor backend já sentiu.

Neste post, vamos passar pelas 7 novidades mais impactantes do PostgreSQL 18 com exemplos práticos de SQL que você pode rodar hoje mesmo.

O que o PostgreSQL 18 traz de novo

Antes de mergulhar nos detalhes, vale entender o panorama geral. O PostgreSQL 18 foca em três frentes: performance (I/O assíncrono e skip scan), produtividade do desenvolvedor (UUIDv7, colunas virtuais, RETURNING melhorado) e integridade de dados (constraints temporais).

Além disso, o protocolo de comunicação client-server foi atualizado pela primeira vez desde 2003 — da versão 3.0 para 3.2. Isso abre caminho para otimizações futuras na comunicação entre aplicação e banco.

O tema comum entre todas essas mudanças? O PostgreSQL está eliminando a necessidade de soluções externas para problemas que deveriam ser resolvidos no próprio banco.

I/O assíncrono: o fim do gargalo de disco

Historicamente, o PostgreSQL executava operações de I/O de forma síncrona. Cada leitura de disco bloqueava o processo até receber os dados de volta. Em workloads com muita leitura sequencial, isso criava um gargalo real — o banco ficava esperando o disco quando poderia estar processando outras requisições.

O PostgreSQL 18 introduz um subsistema de I/O assíncrono (AIO) que permite disparar múltiplas requisições de leitura em paralelo, sem esperar cada uma terminar antes de iniciar a próxima.

Para verificar o modo de I/O ativo na sua instância:

SHOW io_method;

Os valores possíveis são sync (legado), worker (processos dedicados para I/O) e io_uring (interface nativa do Linux para I/O assíncrono, a mais rápida).

Você também pode verificar quantos workers estão ativos:

SHOW io_workers;

Os ganhos reportados pela comunidade são expressivos: 2 a 3x de melhoria em sequential scans para workloads de leitura intensiva. O impacto é ainda mais perceptível em ambientes cloud, onde a latência de disco é naturalmente mais alta que em SSDs locais.

Um ponto importante: o AIO beneficia principalmente scans sequenciais e bitmap heap scans. Index scans pontuais, que já são rápidos por natureza, não apresentam ganho significativo.

Index skip scan: queries rápidas sem índices extras

Quantas vezes você criou um índice extra só porque sua query não usava a primeira coluna de um índice composto? O skip scan resolve exatamente esse problema.

Antes do PostgreSQL 18, se você tinha um índice em (region, category, date) e fazia uma query filtrando apenas por category e date, o banco ignorava o índice completamente e fazia um sequential scan. A solução era criar outro índice em (category, date).

Agora, o B-tree skip scan permite que o PostgreSQL "pule" entre os valores da primeira coluna e leia apenas as porções relevantes do índice:

-- Índice existente: (region, category, date)
-- Antes do PG 18: sequential scan (ignorava o índice)
-- PG 18: skip scan (usa o índice pulando entre regions)
SELECT *
FROM sales
WHERE category = 'Electronics'
  AND date > '2025-01-01';

O skip scan funciona melhor quando a coluna de prefixo tem baixa cardinalidade — ou seja, poucos valores distintos. Se a coluna region tem 5 valores possíveis, o banco faz 5 buscas pontuais no índice. Se tem 5 milhões, a vantagem desaparece.

Para verificar se o otimizador está usando skip scan, basta analisar o plano de execução:

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM sales
WHERE category = 'Electronics'
  AND date > '2025-01-01';

O node no resultado continua sendo Index Scan ou Index Only Scan — não existe um tipo separado "Skip Scan". O indicador é o campo Index Searches: N que aparece no output do EXPLAIN ANALYZE. Se N for maior que 1, significa que o PostgreSQL fez múltiplas buscas no índice, pulando entre valores distintos da coluna de prefixo — ou seja, skip scan ativo.

UUIDv7 nativo: identificadores com ordem temporal

O debate entre auto-increment e UUID como chave primária é antigo. Auto-increment é eficiente para o B-tree mas vaza informação sobre o volume de dados. UUID v4 é aleatório e seguro, mas fragmenta o índice horrivelmente porque os valores não têm nenhuma ordem.

O UUIDv7 resolve esse impasse. Ele embute um timestamp de milissegundos nos primeiros 48 bits, o que significa que UUIDs gerados em sequência são automaticamente ordenáveis por tempo. O B-tree adora isso.

No PostgreSQL 18, a geração é nativa:

SELECT uuidv7();
-- 01980de8-ad3d-715c-b739-faf2bb1a7aad

Você também pode extrair o timestamp de um UUIDv7 existente:

SELECT uuid_extract_timestamp(
  '01980de8-ad3d-715c-b739-faf2bb1a7aad'
);
-- 2025-09-24 12:30:15.123+00

E até gerar UUIDs com offset temporal, útil para testes e dados históricos:

SELECT uuidv7(INTERVAL '-7 days');
-- Gera um UUIDv7 com timestamp de 7 dias atrás

Na prática, se você está começando um projeto novo com PostgreSQL 18, use uuidv7() como default da chave primária:

CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT uuidv7(),
  customer_id UUID NOT NULL,
  total DECIMAL(10,2) NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

Os benefícios são imediatos: menos fragmentação de índice comparado ao UUID v4, ordenação natural por tempo de criação e zero dependência de bibliotecas externas.

Colunas virtuais geradas: dados calculados sem armazenamento

Colunas geradas existem desde o PostgreSQL 12, mas até a versão 17 só existia o tipo STORED — o valor era calculado e gravado fisicamente no disco a cada INSERT ou UPDATE. Isso consumia espaço e adicionava overhead de escrita.

O PostgreSQL 18 introduz colunas VIRTUAL (e as torna o padrão). Elas são calculadas no momento da leitura, sem ocupar espaço em disco:

CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  price DECIMAL(10,2) NOT NULL,
  tax_rate DECIMAL(5,4) DEFAULT 0.0875,
  total DECIMAL(10,2) GENERATED ALWAYS AS (price * (1 + tax_rate))
);

Como VIRTUAL agora é o default, não precisa especificar. Mas se quiser ser explícito:

-- Explicitamente virtual
full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) VIRTUAL

-- Explicitamente armazenada (comportamento antigo)
full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED

Quando usar cada tipo? VIRTUAL é ideal quando o cálculo é barato e a coluna é lida com pouca frequência — evita desperdício de espaço. STORED compensa quando o cálculo é caro ou a coluna é lida constantemente, pois o custo é pago na escrita.

Um detalhe importante: colunas VIRTUAL não podem ser indexadas diretamente. Se você precisa de um índice na coluna gerada, use STORED.

RETURNING com OLD e NEW: DML mais expressivo

A cláusula RETURNING já existia no PostgreSQL, mas retornava apenas os valores finais da operação. Se você precisava saber o valor anterior de uma coluna antes do UPDATE, tinha que fazer um SELECT antes ou usar um trigger.

O PostgreSQL 18 adiciona os qualificadores OLD e NEW, inspirados na sintaxe de triggers:

UPDATE users
SET email = 'novo@exemplo.com'
WHERE id = 42
RETURNING OLD.email AS email_anterior,
          NEW.email AS email_atual;

Resultado:

 email_anterior      | email_atual
---------------------+--------------------
 antigo@exemplo.com  | novo@exemplo.com

Isso é especialmente útil para audit logs sem precisar de triggers:

WITH changes AS (
  UPDATE products
  SET price = price * 1.10
  WHERE category = 'premium'
  RETURNING id,
            OLD.price AS preco_antigo,
            NEW.price AS preco_novo
)
INSERT INTO price_history (product_id, old_price, new_price, changed_at)
SELECT id, preco_antigo, preco_novo, now()
FROM changes;

O OLD e NEW também funcionam com DELETE (onde NEW é sempre NULL) e com INSERT (onde OLD é sempre NULL). Em comandos MERGE, ambos estão disponíveis dependendo da ação executada.

Constraints temporais e o novo protocolo 3.2

Duas novidades que merecem destaque juntas por serem mais especializadas, mas igualmente impactantes.

WITHOUT OVERLAPS: constraints sobre períodos

Se você já implementou um sistema de reservas, sabe a dor de validar que dois períodos não se sobrepõem. Antes, isso exigia triggers ou lógica na aplicação. O PostgreSQL 18 traz a cláusula WITHOUT OVERLAPS para constraints de chave primária e unique:

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE room_bookings (
  room_id INT NOT NULL,
  during TSTZRANGE NOT NULL,
  guest_name TEXT NOT NULL,
  PRIMARY KEY (room_id, during WITHOUT OVERLAPS)
);

Agora, tentar inserir uma reserva que conflita com outra gera um erro de constraint:

-- Reserva existente: sala 101, 1 a 5 de março
INSERT INTO room_bookings VALUES
  (101, '[2026-03-01, 2026-03-05)', 'Alice');

-- Tentativa de reserva conflitante: erro!
INSERT INTO room_bookings VALUES
  (101, '[2026-03-03, 2026-03-07)', 'Bob');
-- ERROR: conflicting key value violates exclusion constraint

O banco garante a integridade no nível de constraint, sem código extra na aplicação.

Protocolo 3.2: a primeira atualização em 22 anos

O protocolo wire do PostgreSQL não era atualizado desde a versão 7.4, lançada em 2003. O PostgreSQL 18 introduz a versão 3.2, mantendo compatibilidade com clientes existentes (que continuam negociando 3.0).

A versão 3.2 em si não traz funcionalidades visíveis para o desenvolvedor, mas abre a porta para melhorias futuras como negociação de features entre client e server, otimizações no formato de mensagens e suporte a novos mecanismos de autenticação — como o OAuth, que também estreia no PostgreSQL 18.

Conclusão

O PostgreSQL 18 não é uma release incremental. Cada novidade resolve um problema concreto que desenvolvedores enfrentam diariamente: I/O assíncrono elimina o gargalo de disco em leituras pesadas, skip scan reduz a necessidade de índices duplicados, UUIDv7 entrega identificadores únicos e ordenáveis sem extensões, colunas virtuais evitam armazenamento desnecessário e o RETURNING com OLD/NEW simplifica audit logs.

Se você ainda está no PostgreSQL 16 ou 17, a migração vale o esforço. Comece testando em um branch de desenvolvimento — se você usa Neon, basta criar um branch do banco de produção, rodar seus testes e validar a compatibilidade. Os ganhos de performance com o AIO sozinho já justificam o upgrade.

A cada release, o PostgreSQL confirma porque segue sendo a escolha padrão para quem leva banco de dados a sério.