REPACK CONCURRENTLY no PostgreSQL 19: o custo real de rodar em produção
Um benchmark detalhado do blog boringSQL mede o que o novo comando REPACK (CONCURRENTLY) do PostgreSQL 19 consome de WAL, memória e tempo de bloqueio enquanto roda, e compara o resultado com pg_repack e pg_squeeze.

Um benchmark detalhado do blog boringSQL mede o que o novo comando REPACK (CONCURRENTLY) do 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 → 19 consome de WAL, memória e tempo de bloqueio enquanto roda, e compara o resultado com pg_repack e pg_squeeze.
Um benchmark detalhado do blog boringSQL mede o que o novo comando REPACK (CONCURRENTLY) do PostgreSQL 19 consome de WAL, memória e tempo de bloqueio enquanto roda, e compara o resultado com pg_repack e pg_squeeze.
Um benchmark detalhado do blog boringSQL mede o que o novo comando REPACK (CONCURRENTLY) do PostgreSQL 19 consome de WAL, memória e tempo de bloqueio enquanto roda, e compara o resultado com pg_repack e pg_squeeze.
Nota da redação: duas afirmações deste texto não puderam ser confirmadas na fonte original (blog boringSQL, republicado no Planet PostgreSQL). A frase que descreve REPACK (CONCURRENTLY) como 'a primeira opção desse tipo disponível em serviços gerenciados' é uma extrapolação nossa: a fonte diz apenas que a ferramenta funciona em serviços gerenciados e deve levar o repacking online a um público mais amplo, sem afirmar ineditismo. Além disso, mais adiante o texto diz que cancelar a sessão do pg_squeeze 'limpa tudo, sem deixar resíduo'; isso está incorreto segundo a fonte, que afirma que cancelar a sessão que chamou squeeze_table() não interrompe nada, o worker continua rodando em segundo plano até terminar sozinho. Mantemos o restante do texto como apurado, mas pedimos que o leitor considere essas duas ressalvas antes de tomar decisões operacionais com base nelas.
O PostgreSQL 19, ainda em beta, traz um comando que muda a rotina de quem convive com tabelas bloatadas: REPACK, que junta num só comando o que hoje é feito por VACUUM FULL e CLUSTER, e que o benchmark compara com as extensões pg_repack e pg_squeeze. A variante REPACK (CONCURRENTLY) faz a reescrita da tabela sem bloquear leitura e escrita durante o processo, e não depende de extensão nem de shared_preload_libraries, o que a torna a primeira opção desse tipo disponível em serviços gerenciados.
O blog boringSQL publicou um benchmark extenso medindo o que esse comando custa de fato enquanto roda, replicado no Planet PostgreSQL. O material compara REPACK (CONCURRENTLY) com pg_repack e pg_squeeze em cenários de tabela parada e sob carga de escrita real.
Como funciona por baixo dos panos
Refazer uma tabela é trivial quando ninguém a usa: VACUUM FULL toma um lock ACCESS EXCLUSIVE, copia as linhas vivas para um arquivo novo, reconstrói os índices e troca os arquivos. A versão concorrente precisa resolver um problema a mais, porque a tabela continua mudando enquanto a cópia acontece. Para isso ela usa o mesmo mecanismo da replicação lógica: cria um slot de replicação temporário, que fornece um snapshot consistente (o ponto de partida da cópia) e garante que toda mudança feita depois fique registrada no WAL.
Um worker em segundo plano lê esse WAL e coleta as alterações enquanto o processo principal copia as linhas visíveis no snapshot para um arquivo novo e reconstrói os índices. Quando a cópia termina, o comando reaplica as mudanças acumuladas. Em bancos com escrita constante, essa reaplicação nunca alcança de vez o presente, então, no fim, REPACK precisa tomar ACCESS EXCLUSIVE, aplicar o último lote, trocar os arquivos e commitar. Tudo, do snapshot à troca, é uma única transação.
O pg_squeeze funciona de forma quase idêntica, porque REPACK foi derivado dele: mesmo slot, mesmo snapshot, mesma transação única. A diferença é que, por viver fora do core, ele exige shared_preload_libraries e reinício do banco, e seu slot retém WAL até conseguir decodificá-lo. Já o pg_repack é anterior à decodificação lógica e registra mudanças com um trigger: todo insert, update e delete grava também uma linha numa tabela de log, que é aplicada em lotes depois da cópia.
Os números do benchmark
Os testes rodaram em PostgreSQL 19beta4, numa tabela de 90 milhões de linhas com três índices, da qual um terço foi apagado e vacuumado, resultando em 60 milhões de linhas vivas em 27,8 GB. Com a tabela parada, os resultados foram próximos entre VACUUM FULL (referência bloqueante, 109 s) e REPACK (CONCURRENTLY) (107 s, 18,8 GB de WAL). O pg_repack levou 162 s e gerou 33,5 GB de WAL, quase o dobro; o pg_squeeze levou 165 s.
Com 3.000 updates por segundo batendo na tabela durante todo o processo, REPACK (CONCURRENTLY) terminou em 130 s contra 198 s do pg_repack e 181 s do pg_squeeze. O WAL do pg_repack chegou a 43,4 GB, 1,8 vez mais do que os outros, porque sua cópia é um INSERT ... SELECT logado linha a linha, enquanto os demais logam o arquivo novo página a página. Em resumo: para quem dimensiona disco de arquivamento, backup e réplicas, o pg_repack custa quase o dobro de WAL pelo mesmo trabalho.
O preço que o resto do cluster paga
A vantagem de não bloquear a tabela tem uma contrapartida que atinge bancos inteiros, não só a tabela em repack. Slots de replicação pertencem ao servidor, não a um banco específico, então o snapshot que o REPACK e o pg_squeeze seguram represa a remoção de linhas mortas em qualquer tabela de qualquer banco do cluster, não apenas na que está sendo reorganizada. O pg_repack, por usar snapshot comum em vez de slot, só represa tabelas do banco em que roda.
Para demonstrar isso, o autor fez um REPACK durar alguns minutos reconstruindo um índice de expressão lento, enquanto outra tabela recebia 1.500 updates por segundo e um VACUUM (VERBOSE) rodava nela a cada cinco segundos. Depois de dois minutos:
| Ferramenta | Linhas mortas represadas na outra tabela | O que segurou | Afeta outros bancos? |
|---|---|---|---|
| REPACK (CONCURRENTLY) | 186.000 | seu slot de replicação temporário e o backend↳Back-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) → do REPACK | Sim |
| pg_repack | 179.000 | a instrução em execução (a cópia, depois cada índice) | Não |
| pg_squeeze | 209.000 | slot de replicação, worker e a sessão que chamou | Sim |
| nada rodando | 3 a 17 | referência | - |
Não há como desligar esse comportamento. O que dá para fazer é manter o repack curto, acompanhar n_dead_tup das tabelas mais movimentadas enquanto ele roda e evitar agendar um repack longo junto de um batch job pesado em outra tabela.
O lock do fim, e o que acontece quando ele demora
Em tabela quieta, conseguir o ACCESS EXCLUSIVE para a troca final leva milissegundos. Mas o comando precisa esperar qualquer transação que segure lock na tabela, como um ETL, um SELECT longo de relatório ou uma sessão ociosa em transação. O autor segurou uma leitura de 20 segundos numa tabela de 200 mil linhas enquanto quatro clientes faziam buscas a 120 mil por segundo: REPACK e pg_squeeze derrubaram o throughput para zero durante 18 segundos, só recuperando quando o relatório terminou. O pg_repack continuou servindo de 4 a 37 requisições por segundo, com cerca de um segundo de latência.
Com lock_timeout = 3s, o REPACK desiste em três segundos com ERROR: canceling statement due to lock timeout, e todo o trabalho já feito (cópia, índices, catch-up) é descartado. Numa tabela de 28 GB isso representa dois minutos de trabalho perdido; numa de 100 GB na nuvem, 38 minutos. O pg_repack lida melhor com isso: pede o lock com timeout curto e, se não conseguir, solta e tenta de novo, deixando consultas passarem entre as tentativas.
Mesmo sem conflito de lock, o catch-up final tem um custo próprio. Sob 3.000 updates/s, o REPACK (CONCURRENTLY) parou gravadores por 7 segundos no fim; o pg_squeeze, por 20 segundos; o pg_repack nunca parou de vez, mas manteve cerca de 27 segundos a 30% do throughput normal no meio do processo, com pior latência de 22,3 segundos contra 7,7 do REPACK.
O teto de memória que pode derrubar o cluster inteiro
O achado mais sério do benchmark é um limite estrutural: para cada linha alterada ou apagada por outras sessões durante o repack, o backend do REPACK mantém cerca de 50 bytes em memória até commitar, sem nenhum parâmetro tipo maintenance_work_mem para conter isso. O autor segurou o comando pouco antes do catch-up, acumulou dezenas de milhões de updates e liberou, num container com limite de memória e sem swap:
| Limite de memória | O que aconteceu | Mudanças replicadas até então |
|---|---|---|
| 1 GB | backend morto por OOM, cluster reinicia | ~18,0 milhões |
| 4 GB | backend morto por OOM, cluster reinicia | ~84,3 milhões |
| 8 GB | ERROR: invalid memory alloc request size 1677721600 | 104.820.740 |
O problema não é falta de RAM: a estrutura que guarda essas entradas dobra de tamanho sempre que enche, e depois de 104.857.600 entradas a próxima duplicação pede uma alocação maior do que o PostgreSQL permite de uma vez. Ou seja, REPACK (CONCURRENTLY) não consegue terminar se mais de 105 milhões de linhas forem alteradas ou apagadas durante o run, e falha justamente no fim, embora a tabela original fique intacta. O problema foi reportado na lista pgsql-hackers; a correção está prevista para o PostgreSQL 20, não para a versão 19.
Na prática, isso vira uma conta de capacidade: a fórmula do benchmark é max (updates + deletes) por segundo ≈ 29.000 / duração do REPACK em horas. Vale medir a taxa de mudança de cada tabela via pg_stat_user_tables (colunas n_tup_upd e n_tup_del) antes de agendar um repack longo, porque um único job noturno que atualiza 120 milhões de linhas é suficiente para estourar o teto sozinho.
MVCC não garantido, e o que isso quebra silenciosamente
A documentação já avisa: REPACK (CONCURRENTLY) não é MVCC-safe. Numa transação REPEATABLE READ que tira seu snapshot sem tocar na tabela (lendo outra tabela antes), passa o repack, e só depois consulta a tabela repactada, o resultado é zero linhas, porque toda linha da cópia nova foi escrita pela transação do REPACK, que aquele snapshot antigo considera estar no futuro:
| Ferramenta | Snapshot antigo vê | Sessão nova vê |
|---|---|---|
| nada / VACUUM FULL | 10.000 | 10.000 |
| REPACK (CONCURRENTLY) | 0 | 10.000 |
| pg_squeeze | 0 | 10.000 |
| pg_repack | 10.000 (esperou) | 10.000 |
O pg_repack só parece seguro porque espera toda transação aberta no banco, leitura inclusive, antes de começar a copiar, o que apenas estreita a janela do problema, sem eliminá-la: a cópia em si é uma transação não commitada enquanto roda. Um export longo em REPEATABLE READ que lê a tabela só depois da troca simplesmente não recebe explicação nenhuma para o resultado vazio.
Requisitos e o que fica pendente
Algumas condições valem antes de considerar REPACK (CONCURRENTLY) em produção:
- A tabela precisa de índice de identidade de réplica (chave primária ou
REPLICA IDENTITY USING INDEX); chave primária adiável não conta, eREPLICA IDENTITY FULL/NOTHINGnão são suportados. - Não roda na tabela-pai de uma partição, só em partições individuais; o autor recomenda inclusive tratar partições grandes como a estratégia real para escapar do teto de 105 milhões de mudanças.
- Exige slot livre sob
max_repack_replication_slots(5 por padrão) e worker livre emmax_worker_processes. wal_level = replicabasta, mas o servidor inteiro passa a operar comeffective_wal_level = logicalenquanto o slot temporário existir, carregando WAL extra até o checkpoint seguinte.
Um detalhe operacional separa as ferramentas quando algo dá errado: cancelar a sessão do REPACK ou do pg_squeeze limpa tudo, sem deixar resíduo em pg_replication_slots nem no diretório de dados. Já matar o cliente do pg_repack no meio do processo (uma conexão SSH derrubada, por exemplo) deixa a tabela de log, a cópia pela metade e o trigger ativo na tabela original, registrando cada mudança num log órfão até alguém apagá-lo manualmente.
A conclusão prática do benchmark é que REPACK (CONCURRENTLY) compensa a maioria dos casos, por gerar menos WAL, não instalar trigger e funcionar em serviços gerenciados, mas dois pontos pedem cautela nesta versão 19: o teto de 105 milhões de mudanças, que derruba o cluster inteiro se estourado sem vm.overcommit_memory=2 configurado, com correção prevista para o PostgreSQL 20, e a ausência de retry automático no lock do fim, que descarta todo o trabalho num timeout e não tem correção anunciada pela fonte.
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.
Pesquisador encontra 76 falhas em extensões que travam o superusuário no Postgres gerenciado
Mehmet Ince mostra como validadores de Foreign Data Wrapper e truques de search_path permitem escapar das camadas de segurança que Azure, AWS, Google, Supabase, Aiven, Neon, PlanetScale e Xata.io usam para impedir que clientes virem superusuário de verdade.














