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, VACUUM
custo. 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_target
deve 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 source
coluna é 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 chats
funciona 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 VALUES
expressã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 COMMITTED
isolamento 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ê INSERT
detectará 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 SELECT
vê 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 SELECT
també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 VALUES
expressã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 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. 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 chats
no 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
.
ON CONFLICT UPDATE
para que haja uma alteração na linha. EntãoRETURNING
irá capturá-lo.