Por que as linhas inseridas em um CTE não podem ser atualizadas na mesma instrução?

12

No PostgreSQL 9.5, dada uma tabela simples criada com:

create table tbl (
    id serial primary key,
    val integer
);

Eu corro SQL para inserir um valor, em seguida, atualizá-lo na mesma instrução:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

O resultado é que o UPDATE é ignorado:

testdb=> select * from tbl;
┌────┬─────┐
 id  val 
├────┼─────┤
  1    1 
└────┴─────┘

Por que é isso? Essa limitação faz parte do padrão SQL (ou seja, está presente em outros bancos de dados) ou é algo específico do PostgreSQL que pode ser corrigido no futuro? A documentação das consultas WITH diz que várias UPDATEs não são suportadas, mas não menciona INSERTs e UPDATEs.

Jeff Turner
fonte

Respostas:

14

Todas as declarações em uma CTE acontecem praticamente ao mesmo tempo. Ou seja, eles são baseados no mesmo instantâneo do banco de dados.

O UPDATEvê o mesmo estado da tabela subjacente que o INSERT, o que significa que a linha com val = 1ainda não está lá. O manual esclarece aqui:

Todas as instruções são executadas com o mesmo instantâneo (consulte o Capítulo 13 ), portanto, elas não podem "ver" os efeitos umas das outras nas tabelas de destino.

Cada instrução pode ver o que é retornado por outro CTE na RETURNINGcláusula. Mas as tabelas subjacentes parecem iguais para elas.

Você precisaria de duas instruções (em uma única transação) para o que você está tentando fazer. O exemplo dado deve realmente ser apenas um, INSERTpara começar, mas isso pode ser devido ao exemplo simplificado.

Erwin Brandstetter
fonte
14

Esta é uma decisão de implementação. É descrito na documentação do Postgres, WITHConsultas (expressões comuns de tabela) . Existem dois parágrafos relacionados ao problema.

Primeiro, a razão do comportamento observado:

As sub-instruções em WITHsão executadas simultaneamente entre si e com a consulta principal . Portanto, ao usar instruções de modificação de dados WITH, a ordem na qual as atualizações especificadas realmente acontecem é imprevisível. Todas as instruções são executadas com o mesmo instantâneo (consulte o Capítulo 13), portanto, elas não podem "ver" os efeitos umas das outras nas tabelas de destino. Isso alivia os efeitos da imprevisibilidade da ordem real das atualizações de linha e significa que os RETURNINGdados são a única maneira de comunicar alterações entre diferentes WITHsub-instruções e a consulta principal. Um exemplo disso é que em ...

Depois de postar uma sugestão junto ao pgsql-docs , Marko Tiikkaja explicou (o que concorda com a resposta de Erwin):

Os casos insert-update e insert-delete não funcionam porque os UPDATEs e DELETEs não têm como ver as linhas INSERTed devido ao fato de sua captura instantânea ter sido tirada antes que o INSERT acontecesse. Não há nada imprevisível nesses dois casos.

Portanto, a razão pela qual sua declaração não é atualizada pode ser explicada pelo primeiro parágrafo acima (sobre "instantâneos"). O que acontece quando você modifica CTEs é que todos eles e a consulta principal são executados e "veem" o mesmo instantâneo dos dados (tabelas), como eram imediatamente antes da execução da instrução. Os CTEs podem passar informações sobre o que foram inseridos / atualizados / excluídos entre si e para a consulta principal usando a RETURNINGcláusula, mas eles não podem ver as alterações nas tabelas diretamente. Então, vamos ver o que acontece em sua declaração:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

Temos 2 partes, a CTE ( newval):

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id

e a consulta principal:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id

O fluxo de execução é algo como isto:

           initial data: tbl
                id  val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id  val           \
            1    1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query

Como resultado, quando a consulta principal une o tbl(como visto no instantâneo) à newvaltabela, ela une uma tabela vazia a uma tabela de 1 linha. Obviamente, ele atualiza 0 linhas. Portanto, a declaração nunca chegou a modificar a linha recém-inserida e é isso que você vê.

A solução no seu caso é reescrever a instrução para inserir os valores corretos em primeiro lugar ou usar 2 instruções. Um que insere e um segundo para atualizar.


Existem outras situações semelhantes, como se a instrução tivesse um INSERTe depois um DELETEnas mesmas linhas. A exclusão falharia exatamente pelos mesmos motivos.

Alguns outros casos, com atualização-atualização e atualização-exclusão e seu comportamento, são explicados no parágrafo seguinte, na mesma página de documentos.

Tentar atualizar a mesma linha duas vezes em uma única instrução não é suportado. Apenas uma das modificações ocorre, mas não é fácil (e às vezes não é possível) prever com precisão qual. Isso também se aplica à exclusão de uma linha que já foi atualizada na mesma instrução: somente a atualização é executada. Portanto, você geralmente deve evitar tentar modificar uma única linha duas vezes em uma única instrução. Em particular, evite escrever sub-instruções WITH que possam afetar as mesmas linhas alteradas pela instrução principal ou por uma sub-instrução irmã. Os efeitos dessa declaração não serão previsíveis.

E na resposta de Marko Tiikkaja:

Os casos update-update e update-delete não são explicitamente causados ​​pelos mesmos detalhes de implementação subjacentes (como os casos insert-update e insert-delete).
O caso atualização-atualização não funciona porque parece internamente com o problema do Dia das Bruxas, e o Postgres não tem como saber quais tuplas poderiam atualizar duas vezes e quais poderiam reintroduzir o problema do Dia das Bruxas.

Portanto, o motivo é o mesmo (como as CTEs modificadas são implementadas e como cada CTE vê o mesmo instantâneo), mas os detalhes diferem nesses 2 casos, pois são mais complexos e os resultados podem ser imprevisíveis no caso de atualização e atualização.

Na inserção-atualização (como o seu caso) e em uma inserção-exclusão semelhante, os resultados são previsíveis. Somente a inserção acontece porque a segunda operação (atualização ou exclusão) não tem como ver e afetar as linhas recém-inseridas.


A solução sugerida é a mesma para todos os casos que tentam modificar as mesmas linhas mais de uma vez: Não faça isso. Escreva instruções que modifiquem cada linha uma vez ou use instruções separadas (2 ou mais).

ypercubeᵀᴹ
fonte