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

Como usar RETURNING com ON CONFLICT no PostgreSQL?


A resposta atualmente aceita parece correta para um único alvo de conflito, poucos conflitos, pequenas tuplas e nenhum gatilho. Evita problema de simultaneidade 1 (veja abaixo) com força bruta. A solução simples tem seu apelo, os efeitos colaterais podem ser menos importantes.

Para todos os outros casos, porém, não atualize linhas idênticas sem necessidade. Mesmo que você não veja nenhuma diferença na superfície, existem vários efeitos colaterais :

  • Pode disparar gatilhos que não devem ser disparados.

  • Ele bloqueia a gravação de linhas "inocentes", possivelmente incorrendo em custos para transações simultâneas.

  • Isso pode fazer com que a linha pareça nova, embora seja antiga (carimbo de data e hora da transação).

  • O mais importante , com o modelo MVCC do PostgreSQL, uma nova versão de linha é escrita para cada UPDATE , não importa se os dados da linha foram alterados. Isso incorre em uma penalidade de desempenho para o próprio UPSERT, inchaço da tabela, inchaço do índice, penalidade de desempenho para operações subsequentes na tabela, VACUUM custo. Um efeito menor para poucas duplicatas, mas massivo para principalmente dupes.

Mais , às vezes não é prático ou mesmo possível usar ON CONFLICT DO UPDATE . O manual:

Para ON CONFLICT DO UPDATE , um conflict_target Deve ser providenciado.

Um único "alvo de conflito" não é possível se vários índices/restrições estiverem envolvidos. Mas aqui está uma solução relacionada para vários índices parciais:
  • UPSERT com base na restrição UNIQUE com valores NULL

De volta ao tópico, você pode conseguir (quase) o mesmo sem atualizações vazias e efeitos colaterais. Algumas das soluções a seguir também funcionam com ON CONFLICT DO NOTHING (sem "alvo de conflito"), para capturar todos possíveis conflitos que possam surgir - que podem ou não ser desejáveis.

Sem carga de gravação simultânea

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, ins AS (
   INSERT INTO chats (usr, contact, name) 
   SELECT * FROM input_rows
   ON CONFLICT (usr, contact) DO NOTHING
   RETURNING id  --, usr, contact              -- return more columns?
   )
SELECT 'i' AS source                           -- 'i' for 'inserted'
     , id  --, usr, contact                    -- return more columns?
FROM   ins
UNION  ALL
SELECT 's' AS source                           -- 's' for 'selected'
     , c.id  --, usr, contact                  -- return more columns?
FROM   input_rows
JOIN   chats c USING (usr, contact);           -- columns of unique index

A source coluna é uma adição opcional para demonstrar como isso funciona. Você pode realmente precisar dele para dizer a diferença entre os dois casos (outra vantagem sobre gravações vazias).

Os bate-papos JOIN chats finais funciona porque as linhas recém-inseridas de um CTE de modificação de dados anexado ainda não estão visíveis na tabela subjacente. (Todas as partes da mesma instrução SQL veem os mesmos instantâneos de tabelas subjacentes.)

Uma vez que os VALUES expressão é independente (não diretamente anexada a um INSERT ) O Postgres não pode derivar tipos de dados das colunas de destino e talvez seja necessário adicionar conversões de tipo explícitas. O manual:

Quando VALUES é usado em INSERT , os valores são todos automaticamente forçados para o tipo de dados da coluna de destino correspondente. Quando usado em outros contextos, pode ser necessário especificar o tipo de dados correto. Se as entradas forem todas constantes literais entre aspas, forçar a primeira é suficiente para determinar o tipo assumido para todas.

A consulta em si (sem contar os efeitos colaterais) pode ser um pouco mais cara para poucos dupes, devido à sobrecarga do CTE e do SELECT adicional (que deve ser barato, pois o índice perfeito existe por definição - uma restrição exclusiva é implementada com um índice).

Pode ser (muito) mais rápido para muitos duplicatas. O custo efetivo de gravações adicionais depende de muitos fatores.

Mas há menos efeitos colaterais e custos ocultos em qualquer caso. É provavelmente mais barato em geral.

As sequências anexadas ainda são avançadas, pois os valores padrão são preenchidos antes testes de conflitos.

Sobre os CTEs:
  • As consultas do tipo SELECT são o único tipo que pode ser aninhado?
  • Desduplicar instruções SELECT na divisão relacional

Com carga de gravação simultânea


Assumindo o padrão READ COMMITTED isolamento da transação. Relacionado:
  • Transações simultâneas resultam em condição de corrida com restrição exclusiva na inserção

A melhor estratégia para se defender contra condições de corrida depende dos requisitos exatos, do número e tamanho das linhas na tabela e nos UPSERTs, do número de transações simultâneas, da probabilidade de conflitos, recursos disponíveis e outros fatores...

Problema de simultaneidade 1


Se uma transação simultânea foi gravada em uma linha que sua transação agora tenta UPSERT, sua transação deve aguardar a conclusão da outra.

Se a outra transação terminar com ROLLBACK (ou qualquer erro, ou seja, ROLLBACK automático ), sua transação pode prosseguir normalmente. Efeito colateral menor possível:lacunas nos números sequenciais. Mas não faltam linhas.

Se a outra transação terminar normalmente (implícito ou explícito COMMIT ), seu INSERT irá detectar um conflito (o UNIQUE índice/restrição é absoluta) e DO NOTHING , portanto, também não retorna a linha. (Também não é possível bloquear a linha conforme demonstrado em problema de simultaneidade 2 abaixo, pois não está visível .) O SELECT vê o mesmo instantâneo desde o início da consulta e também não pode retornar a linha ainda invisível.

Quaisquer linhas estão faltando no conjunto de resultados (mesmo que existam na tabela subjacente)!

Isso pode estar ok como está . Especialmente se você não estiver retornando linhas como no exemplo e estiver satisfeito sabendo que a linha está lá. Se isso não for bom o suficiente, existem várias maneiras de contornar isso.

Você pode verificar a contagem de linhas da saída e repetir a instrução se ela não corresponder à contagem de linhas da entrada. Pode ser bom o suficiente para o caso raro. O ponto é iniciar uma nova consulta (pode ser na mesma transação), que verá as linhas recém-confirmadas.

Ou verifique se há linhas de resultado ausentes dentro a mesma consulta e substituir aqueles com o truque de força bruta demonstrado na resposta de Alextoni.
WITH input_rows(usr, contact, name) AS ( ... )  -- see above
, ins AS (
   INSERT INTO chats AS c (usr, contact, name) 
   SELECT * FROM input_rows
   ON     CONFLICT (usr, contact) DO NOTHING
   RETURNING id, usr, contact                   -- we need unique columns for later join
   )
, sel AS (
   SELECT 'i'::"char" AS source                 -- 'i' for 'inserted'
        , id, usr, contact
   FROM   ins
   UNION  ALL
   SELECT 's'::"char" AS source                 -- 's' for 'selected'
        , c.id, usr, contact
   FROM   input_rows
   JOIN   chats c USING (usr, contact)
   )
, ups AS (                                      -- RARE corner case
   INSERT INTO chats AS c (usr, contact, name)  -- another UPSERT, not just UPDATE
   SELECT i.*
   FROM   input_rows i
   LEFT   JOIN sel   s USING (usr, contact)     -- columns of unique index
   WHERE  s.usr IS NULL                         -- missing!
   ON     CONFLICT (usr, contact) DO UPDATE     -- we've asked nicely the 1st time ...
   SET    name = c.name                         -- ... this time we overwrite with old value
   -- SET name = EXCLUDED.name                  -- alternatively overwrite with *new* value
   RETURNING 'u'::"char" AS source              -- 'u' for updated
           , id  --, usr, contact               -- return more columns?
   )
SELECT source, id FROM sel
UNION  ALL
TABLE  ups;

É como a consulta acima, mas adicionamos mais uma etapa com o CTE ups , antes de retornarmos o completo conjunto de resultados. Esse último CTE não fará nada na maioria das vezes. Somente se as linhas desaparecerem do resultado retornado, usamos força bruta.

Mais sobrecarga, ainda. Quanto mais conflitos com linhas pré-existentes, maior a probabilidade de isso superar a abordagem simples.

Um efeito colateral:o 2º UPSERT escreve linhas fora de ordem, então reintroduz a possibilidade de deadlocks (veja abaixo) se três ou mais as transações gravadas nas mesmas linhas se sobrepõem. Se isso for um problema, você precisa de uma solução diferente - como repetir toda a declaração conforme mencionado acima.

Problema de simultaneidade 2


Se as transações simultâneas puderem gravar nas colunas envolvidas das linhas afetadas e você precisar garantir que as linhas encontradas ainda estejam lá em um estágio posterior da mesma transação, você pode bloquear as linhas existentes barato no CTE ins (que de outra forma seria desbloqueado) com:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

E adicione uma cláusula de bloqueio ao SELECT também, como FOR UPDATE .

Isso faz com que as operações de gravação concorrentes esperem até o final da transação, quando todos os bloqueios são liberados. Então seja breve.

Mais detalhes e explicação:
  • Como incluir linhas excluídas em RETURNING from INSERT ... ON CONFLICT
  • O SELECT ou INSERT está em uma função propensa a condições de corrida?

Impasses?


Defenda-se contra impasses inserindo linhas em ordem consistente . Ver:
  • Deadlock com INSERTs de várias linhas apesar de ON CONFLICT NÃO FAZER NADA

Tipos de dados e conversões

Tabela existente como modelo para tipos de dados...


Casts de tipo explícito para a primeira linha de dados no VALUES independente expressão pode ser inconveniente. Existem maneiras de contornar isso. Você pode usar qualquer relação existente (tabela, visualização, ...) como modelo de linha. A tabela de destino é a escolha óbvia para o caso de uso. Os dados de entrada são forçados a tipos apropriados automaticamente, como no VALUES cláusula de um INSERT :
WITH input_rows AS (
  (SELECT usr, contact, name FROM chats LIMIT 0)  -- only copies column names and types
   UNION ALL
   VALUES
      ('foo1', 'bar1', 'bob1')  -- no type casts here
    , ('foo2', 'bar2', 'bob2')
   )
   ...

Isso não funciona para alguns tipos de dados. Ver:
  • Como converter o tipo NULL ao atualizar várias linhas

... e nomes


Isso também funciona para todos tipos de dados.

Ao inserir em todas as colunas (principais) da tabela, você pode omitir os nomes das colunas. Assumindo a tabela chats no exemplo consiste apenas nas 3 colunas usadas no UPSERT:
WITH input_rows AS (
   SELECT * FROM (
      VALUES
      ((NULL::chats).*)         -- copies whole row definition
      ('foo1', 'bar1', 'bob1')  -- no type casts needed
    , ('foo2', 'bar2', 'bob2')
      ) sub
   OFFSET 1
   )
   ...

Além:não use palavras reservadas como "user" como identificador. Isso é uma espingarda carregada. Use identificadores legais, minúsculos e sem aspas. Eu o substituí por usr .