como emular “insert ignore” e “on key key update” (sql merge) com o postgresql?

140

Alguns servidores SQL têm um recurso que INSERTé ignorado se violar uma restrição de chave primária / exclusiva. Por exemplo, o MySQL possui INSERT IGNORE.

Qual é a melhor maneira de emular INSERT IGNOREe ON DUPLICATE KEY UPDATEcom o PostgreSQL?

gpilotino
fonte
Veja também: stackoverflow.com/questions/5269590/…
Dave Jarvis
6
como de 9,5, é possível nativamente: stackoverflow.com/a/34639631/4418
Warren
Emular o MySQL: ON DUPLICATE KEY UPDATEno PgSQL 9.5 ainda é um pouco impossível, porque o ON CLAUSEequivalente ao PgSQL exige que você forneça o nome da restrição, enquanto o MySQL pode capturar qualquer restrição sem a necessidade de defini-la. Isso me impede de "emular" esse recurso sem reescrever consultas.
NeverEndingQueue

Respostas:

35

Tente fazer uma atualização. Se não modificar nenhuma linha que significa que não existia, faça uma inserção. Obviamente, você faz isso dentro de uma transação.

É claro que você pode agrupar isso em uma função se não quiser colocar o código extra no lado do cliente. Você também precisa de um loop para a condição de corrida muito rara nesse pensamento.

Há um exemplo disso na documentação: http://www.postgresql.org/docs/9.3/static/plpgsql-control-structures.html , exemplo 40-2, na parte inferior.

Essa é geralmente a maneira mais fácil. Você pode fazer alguma mágica com regras, mas provavelmente será muito mais confuso. Eu recomendaria a abordagem de função de envolvimento em qualquer dia.

Isso funciona para valores de linha única ou poucas linhas. Se você está lidando com grandes quantidades de linhas, por exemplo, de uma subconsulta, é melhor dividi-la em duas consultas, uma para INSERT e outra para UPDATE (como uma junção / subseleção apropriada, é claro - não é necessário escrever sua principal) filtrar duas vezes)

Magnus Hagander
fonte
4
"Se você está lidando com grandes quantidades de linhas", esse é exatamente o meu caso. Eu quero atualizar / inserir linhas em massa e com o mysql eu posso fazer isso com apenas uma consulta sem nenhum loop. Agora, eu me pergunto se isso também é possível com o postgresql: usar apenas uma consulta para atualizar em massa OU inserir. Você diz: "é melhor dividi-lo em duas consultas, uma para INSERT e outra para UPDATE", mas como posso fazer uma inserção que não gere erros nas chaves duplicadas? (ex. "INSERIR
IGNORAR
4
Magnus significava que você usasse uma consulta como esta: "inicie a transação; crie a tabela temporária temporary_table como selecione * do teste em que false; copie a tabela temporária de 'data_file.csv'; bloqueie o teste da tabela; atualize o conjunto de testes data = temporary_table.data de temporary_table em que test.id = temporary_table.id; inserto em teste seleccionar * de temporary_table onde id não em (ID de seleccionar a partir de teste) na forma de um"
Tometzky
25
Atualização: com o PostgreSQL 9.5, agora é tão simples quanto INSERT ... ON CONFLICT DO NOTHING;. Consulte também a resposta stackoverflow.com/a/34639631/2091700 .
Alphaaa
Importante, o padrão SQL nãoMERGE é um upsert seguro para simultaneidade, a menos que você faça o primeiro. As pessoas usam dessa maneira, mas está errado. LOCK TABLE
Craig Ringer
1
Com v9.5 é agora uma característica 'nativa', portanto, verifique o comentário de @Alphaaa (apenas publicidade o comentário que anuncia a resposta)
Camilo Delvasto
178

Com o PostgreSQL 9.5, agora é uma funcionalidade nativa (como o MySQL tem há vários anos):

INSERIR ... EM CONFLITO, NADA / ATUALIZAR ("UPSERT")

9.5 traz suporte para operações "UPSERT". O INSERT é estendido para aceitar uma cláusula ON CONFLICT DO UPDATE / IGNORE. Esta cláusula especifica uma ação alternativa a ser tomada no caso de uma possível violação de duplicação.

...

Mais um exemplo de nova sintaxe:

INSERT INTO user_logins (username, logins)
VALUES ('Naomi',1),('James',1) 
ON CONFLICT (username)
DO UPDATE SET logins = user_logins.logins + EXCLUDED.logins;
Warren
fonte
100

Edit: no caso de você ter perdido a resposta de warren , o PG9.5 agora tem isso nativamente; hora de atualizar!


Com base na resposta de Bill Karwin, para explicar como seria uma abordagem baseada em regras (transferência de outro esquema no mesmo banco de dados e com uma chave primária de várias colunas):

CREATE RULE "my_table_on_duplicate_ignore" AS ON INSERT TO "my_table"
  WHERE EXISTS(SELECT 1 FROM my_table 
                WHERE (pk_col_1, pk_col_2)=(NEW.pk_col_1, NEW.pk_col_2))
  DO INSTEAD NOTHING;
INSERT INTO my_table SELECT * FROM another_schema.my_table WHERE some_cond;
DROP RULE "my_table_on_duplicate_ignore" ON "my_table";

Nota: A regra se aplica a todas as INSERToperações até que a regra seja descartada, portanto, não completamente ad hoc.

EoghanM
fonte
@sema você quer dizer se another_schema.my_tablecontém duplicatas de acordo com as restrições de my_table?
EoghanM
2
@EoghanM Testei a regra no postgresql 9.3 e ainda podia inserir duplicatas com várias instruções de inserção de linha, como, por exemplo, INSERT INTO "my_table" (a, b), (a, b); (Assumindo que fileira (a, b) não existe, em "my_table" ainda.)
sema
@sema, gotcha - isso deve significar que a regra é executada no início de todos os dados a serem inseridos e não reexecutada após a inserção de cada linha. Uma abordagem seria para inserir seus dados em outra tabela temporária primeiro que não tem quaisquer restrições, e depois fazendoINSERT INTO "my_table" SELECT DISTINCT ON (pk_col_1, pk_col_2) * FROM the_tmp_table;
EoghanM
@EoghanM Outra abordagem é para relaxar restrições duplicados temporariamente e aceitar duplicatas na inserção, mas duplicatas remove depois comDELETE FROM my_table WHERE ctid IN (SELECT ctid FROM (SELECT ctid,ROW_NUMBER() OVER (PARTITION BY pk_col_1,pk_col_2) AS rn FROM my_table) AS dups WHERE dups.rn > 1);
sema
Estou tendo o problema descrito por @sema. Se eu fizer uma inserção (a, b), (a, b), isso gera um erro. Existe uma maneira de suprimir os erros, também neste caso?
Diogo Melo
35

Para aqueles que possuem o Postgres 9.5 ou superior, a nova sintaxe ON CONFLICT DO NADA deve funcionar:

INSERT INTO target_table (field_one, field_two, field_three ) 
SELECT field_one, field_two, field_three
FROM source_table
ON CONFLICT (field_one) DO NOTHING;

Para aqueles de nós que possuem uma versão anterior, essa associação correta funcionará:

INSERT INTO target_table (field_one, field_two, field_three )
SELECT source_table.field_one, source_table.field_two, source_table.field_three
FROM source_table 
LEFT JOIN target_table ON source_table.field_one = target_table.field_one
WHERE target_table.field_one IS NULL;
hanmari
fonte
A segunda abordagem não funciona ao fazer uma grande inserção em um ambiente simultâneo. Você começa uma Unique violation: 7 ERROR: duplicate key value violates unique constraintquando target_tabletinha outra linha inserido nele enquanto esta consulta estava sendo executado, se as suas chaves, na verdade, duplicar o outro. Acredito que o bloqueio target_tableajudará, mas a concorrência obviamente sofrerá.
G. Kashtanov
1
ON CONFLICT (field_one) DO NOTHINGé a melhor parte da resposta.
Abel Callejo
24

Para obter a inserção, ignore a lógica, você pode fazer algo como abaixo. Achei que a simples inserção de uma instrução select de valores literais funcionou melhor, então você pode mascarar as chaves duplicadas com uma cláusula NOT EXISTS. Para obter a atualização da lógica duplicada, suspeito que um loop pl / pgsql seria necessário.

INSERT INTO manager.vin_manufacturer
(SELECT * FROM( VALUES
  ('935',' Citroën Brazil','Citroën'),
  ('ABC', 'Toyota', 'Toyota'),
  ('ZOM',' OM','OM')
  ) as tmp (vin_manufacturer_id, manufacturer_desc, make_desc)
  WHERE NOT EXISTS (
    --ignore anything that has already been inserted
    SELECT 1 FROM manager.vin_manufacturer m where m.vin_manufacturer_id = tmp.vin_manufacturer_id)
)
Keyo
fonte
E se o tmp contiver uma linha duplicada, o que pode acontecer?
Henley Chiu
Você sempre pode selecionar com a palavra-chave distinta.
Keyo
5
Assim como um FYI, o truque "ONDE NÃO EXISTE" não funciona em várias transações porque as diferentes transações não podem ver os dados adicionados recentemente das outras transações.
Dave Johansen
21
INSERT INTO mytable(col1,col2) 
    SELECT 'val1','val2' 
    WHERE NOT EXISTS (SELECT 1 FROM mytable WHERE col1='val1')
user2342158
fonte
Qual o impacto de várias transações, todas tentando fazer a mesma coisa? É possível que entre a execução onde não existe e a inserção executando alguma outra transação insira uma linha? E se o Postgres pode impedir isso, o postgres não está introduzindo um ponto de sincronização em todas essas transações quando o atinge?
07714
Isso não funciona com várias transações, porque os dados adicionados recentemente não são visíveis para as outras transações.
Dave Johansen
12

Parece que o PostgreSQL suporta um objeto de esquema chamado regra .

http://www.postgresql.org/docs/current/static/rules-update.html

Você pode criar uma regra ON INSERTpara uma determinada tabela, fazendo isso NOTHINGse existir uma linha com o valor da chave primária especificado, ou então fazendo isso em UPDATEvez de INSERTse existir uma linha com o valor da chave primária especificado.

Eu não tentei isso sozinho, então não posso falar por experiência própria ou dar um exemplo.

Bill Karwin
fonte
1
se eu entendi bem, essas regras são gatilhos que são executados toda vez que uma instrução é chamada. e se eu quiser aplicar a regra a apenas uma consulta? eu tenho que criar a regra e destruí-la imediatamente? (o que acontece com as condições de corrida?)
gpilotino
3
Sim, eu teria as mesmas perguntas também. O mecanismo de regras é a coisa mais próxima que pude encontrar no PostgreSQL para o INSERT IGNORE do MySQL ou ON UPDATE DE CHAVE DUPLICATE. Se procurarmos por "postgresql na atualização duplicada de chave", você encontrará outras pessoas recomendando o mecanismo de regras, mesmo que uma regra se aplique a qualquer INSERT, não apenas de forma ad hoc.
Bill Karwin
4
O PostgreSQL suporta DDL transacional, o que significa que, se você criar uma regra e soltá-la em uma única transação, a regra nunca será visível fora (e, portanto, nunca terá qualquer efeito fora) dessa transação.
cdhowie
6

Como @hanmari mencionou em seu comentário. ao inserir em uma tabela do postgres, o conflito on (..) não faz nada é o melhor código a ser usado para não inserir dados duplicados .:

query = "INSERT INTO db_table_name(column_name)
         VALUES(%s) ON CONFLICT (column_name) DO NOTHING;"

A linha de código ON CONFLICT permitirá que a instrução insert ainda insira linhas de dados. O código de consulta e valores é um exemplo de data inserida de um Excel em uma tabela do postgres db. Tenho restrições adicionadas a uma tabela do postgres que utilizo para garantir que o campo ID seja exclusivo. Em vez de executar uma exclusão em linhas de dados iguais, adiciono uma linha de código sql que renumera a coluna ID iniciando em 1. Exemplo:

q = 'ALTER id_column serial RESTART WITH 1'

Se meus dados tiverem um campo de ID, eu não o uso como o ID primário / ID de série, crio uma coluna de ID e defino-o como serial. Espero que esta informação seja útil para todos. * Não tenho diploma universitário em desenvolvimento / codificação de software. Tudo o que sei em codificação, estudo por conta própria.

Yankeeownz
fonte
isso não funciona em índices únicos compostos!
Nulik 9/07
4

Esta solução evita o uso de regras:

BEGIN
   INSERT INTO tableA (unique_column,c2,c3) VALUES (1,2,3);
EXCEPTION 
   WHEN unique_violation THEN
     UPDATE tableA SET c2 = 2, c3 = 3 WHERE unique_column = 1;
END;

mas tem uma desvantagem de desempenho (consulte PostgreSQL.org ):

Um bloco que contém uma cláusula EXCEPTION é significativamente mais caro para entrar e sair do que um bloco sem uma. Portanto, não use EXCEPTION sem necessidade.

Numero quatro
fonte
1

Em massa, você sempre pode excluir a linha antes da inserção. A exclusão de uma linha que não existe não causa um erro, portanto é ignorada com segurança.

David Noriega
fonte
2
Esta abordagem será bastante propenso a condições de corrida estranhos, eu não recomendo ...
Steven Schlansker
1
+1 Isso é fácil e genérico. Se usado com cuidado, isso pode realmente ser uma solução simples.
Wouter van Nifterick
1
Também não funcionará quando os dados existentes forem alterados após a inserção (mas não na chave duplicada) e queremos manter as atualizações. Esse é o cenário em que há scripts SQL que são escritos para vários sistemas ligeiramente diferentes, como atualizações de db executadas em sistemas de produção, controle de qualidade, desenvolvimento e teste.
21712 Hanno Fietz
1
A chave estrangeira pode ser um problema se você as criar com DEFERRABLE INITIALLY DEFERREDsinalizadores.
temoto
-1

Para scripts de importação de dados, para substituir "SE NÃO EXISTE", de certa forma, existe uma formulação um pouco estranha que, no entanto, funciona:

DO
$do$
BEGIN
PERFORM id
FROM whatever_table;

IF NOT FOUND THEN
-- INSERT stuff
END IF;
END
$do$;
analytik_work
fonte