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 cadaUPDATE
, 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:
ParaON CONFLICT DO UPDATE
, umconflict_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:
QuandoVALUES
é usado emINSERT
, 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
.