---
title: "PostgreSQL 18 na prática: I/O assíncrono, UUIDv7 e o que testar antes do upgrade"
description: "Como medir o I/O assíncrono com io_method e io_uring, quando usar uuidv7() como chave primária, e as armadilhas de upgrade: checksums por padrão, estatísticas preservadas e fim do MD5."
date: "2026-09-29"
tags: ["postgresql", "banco-de-dados", "io-uring", "uuid", "linux", "performance", "upgrade"]
url: "https://abjr.dev/noticias/postgresql-18-io-assincrono-uuidv7"
---

O PostgreSQL 18, lançado em setembro de 2025, trouxe a mudança interna mais profunda em anos: o banco deixou de depender só de leituras síncronas e do *readahead* do sistema operacional. Veio junto uma lista de mudanças menores que afetam o dia a dia: `uuidv7()` nativo, skip scan em índices B-tree, `OLD` e `NEW` no `RETURNING`, e alguns padrões novos que podem travar um `pg_upgrade` se você não souber deles.

Se você ainda está no 16 ou no 17, este texto é o que eu testaria antes de subir de versão, com os comandos para medir em vez de acreditar em número de anúncio.

## I/O assíncrono: o que mudou

Até a versão 17, cada processo do PostgreSQL pedia um bloco de 8 KB ao kernel e esperava a resposta. Para *sequential scans*, o banco contava com o readahead do Linux para adiantar leituras. Em disco local rápido, funcionava. Em disco de rede, como os volumes das nuvens, cada leitura carrega a latência da rede, e esperar uma por vez desperdiça o hardware.

A versão 18 introduz um subsistema de **AIO**, controlado pelo parâmetro `io_method`:

| `io_method` | Como funciona | Plataforma |
| --- | --- | --- |
| `worker` (padrão) | Processos auxiliares (`io_workers`, 3 por padrão) fazem as leituras em paralelo | Todas |
| `io_uring` | Usa a interface assíncrona do kernel Linux, sem processos intermediários | Linux, com build que inclui liburing |
| `sync` | Comportamento antigo, síncrono | Todas |

Nesta primeira versão, o AIO cobre **leituras**: sequential scans, bitmap heap scans e vacuum. Escrita continua pelo caminho tradicional. O parâmetro `effective_io_concurrency`, que controla quantas leituras podem ficar em voo, também teve o padrão aumentado.

O anúncio oficial fala em ganhos de até 3× em leitura. O "até" importa: o ganho aparece em armazenamento com latência alta e em consultas que leem muito do disco. Com o conjunto de dados inteiro no `shared_buffers` ou no cache do sistema operacional, quase não há diferença, porque não há I/O para paralelizar.

## Como medir na sua carga

Não confie no número de ninguém, inclusive o meu. Monte o teste com a sua carga.

Confira o que está ativo:

```sql
SHOW io_method;
SHOW io_workers;
SHOW effective_io_concurrency;
```

Crie uma tabela maior que a memória disponível para o banco, para forçar leitura de disco:

```sql
CREATE TABLE leitura_teste AS
SELECT g AS id, md5(g::text) AS payload, now() - (g || ' seconds')::interval AS criado_em
FROM generate_series(1, 50000000) AS g;

VACUUM ANALYZE leitura_teste;
```

Para cada valor de `io_method`, reinicie o banco (o parâmetro exige restart), limpe o cache do sistema operacional e rode a mesma consulta:

```bash
sudo systemctl restart postgresql
sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
```

```sql
EXPLAIN (ANALYZE) SELECT count(*) FROM leitura_teste WHERE payload LIKE 'ab%';
```

No 18, `EXPLAIN ANALYZE` já inclui `BUFFERS` por padrão. Compare o tempo total e a linha `shared read`: é ela que mostra quanto veio do disco. Rode cada configuração algumas vezes e descarte a primeira.

Enquanto a consulta roda, a view `pg_aios` mostra as operações de I/O assíncrono em andamento. É útil para confirmar que o AIO está de fato sendo usado.

### Antes de ligar `io_uring`

- O binário precisa ter sido compilado com suporte a liburing. Pacotes das distribuições e do repositório PGDG costumam ter; builds customizadas, nem sempre. Se não tiver, o banco recusa o valor.
- Em container, o perfil seccomp padrão do Docker bloqueia as syscalls de io_uring. O banco sobe, mas o `io_uring` não funciona. Esse é um dos poucos casos em que afrouxar o seccomp é o caminho, e vale fazer só com um perfil customizado que libera exatamente essas syscalls.
- Algumas distribuições e ambientes gerenciados desligam io_uring no kernel por política de segurança. Nesses casos, `worker` é a escolha.

Na minha leitura, `worker` já entrega a maior parte do ganho, funciona em qualquer lugar e é o padrão por um motivo. `io_uring` é o ajuste fino para quem controla o host.

## UUIDv7: chave primária que não espalha escrita

A função `uuidv7()` gera identificadores no formato padronizado pelo RFC 9562. Os primeiros 48 bits são um timestamp em milissegundos, o resto é aleatório.

```sql
CREATE TABLE pedido (
    id uuid PRIMARY KEY DEFAULT uuidv7(),
    cliente_id bigint NOT NULL,
    total numeric(12, 2) NOT NULL
);

SELECT uuid_extract_timestamp(id) FROM pedido LIMIT 1;
```

Por que isso importa: com UUIDv4, totalmente aleatório, cada `INSERT` cai numa página aleatória do índice B-tree da chave primária. Em tabela grande, isso significa ler páginas do disco para inserir, sujar muitas páginas diferentes no checkpoint e gerar mais WAL. Com UUIDv7, IDs novos são sempre maiores que os antigos, e as inserções se concentram no fim do índice, como acontece com um `bigserial`.

Você fica com as vantagens do UUID, que são gerar o ID fora do banco, não expor contagem e mesclar dados de várias origens, sem o custo de escrita do v4.

**O custo que ninguém menciona:** o UUIDv7 carrega o momento da criação. Qualquer pessoa que veja o ID sabe quando o registro foi criado, com precisão de milissegundo. Para ID de pedido interno, tanto faz. Para ID exposto em URL pública de algo em que o momento de criação é sensível, como cadastro de usuário ou documento, pode ser um vazamento. Nesses casos, UUIDv4 continua sendo a escolha, ou um ID público separado da chave primária.

## Outras mudanças que valem o upgrade

**Skip scan em B-tree.** Um índice em `(status, criado_em)` agora pode servir uma consulta que filtra só `criado_em`, se `status` tiver poucos valores distintos. O planejador "pula" entre os valores de `status`. Antes, esse caso exigia um segundo índice só em `criado_em`.

**`OLD` e `NEW` no `RETURNING`.** Dá para ver o antes e o depois no mesmo comando, sem trigger nem consulta extra:

```sql
UPDATE produto
SET preco = preco * 1.1
WHERE categoria = 'cabos'
RETURNING id, old.preco AS preco_anterior, new.preco AS preco_novo;
```

Isso simplifica auditoria. Quem já precisou de trigger só para registrar "valor antigo e novo", como no texto sobre [triggers no PostgreSQL disparando APIs](/noticias/trigger-postgresql-disparar-api), vai reconhecer o alívio.

**Colunas geradas virtuais.** `GENERATED ALWAYS AS (...)` agora é virtual por padrão: calculado na leitura, sem ocupar disco. A limitação na versão 18 é que colunas virtuais não podem ser indexadas. Se você precisa de índice, declare `STORED` explicitamente.

**Restrições temporais.** Chaves primárias e únicas aceitam `WITHOUT OVERLAPS` em um intervalo, o que impede, por exemplo, duas reservas da mesma sala em períodos sobrepostos, direto no esquema. Exige a extensão `btree_gist`.

**OAuth 2.0.** O banco pode autenticar usando tokens de um provedor de identidade, via um módulo validador. É o começo de tirar senha de banco das mãos das pessoas.

## Armadilhas de upgrade

**Checksums de dados agora são padrão.** O `initdb` do 18 ativa checksums por padrão. O `pg_upgrade` exige que o cluster antigo e o novo tenham a mesma configuração. Se o seu cluster antigo não tem checksums, o que é o caso da maioria criada com os padrões antigos, o upgrade falha. Saída: criar o cluster novo com `initdb --no-data-checksums`, ou ativar checksums no antigo antes com `pg_checksums` (exige o banco parado, e leva tempo proporcional ao tamanho).

**Estatísticas preservadas.** Até o 17, depois de um `pg_upgrade` o banco ficava sem estatísticas do planejador até alguém rodar `ANALYZE` em tudo. Durante esse tempo, planos ruins. O 18 preserva as estatísticas básicas. As estendidas (`CREATE STATISTICS`) ainda precisam ser recriadas, e o `vacuumdb` ganhou uma opção para processar só o que falta:

```bash
vacuumdb --all --analyze-in-stages --missing-stats-only
```

**MD5 descontinuado.** Senhas MD5 ainda funcionam, mas geram aviso e vão sair numa versão futura. Migre para SCRAM:

```sql
SET password_encryption = 'scram-sha-256';
\password nome_do_usuario
```

E troque `md5` por `scram-sha-256` no `pg_hba.conf`. Confira antes se todos os drivers das aplicações suportam SCRAM; drivers muito antigos não suportam.

**`pg_upgrade --swap`.** Um modo novo que move os diretórios de dados em vez de copiar ou criar links. É o mais rápido, mas o cluster antigo deixa de ser utilizável. Tenha backup testado antes.

## Checklist de upgrade

- [ ] Conferir checksums do cluster atual (`SHOW data_checksums;`) e decidir entre `--no-data-checksums` ou `pg_checksums`.
- [ ] Rodar `pg_upgrade --check` numa cópia antes do dia.
- [ ] Planejar `vacuumdb --analyze-in-stages --missing-stats-only` logo após o upgrade.
- [ ] Migrar senhas MD5 para SCRAM e testar todos os clientes.
- [ ] Medir `io_method = worker` contra `sync` com a carga real; testar `io_uring` só se o host permitir.
- [ ] Revisar extensões: todas precisam ter versão compatível com o 18 instalada no servidor novo.
- [ ] Para tabelas novas com UUID, preferir `uuidv7()`, exceto quando o timestamp embutido for um problema.

## Em resumo

O PostgreSQL 18 não muda o SQL que a sua aplicação escreve, mas muda como o banco conversa com o disco. Para quem roda em nuvem, medir `io_method` com a carga real é o ajuste de maior retorno. Para modelagem nova, `uuidv7()` resolve o velho dilema entre UUID e chave sequencial, com uma ressalva de privacidade. E, no upgrade, o novo padrão de checksums é o detalhe que mais derruba planos que funcionavam nas versões anteriores.

## Fontes

- [PostgreSQL 18 Released!](https://www.postgresql.org/about/news/postgresql-18-released-3142/)
- [PostgreSQL 18 — Release Notes](https://www.postgresql.org/docs/18/release-18.html)
- [PostgreSQL 18 — Resource Consumption (io_method, io_workers)](https://www.postgresql.org/docs/18/runtime-config-resource.html)
- [PostgreSQL 18 — UUID Functions](https://www.postgresql.org/docs/18/functions-uuid.html)
- [PostgreSQL 18 — pg_upgrade](https://www.postgresql.org/docs/18/pgupgrade.html)
- [PostgreSQL 18 — pg_checksums](https://www.postgresql.org/docs/18/app-pgchecksums.html)
- [PostgreSQL 18 — OAuth Authorization](https://www.postgresql.org/docs/18/auth-oauth.html)
- [RFC 9562 — Universally Unique IDentifiers (inclui UUIDv7)](https://www.rfc-editor.org/rfc/rfc9562)
