PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Como atualizar todas as colunas com INSERT ... ON CONFLICT ...?


A UPDATE sintaxe requer para nomear explicitamente as colunas de destino. Possíveis razões para evitar isso:
  • Você tem muitas colunas e quer apenas encurtar a sintaxe.
  • Você não sabe nomes de coluna, exceto para a(s) coluna(s) exclusiva(s).

"All columns" tem que significar "todas as colunas da tabela de destino" (ou pelo menos "colunas iniciais da tabela" ) em ordem e tipo de dados correspondentes. Caso contrário, você teria que fornecer uma lista de nomes de coluna de destino de qualquer maneira.

Tabela de teste:
CREATE TABLE tbl (
   id    int PRIMARY KEY
 , text  text
 , extra text
);

INSERT INTO tbl AS t
VALUES (1, 'foo')
     , (2, 'bar');

1. DELETE &INSERT em uma única consulta


Sem saber nenhum nome de coluna, exceto id .

Funciona apenas para "todas as colunas da tabela de destino" . Embora a sintaxe funcione até mesmo para um subconjunto inicial, as colunas em excesso na tabela de destino seriam redefinidas para NULL com DELETE e INSERT .

UPSERT (INSERT ... ON CONFLICT ... ) é necessário para evitar problemas de simultaneidade/bloqueio sob carga de gravação simultânea e apenas porque não há uma maneira geral de bloquear linhas ainda não existentes no Postgres (bloqueio de valor ).

Seu requisito especial afeta apenas o UPDATE papel. Possíveis complicações não se aplicam quando existir linhas são afetadas. Esses estão bloqueados corretamente. Simplificando um pouco mais, você pode reduzir seu caso para DELETE e INSERT :
WITH data(id) AS (              -- Only 1st column gets explicit name!
   VALUES
      (1, 'foo_upd', 'a')       -- changed
    , (2, 'bar', 'b')           -- unchanged
    , (3, 'baz', 'c')           -- new
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data d
   WHERE  t.id = d.id
   -- AND    t <> d              -- optional, to avoid empty updates
   )                             -- only works for complete rows
INSERT INTO tbl AS t
TABLE  data                      -- short for: SELECT * FROM data
ON     CONFLICT (id) DO NOTHING
RETURNING t.id;

No modelo Postgres MVCC, um UPDATE é basicamente o mesmo que DELETE e INSERT de qualquer forma (exceto para alguns casos de canto com simultaneidade, atualizações HOT e grandes valores de coluna armazenados fora de linha). Como você deseja substituir todas as linhas de qualquer maneira, apenas remova as linhas conflitantes antes do INSERT . As linhas excluídas permanecem bloqueadas até que a transação seja confirmada. O INSERT só poderá encontrar linhas conflitantes para valores de chave anteriormente não existentes se uma transação simultânea acontecer para inseri-los simultaneamente (após o DELETE , mas antes do INSERT ).

Você perderia valores de coluna adicionais para linhas afetadas neste caso especial. Nenhuma exceção levantada. Mas se as consultas concorrentes tiverem a mesma prioridade, isso dificilmente será um problema:a outra consulta ganhou para algumas linhas. Além disso, se a outra consulta for um UPSERT semelhante, sua alternativa é aguardar a confirmação dessa transação e, em seguida, atualizar imediatamente. "Vencer" poderia ser uma vitória de Pirro.

Sobre "atualizações vazias":
  • Como posso (ou posso) SELECT DISTINCT em várias colunas?

Não, minha consulta deve vencer!


Pronto, você pediu:
WITH data(id) AS (                   -- Only 1st column gets explicit name!
   VALUES                            -- rest gets default names "column2", etc.
     (1, 'foo_upd', NULL)              -- changed
   , (2, 'bar', NULL)                  -- unchanged
   , (3, 'baz', NULL)                  -- new
   , (4, 'baz', NULL)                  -- new
   )
, ups AS (
   INSERT INTO tbl AS t
   TABLE  data                       -- short for: SELECT * FROM data
   ON     CONFLICT (id) DO UPDATE
   SET    id = t.id
   WHERE  false                      -- never executed, but locks the row!
   RETURNING t.id
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data     d
   LEFT   JOIN ups u USING (id)
   WHERE  u.id IS NULL               -- not inserted !
   AND    t.id = d.id
   -- AND    t <> d                  -- avoid empty updates - only for full rows
   RETURNING t.id
   )
, ins AS (
   INSERT INTO tbl AS t
   SELECT *
   FROM   data
   JOIN   del USING (id)             -- conflict impossible!
   RETURNING id
   )
SELECT ARRAY(TABLE ups) AS inserted  -- with UPSERT
     , ARRAY(TABLE ins) AS updated   -- with DELETE & INSERT;

Como?
  • Os data do 1º CTE apenas fornece dados. Pode ser uma tabela.
  • Os ups do 2º CTE :UPSERT. Linhas com id conflitante não são alterados, mas também bloqueados .
  • O 3º CTE del exclui linhas conflitantes. Eles permanecem bloqueados.
  • O 4º CTE ins insere linhas inteiras . Permitido apenas para a mesma transação
  • O SELECT final é apenas para a demonstração para mostrar o que aconteceu.

Para verificar se há atualizações vazias, teste (antes e depois) com:
SELECT ctid, * FROM tbl; -- did the ctid change?

A verificação (comentada) para quaisquer alterações na linha AND t <> d funciona mesmo com valores NULL porque estamos comparando dois valores de linha digitados de acordo com o manual:

dois valores de campo NULL são considerados iguais e um NULL é considerado maior que um não NULL

2. SQL dinâmico


Isso também funciona para um subconjunto de colunas iniciais, preservando os valores existentes.

O truque é deixar o Postgres construir a string de consulta com os nomes das colunas dos catálogos do sistema dinamicamente e depois executá-la.

Veja as respostas relacionadas para o código:

  • Atualizar várias colunas em uma função de gatilho em plpgsql

  • Atualização em massa de todas as colunas

  • SQL atualiza campos de uma tabela a partir de campos de outra