sqlpostgresqlctedelete

CTE + DELETE ... RETURNING: mova linhas em um unico comando

Arquive e mova linhas de forma atomica com CTEs que modificam dados, sem janela de corrida entre DELETE e INSERT.

3 min de leituraReferencesql · postgresql · cte · delete · returning

Mover linhas de uma tabela para outra parece trivial: INSERT ... SELECT e depois DELETE. Mas entre esses dois comandos existe uma janela em que os dados ficam duplicados ou se perdem. Os CTEs que modificam dados no PostgreSQL reduzem tudo a um unico comando atomico.

O problema dos dois comandos

O classico trecho de "arquivar e depois apagar" e assim:

INSERT INTO orders_archive
SELECT * FROM orders WHERE status = 'cancelled';

DELETE FROM orders WHERE status = 'cancelled';

Duas coisas podem dar errado. Primeiro, entre o INSERT e o DELETE outra sessao pode alterar ou remover uma linha, entao voce arquiva uma coisa e apaga outra. Segundo, se uma falha cair entre os comandos, as linhas acabam nas duas tabelas ao mesmo tempo. Envolver tudo em uma transacao resolve a falha, mas a logica continua dividida em dois comandos e percorre a tabela duas vezes.

DELETE ... RETURNING dentro do WITH

O PostgreSQL permite colocar INSERT, UPDATE e DELETE diretamente em uma clausula WITH. As linhas apagadas voltam pelo RETURNING e alimentam direto o proximo passo:

WITH moved AS (
    DELETE FROM orders
    WHERE status = 'cancelled'
    RETURNING *
)
INSERT INTO orders_archive
SELECT * FROM moved;

E um unico comando. As linhas sao apagadas e arquivadas na mesma transacao, sem janela intermediaria e sem uma segunda varredura da tabela. Liste as colunas explicitamente quando os esquemas diferem:

WITH moved AS (
    DELETE FROM orders
    WHERE created_at < now() - interval '1 year'
    RETURNING id, user_id, amount, status, created_at
)
INSERT INTO orders_archive (id, user_id, amount, status, archived_at)
SELECT id, user_id, amount, status, now()
FROM moved;

A regra do snapshot unico

O detalhe central: cada subcomando de um CTE que modifica dados enxerga o mesmo snapshot, tirado quando o comando comeca. As mudancas de um CTE nao sao visiveis para os outros por um SELECT comum; elas sao visiveis apenas pelo RETURNING.

  • Se users referencia os orders que voce apaga, um SELECT FROM orders no mesmo comando ainda "enxerga" as linhas que um CTE irmao esta apagando.
  • A ordem de execucao dos subcomandos nao e definida; confie apenas no fluxo de dados pelo RETURNING.
  • Tentar alterar a mesma linha duas vezes em um comando (dois UPDATE, por exemplo) gera um resultado indefinido e nao e recomendado.

Essa regra e justamente o que torna a mudanca correta: voce opera sobre um conjunto fixo de linhas apagadas, nao sobre uma tabela viva e mutavel.

Particionar linhas em uma unica passada

O mesmo truque distribui linhas para varios destinos. Movemos os funcionarios desligados e contamos quantos sairam de cada departamento ao mesmo tempo:

WITH removed AS (
    DELETE FROM employees
    WHERE salary IS NULL
    RETURNING id, name, manager_id, dept, salary
), archived AS (
    INSERT INTO employees_archive
    SELECT * FROM removed
    RETURNING dept
)
SELECT dept, count(*) AS moved
FROM archived
GROUP BY dept;

O SELECT final devolve um relatorio do trabalho feito, util para logs e verificacoes.

Pegadinhas e diferencas entre motores

  • Pegadinha: os gatilhos AFTER DELETE da tabela de origem sao disparados, mas os CTEs que modificam dados nao garantem a ordem em relacao a eles; nao coloque efeitos colaterais de gatilhos na sua logica de mudanca.
  • RETURNING * puxa todas as colunas das linhas apagadas; se o esquema de origem mudar ele se desloca em silencio, entao liste as colunas no codigo de producao.
  • O MySQL nao tem CTEs que modificam dados: um DELETE dentro de WITH e rejeitado. Ali voce move linhas com uma transacao contendo INSERT ... SELECT mais DELETE, ou INSERT ... SELECT ... FOR UPDATE para travar as linhas.
  • O ClickHouse e um motor analitico: ali o DELETE e uma mutacao pesada e o RETURNING nao existe. As mudancas sao feitas com INSERT INTO ... SELECT mais uma mutacao a parte ou manipulacao de particoes.

Os CTEs que modificam dados sao a forma idiomatica do PostgreSQL de expressar "mova estas linhas" como uma intencao unica, atomica e legivel.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador