Como usar RETURNING com ON CONFLICT no PostgreSQL?


149

Eu tenho o seguinte UPSERT no PostgreSQL 9.5:

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

Se não houver conflitos, ele retornará algo como isto:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

Mas se houver conflitos, ele não retornará nenhuma linha:

----------
    | id |
----------

Desejo retornar as novas idcolunas se não houver conflitos ou retornar as idcolunas existentes das colunas conflitantes.
Isso pode ser feito? Se sim, como?


1
Use ON CONFLICT UPDATEpara que haja uma alteração na linha. Então RETURNINGirá capturá-lo.
Gordon Linoff

1
@GordonLinoff E se não houver nada para atualizar?
Okku 06/06

1
Se não há nada a atualização, isso significa que não havia conflito para que ele apenas insere os novos valores e retornar a sua id
zola

1
Você encontrará outras maneiras aqui . Eu adoraria saber a diferença entre os dois em termos de desempenho.
Stanislasdrg Restabelecer Monica

Respostas:


88

Eu tinha exatamente o mesmo problema e resolvi-o usando 'atualizar' em vez de 'não fazer nada', apesar de não ter nada para atualizar. No seu caso, seria algo como isto:

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

Essa consulta retornará todas as linhas, independentemente de terem sido inseridas ou já existirem antes.


11
Um problema com essa abordagem é que o número de sequência da chave primária é incrementado a cada conflito (atualização falsa), o que basicamente significa que você pode acabar com grandes lacunas na sequência. Alguma idéia de como evitar isso?
precisa

9
@Mischa: e daí? Seqüências não são garantidos para ser gapless em primeiro lugar e as lacunas não importam (e se o fizerem, uma sequência é a coisa errada a fazer)
a_horse_with_no_name

24
Eu não recomendaria usar isso na maioria dos casos. Eu adicionei uma resposta porque.
Erwin Brandstetter

4
Esta resposta parece não atingir o DO NOTHINGaspecto da pergunta original - para mim parece atualizar o campo sem conflito (aqui, "nome") para todas as linhas.
PeterJCLaw

Conforme discutido na resposta muito longa abaixo, usar "Atualizar" para um campo que não foi alterado não é uma solução "limpa" e pode causar outros problemas.
Bill Worthington

202

A resposta atualmente aceita parece adequada para um único destino de conflito, poucos conflitos, pequenas tuplas e sem gatilhos. Evita o problema 1 de simultaneidade (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, no entanto, 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 linhas "inocentes", possivelmente incorrendo em custos para transações simultâneas.

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

  • Mais importante , com o modelo MVCC do PostgreSQL, uma nova versão de linha é gravada para todos UPDATE, independentemente de os dados da linha serem alterados. Isso incorre em uma penalidade de desempenho para o próprio UPSERT, inchaço da mesa, inchaço do índice, penalidade de desempenho para operações subsequentes na mesa, VACUUMcusto. Um efeito menor para poucas duplicatas, mas massivo para a maioria dos enganados.

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

Para ON CONFLICT DO UPDATE, um conflict_targetdeve ser fornecido.

Um único "destino de conflito" não é possível se vários índices / restrições estiverem envolvidos.

Você pode conseguir (quase) o mesmo sem atualizações vazias e efeitos colaterais. Algumas das soluções a seguir também funcionam ON CONFLICT DO NOTHING(sem "destino de conflito"), para capturar todos os conflitos possíveis que possam surgir - o que pode ou não ser desejável.

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 sourcecoluna é uma adição opcional para demonstrar como isso funciona. Você pode realmente precisar dizer a diferença entre os dois casos (outra vantagem sobre gravações vazias).

O final JOIN chatsfunciona porque as linhas recém-inseridas de um CTE modificador 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 das tabelas subjacentes.)

Como a VALUESexpressão é independente (não está diretamente anexada a um INSERT), o Postgres não pode derivar tipos de dados das colunas de destino e pode ser necessário adicionar conversões de tipo explícitas. O manual:

Quando VALUESé usado INSERT, todos os valores são coagidos automaticamente ao tipo de dados da coluna de destino correspondente. Quando usado em outros contextos, pode ser necessário especificar o tipo de dados correto. Se todas as entradas forem constantes literais entre aspas, coagir 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 alguns burros, devido à sobrecarga da CTE e à adicional SELECT(que deve ser barata, pois o índice perfeito existe por definição - uma restrição exclusiva é implementada com Um índice).

Pode ser (muito) mais rápido para muitas 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 no geral.

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

Sobre CTEs:

Com carga de gravação simultânea

Assumindo READ COMMITTEDisolamento de transação padrão . Palavras-chave:

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

Problema de simultaneidade 1

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

Se as outras extremidades de transação com ROLLBACK(ou qualquer erro, ou seja, automática ROLLBACK), a transação pode prosseguir normalmente. Menor efeito colateral possível: lacunas nos números seqüenciais. Mas não há linhas ausentes.

Se a outra transação terminar normalmente (implícita ou explícita COMMIT), você INSERTdetectará um conflito (o UNIQUEíndice / restrição é absoluto) e DO NOTHING, portanto, também não retornará a linha. (Também não é possível bloquear a linha, conforme demonstrado no problema de concorrência 2 abaixo, pois não é visível .) Ele SELECTvê o mesmo instantâneo desde o início da consulta e também não pode retornar a linha ainda invisível.

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

Isso pode estar ok como está . Especialmente se você não está retornando linhas como no exemplo e está 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 estar na mesma transação), que verá as linhas recém confirmadas.

Ou verifique se há linhas de resultados ausentes na mesma consulta e substitua aquelas com o truque da 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 ao CTE ups, antes de retornar o conjunto completo de resultados. Esse último CTE não fará nada na maioria das vezes. Somente se faltarem linhas no resultado retornado, usaremos força bruta.

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

Um efeito colateral: o segundo UPSERT grava linhas fora de ordem e reintroduz a possibilidade de conflitos (veja abaixo) se três ou mais transações gravadas nas mesmas linhas se sobrepuserem. Se isso for um problema, você precisará de uma solução diferente - como repetir toda a declaração, como mencionado acima.

Problema de concorrência 2

Se transações simultâneas puderem gravar em colunas envolvidas de linhas afetadas, e você precisar garantir que as linhas encontradas ainda estejam lá em um estágio posterior da mesma transação, poderá bloquear as linhas existentes mais baratas no CTE ins(que, de outra forma, seriam desbloqueadas) 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 SELECTtambémFOR UPDATE .

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

Mais detalhes e explicação:

Deadlocks?

Defenda-se contra conflitos , inserindo linhas em ordem consistente . Vejo:

Tipos de dados e transmissões

Tabela existente como modelo para tipos de dados ...

Conversões explícitas de tipo para a primeira linha de dados na VALUESexpressão independente podem ser inconvenientes. Existem maneiras de contornar isso. Você pode usar qualquer relação existente (tabela, exibição, ...) como modelo de linha. A tabela de destino é a escolha óbvia para o caso de uso. Os dados de entrada são coagidos a tipos apropriados automaticamente, como na VALUESclá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. Vejo:

... e nomes

Isso também funciona para todos os tipos de dados.

Ao inserir em todas as colunas (iniciais) da tabela, você pode omitir os nomes das colunas. Supondo que a tabela chatsno exemplo consista 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
   )
   ...

Aparte: não use palavras reservadas como "user"como identificador. Essa é uma espingarda carregada. Use identificadores legais, em minúsculas e sem aspas. Eu substituí por usr.


2
Você implicam este método não irá criar lacunas nos folhetins, mas eles são: INSERT ... ON CONFLICT não fazer nada faz incremento da série de cada vez do que eu posso ver
harmic

1
não que isso importe muito, mas por que os seriais são incrementados? e não há como evitar isso?
Saliente

1
@salient: Como adicionei acima: os valores padrão da coluna são preenchidos antes do teste de conflitos e as seqüências nunca são revertidas, para evitar conflitos com gravações simultâneas.
Erwin Brandstetter

7
Incrível. Funciona como um encanto e fácil de entender quando você olha com cuidado. Eu ainda desejo ON CONFLICT SELECT...que uma coisa embora :)
Roshambo

3
Incrível. Os criadores do Postgres parecem estar torturando os usuários. Porque não basta simplesmente fazer retornar cláusula sempre retornam valores, independentemente da existência eram inserções ou não?
Anatoly Alekseev

16

Upsert, sendo uma extensão da INSERTconsulta, pode ser definido com dois comportamentos diferentes em caso de conflito de restrição: DO NOTHINGou DO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

Observe também que RETURNINGnada retorna, porque nenhuma tupla foi inserida . Agora com DO UPDATE, é possível executar operações na tupla com as quais há um conflito. Primeiro, observe que é importante definir uma restrição que será usada para definir que há um conflito.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)

2
Ótima maneira de sempre obter o ID da linha afetada e saber se era uma inserção ou upsert. Apenas o que eu precisava.
Moby Duck

Isso ainda está usando o "Do Update", cujas desvantagens já foram discutidas.
Bill Worthington

4

Para inserções de um único item, eu provavelmente usaria uma coalescência ao retornar o ID:

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

O principal objetivo do uso ON CONFLICT DO NOTHINGé evitar gerar erros, mas não causará retornos de linha. Então, precisamos de outro SELECTpara obter o ID existente.

Nesse SQL, se falhar em conflitos, ele não retornará nada, o segundo SELECTobterá a linha existente; se ele for inserido com êxito, haverá dois registros iguais e precisamos UNIONmesclar o resultado.


Esta solução funciona bem e evita fazer uma gravação desnecessária (atualização) no banco de dados !! Agradável!
Simon C

0

Modifiquei a resposta incrível de Erwin Brandstetter, que não incrementa a sequência e também não bloqueia nenhuma linha. Eu sou relativamente novo no PostgreSQL, portanto, sinta-se à vontade para me informar se você encontrar algum inconveniente nesse método:

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

Isso pressupõe que a tabela chatstenha uma restrição exclusiva nas colunas (usr, contact).

Atualização: adicionadas as revisões sugeridas do spatar (abaixo). Obrigado!


1
Em vez de CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existsapenas escrever r.id IS NOT NULL as row_exists. Em vez de WHERE row_exists=FALSEapenas escrever WHERE NOT row_exists.
spatar 12/06
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.