Postgres: INSERT se já não existir

361

Estou usando o Python para escrever em um banco de dados postgres:

sql_string = "INSERT INTO hundred (name,name_slug,status) VALUES ("
sql_string += hundred + ", '" + hundred_slug + "', " + status + ");"
cursor.execute(sql_string)

Mas como algumas das minhas linhas são idênticas, recebo o seguinte erro:

psycopg2.IntegrityError: duplicate key value  
  violates unique constraint "hundred_pkey"

Como posso escrever uma instrução SQL 'INSERT, a menos que essa linha já exista'?

Eu já vi declarações complexas como esta recomendadas:

IF EXISTS (SELECT * FROM invoices WHERE invoiceid = '12345')
UPDATE invoices SET billed = 'TRUE' WHERE invoiceid = '12345'
ELSE
INSERT INTO invoices (invoiceid, billed) VALUES ('12345', 'TRUE')
END IF

Mas, em primeiro lugar, é um exagero para o que eu preciso e, em segundo lugar, como posso executar um deles como uma sequência simples?

AP257
fonte
56
Independentemente de como você resolve esse problema, não deve gerar sua consulta dessa maneira. Use parâmetros em sua consulta e passe os valores separadamente; veja stackoverflow.com/questions/902408/…
Thomas Wouters
3
Por que não capturar a exceção e ignorá-la?
Matthew Mitchell
5
A partir de Posgres 9.5 (atualmente em beta 2) há um novo upsert como recurso, consulte: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno
2
Você já pensou em aceitar uma resposta para isso? =]
Relequestual

Respostas:

513

O Postgres 9.5 (lançado desde 07-01-2016) oferece um comando "upsert" , também conhecido como cláusula ON CONFLICT, para INSERT :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Ele resolve muitos dos problemas sutis que você pode encontrar ao usar a operação simultânea, que algumas outras respostas propõem.

Arie
fonte
14
9.5 foi lançado.
luckydonald
2
@TusharJain anterior ao PostgreSQL 9.5, você pode fazer um UPSERT "antiquado" (com CTE), mas você pode ter problemas com as condições da corrida e ele não terá o desempenho do estilo 9.5. Há um bom detalhe sobre upsert neste blog (na área atualizada na parte inferior), incluindo alguns links, se você quiser ler mais sobre os detalhes.
Skyguard
17
Para aqueles necessários, aqui estão dois exemplos simples. (1) INSERT se não existir mais NADA - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) INSERT se não existir mais ATUALIZAÇÃO - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Estes exemplos são do manual - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan
13
Há uma ressalva / efeito colateral. Em uma tabela com coluna de sequência (serial ou bigserial), mesmo que nenhuma linha seja inserida, a sequência é incrementada a cada tentativa de inserção.
Grzegorz Luczywo
2
Seria melhor vincular à documentação INSERT em vez de apontar para release. Ligação Doc: postgresql.org/docs/9.5/static/sql-insert.html
borjagvo
379

Como posso escrever uma instrução SQL 'INSERT, a menos que essa linha já exista'?

Existe uma boa maneira de fazer INSERT condicional no PostgreSQL:

INSERT INTO example_table
    (id, name)
SELECT 1, 'John'
WHERE
    NOT EXISTS (
        SELECT id FROM example_table WHERE id = 1
    );

CAVEAT Essa abordagem não é 100% confiável para operações de gravação simultâneas . Existe uma condição de raça muito pequena entre SELECTo NOT EXISTSanti-semi-join e o INSERTpróprio. Ele pode falhar sob tais condições.

John Doe
fonte
Quão seguro é isso assumindo que o campo "nome" tenha uma restrição ÚNICA? Será que alguma vez falhará com violação única?
agnsaft
2
Isso funciona bem. O único problema é o acoplamento, eu acho: e se alguém modificar a tabela de modo que mais colunas sejam únicas. Nesse caso, todos os scripts devem ser modificados. Seria bom se houvesse uma maneira mais genérica para fazer isso ...
Willem Van Onsem
11
É possível usá-lo com, RETURNS idpor exemplo, para idsaber se foi inserido ou não?
precisa saber é o seguinte
2
@OlivierPons sim, é possível. Adicione RETURNING idno e da consulta e ele retornará um novo ID de linha ou nada, se nenhuma linha tiver sido inserida.
AlexM
4
Eu descobri que isso não é confiável. Parece que o Postgres às vezes executa a inserção antes de executar a seleção e acabo com uma violação de chave duplicada, mesmo que o registro ainda não tenha sido inserido. Tente usar a versão => 9.5 com ON CONFLICT.
Michael Silver
51

Uma abordagem seria criar uma tabela não restrita (sem índices exclusivos) para inserir todos os seus dados e fazer uma seleção distinta daquela para fazer sua inserção na sua tabela de cem.

Tão alto nível seria. Suponho que todas as três colunas sejam distintas no meu exemplo, portanto, para a etapa 3, altere a junção NOT EXITS para ingressar apenas nas colunas exclusivas da tabela de cem.

  1. Crie tabela temporária. Veja os documentos aqui .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. INSERIR Dados na tabela temporária.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Adicione quaisquer índices à tabela temporária.

  4. Faça a inserção da tabela principal.

    INSERT INTO hundred(name, name_slug, status) 
        SELECT DISTINCT name, name_slug, status
        FROM hundred
        WHERE NOT EXISTS (
            SELECT 'X' 
            FROM temp_data
            WHERE 
                temp_data.name          = hundred.name
                AND temp_data.name_slug = hundred.name_slug
                AND temp_data.status    = status
        );
Kuberchaun
fonte
3
Essa é a maneira mais rápida que eu encontrei de fazer inserções em massa quando não sei se a linha já existe.
nate c
selecione 'X'? alguém pode esclarecer? Esta é simplesmente uma instrução select correta: SELECT name,name_slug,statusou*
roberthuttinger
3
Pesquisa subconsulta correlacionada. 'X' pode ser alterado para 1 ou até 'SadClown'. O SQL exige que exista algo e 'X' é uma coisa comum de se usar. É pequeno e torna óbvio que uma subconsulta correlacionada está sendo usada e atende aos requisitos do que o SQL exige.
Kuberchaun # 9/14
Você mencionou "insira todos os seus dados (assumindo a tabela temporária) e faça uma seleção diferente daquela". Nesse caso, não deveria ser SELECT DISTINCT name, name_slug, status FROM temp_data?
gibbz00
17

Infelizmente, PostgreSQLnão suporta nem MERGEnem ON DUPLICATE KEY UPDATE, então você terá que fazer isso em duas instruções:

UPDATE  invoices
SET     billed = 'TRUE'
WHERE   invoices = '12345'

INSERT
INTO    invoices (invoiceid, billed)
SELECT  '12345', 'TRUE'
WHERE   '12345' NOT IN
        (
        SELECT  invoiceid
        FROM    invoices
        )

Você pode envolvê-lo em uma função:

CREATE OR REPLACE FUNCTION fn_upd_invoices(id VARCHAR(32), billed VARCHAR(32))
RETURNS VOID
AS
$$
        UPDATE  invoices
        SET     billed = $2
        WHERE   invoices = $1;

        INSERT
        INTO    invoices (invoiceid, billed)
        SELECT  $1, $2
        WHERE   $1 NOT IN
                (
                SELECT  invoiceid
                FROM    invoices
                );
$$
LANGUAGE 'sql';

e apenas chame:

SELECT  fn_upd_invoices('12345', 'TRUE')
Quassnoi
fonte
11
Na verdade, isso não funciona: eu posso ligar INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);várias vezes e ele continua inserindo a linha.
AP257 9/03/11
11
@ AP257: CREATE TABLE hundred (name TEXT, name_slug TEXT, status INT); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); SELECT * FROM hundred. Há um registro.
Quassnoi 9/03/11
12

Você pode usar VALUES - disponível no Postgres:

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;
crististm
fonte
12
SELECT nome FROM Pessoa <--- e se houver um bilhão de linhas em pessoa?
Henley Chiu
11
Eu acho que essa é uma maneira rápida e agradável de resolver o problema, mas somente quando você tiver certeza de que a tabela de origem nunca ficará grande. Eu tenho uma tabela que nunca terá mais de 1000 linhas, para que eu possa usar esta solução.
Leonard
WOW, isso é exatamente o que eu precisava. Eu estava preocupado que precisaria criar uma função ou uma tabela temporária, mas isso exclui tudo isso - obrigado!
Amalgovinus
8

Eu sei que essa pergunta é de um tempo atrás, mas achei que isso poderia ajudar alguém. Eu acho que a maneira mais fácil de fazer isso é através de um gatilho. Por exemplo:

Create Function ignore_dups() Returns Trigger
As $$
Begin
    If Exists (
        Select
            *
        From
            hundred h
        Where
            -- Assuming all three fields are primary key
            h.name = NEW.name
            And h.hundred_slug = NEW.hundred_slug
            And h.status = NEW.status
    ) Then
        Return NULL;
    End If;
    Return NEW;
End;
$$ Language plpgsql;

Create Trigger ignore_dups
    Before Insert On hundred
    For Each Row
    Execute Procedure ignore_dups();

Execute esse código em um prompt do psql (ou como você deseja executar consultas diretamente no banco de dados). Então você pode inserir normalmente do Python. Por exemplo:

sql = "Insert Into hundreds (name, name_slug, status) Values (%s, %s, %s)"
cursor.execute(sql, (hundred, hundred_slug, status))

Observe que, como o @Thomas_Wouters já mencionado, o código acima aproveita os parâmetros em vez de concatenar a string.

ktr
fonte
Se alguém também estava se perguntando, nos documentos : "Os gatilhos no nível da linha disparados ANTES podem retornar nulo para sinalizar ao gerenciador de gatilhos para ignorar o restante da operação desta linha (ou seja, os gatilhos subsequentes não são disparados e o INSERT / UPDATE / DELETE não ocorre para esta linha). Se um valor não nulo for retornado, a operação continuará com esse valor de linha. "
Pete
Exatamente esta resposta que eu estava procurando. Limpe o código, usando a função + trigger em vez da instrução select. +1
Jacek Krawczyk 03/02
Eu amo essa resposta, use a função e o gatilho. Agora eu encontrar outra maneira de romper o impasse usando funções e gatilhos ...
Sukma Saputra
7

Existe uma boa maneira de fazer INSERT condicional no PostgreSQL usando a consulta WITH:

WITH a as(
select 
 id 
from 
 schema.table_name 
where 
 column_name = your_identical_column_value
)
INSERT into 
 schema.table_name
(col_name1, col_name2)
SELECT
    (col_name1, col_name2)
WHERE NOT EXISTS (
     SELECT
         id
     FROM
         a
        )
  RETURNING id 
Ritesh Jha
fonte
7

Este é exatamente o problema que enfrento e minha versão é 9.5

E eu resolvo isso com a consulta SQL abaixo.

INSERT INTO example_table (id, name)
SELECT 1 AS id, 'John' AS name FROM example_table
WHERE NOT EXISTS(
            SELECT id FROM example_table WHERE id = 1
    )
LIMIT 1;

Espero que ajude alguém que tenha o mesmo problema com a versão> = 9.5.

Obrigado pela leitura.

tuanngocptn
fonte
5

INSERIR .. ONDE NÃO EXISTE é uma boa abordagem. E as condições de corrida podem ser evitadas pela transação "envelope":

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;
Pavel Francírek
fonte
2

É fácil com as regras:

CREATE RULE file_insert_defer AS ON INSERT TO file
WHERE (EXISTS ( SELECT * FROM file WHERE file.id = new.id)) DO INSTEAD NOTHING

Mas falha com gravações simultâneas ...


fonte
1

A abordagem com os mais votados (de John Doe) funciona de alguma forma para mim, mas, no meu caso, das 422 linhas esperadas, recebo apenas 180. Não consegui encontrar nada errado e não há erros, por isso procurei uma solução diferente. abordagem simples.

Usar IF NOT FOUND THENdepois de SELECTapenas funciona perfeitamente para mim.

(descrito na documentação do PostgreSQL )

Exemplo da documentação:

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
  RAISE EXCEPTION 'employee % not found', myname;
END IF;
vchrizz
fonte
1

A classe de cursor psycopgs possui o atributo rowcount .

Esse atributo somente leitura especifica o número de linhas que a última execução * () produziu (para instruções DQL como SELECT) ou afetou (para instruções DML como UPDATE ou INSERT).

Portanto, você pode tentar UPDATE primeiro e INSERT apenas se o número de linhas for 0.

Mas, dependendo dos níveis de atividade em seu banco de dados, você pode atingir uma condição de corrida entre UPDATE e INSERT, onde outro processo pode criar esse registro nesse ínterim.

johnbaum
fonte
Presumivelmente, agrupar essas consultas em uma transação aliviaria a condição de corrida.
Daniel Lyons
Graças, realmente solução simples e limpo
Alexander Malfait
1

Sua coluna "cem" parece ser definida como chave primária e, portanto, deve ser única, o que não é o caso. O problema não é com os seus dados.

Sugiro que você insira um ID como tipo de série para manusear a chave primária

Boodoo
fonte
1

Se você disser que muitas de suas linhas são idênticas, você terminará a verificação várias vezes. Você pode enviá-los e o banco de dados determinará se o inserirá ou não com a cláusula ON CONFLICT da seguinte maneira

  INSERT INTO Hundred (name,name_slug,status) VALUES ("sql_string += hundred  
  +",'" + hundred_slug + "', " + status + ") ON CONFLICT ON CONSTRAINT
  hundred_pkey DO NOTHING;" cursor.execute(sql_string);
opena
fonte
0

Eu estava procurando uma solução semelhante, tentando encontrar SQL que funcionasse no PostgreSQL e no HSQLDB. (Foi o HSQLDB que tornou isso difícil.) Usando o seu exemplo como base, este é o formato que encontrei em outro lugar.

sql = "INSERT INTO hundred (name,name_slug,status)"
sql += " ( SELECT " + hundred + ", '" + hundred_slug + "', " + status
sql += " FROM hundred"
sql += " WHERE name = " + hundred + " AND name_slug = '" + hundred_slug + "' AND status = " + status
sql += " HAVING COUNT(*) = 0 );"
Jeff Fairley
fonte
-1

Aqui está uma função python genérica que, com um nome de tabela, colunas e valores, gera o equivalente de upsert para o postgresql.

json de importação

def upsert(table_name, id_column, other_columns, values_hash):

    template = """
    WITH new_values ($$ALL_COLUMNS$$) as (
      values
         ($$VALUES_LIST$$)
    ),
    upsert as
    (
        update $$TABLE_NAME$$ m
            set
                $$SET_MAPPINGS$$
        FROM new_values nv
        WHERE m.$$ID_COLUMN$$ = nv.$$ID_COLUMN$$
        RETURNING m.*
    )
    INSERT INTO $$TABLE_NAME$$ ($$ALL_COLUMNS$$)
    SELECT $$ALL_COLUMNS$$
    FROM new_values
    WHERE NOT EXISTS (SELECT 1
                      FROM upsert up
                      WHERE up.$$ID_COLUMN$$ = new_values.$$ID_COLUMN$$)
    """

    all_columns = [id_column] + other_columns
    all_columns_csv = ",".join(all_columns)
    all_values_csv = ','.join([query_value(values_hash[column_name]) for column_name in all_columns])
    set_mappings = ",".join([ c+ " = nv." +c for c in other_columns])

    q = template
    q = q.replace("$$TABLE_NAME$$", table_name)
    q = q.replace("$$ID_COLUMN$$", id_column)
    q = q.replace("$$ALL_COLUMNS$$", all_columns_csv)
    q = q.replace("$$VALUES_LIST$$", all_values_csv)
    q = q.replace("$$SET_MAPPINGS$$", set_mappings)

    return q


def query_value(value):
    if value is None:
        return "NULL"
    if type(value) in [str, unicode]:
        return "'%s'" % value.replace("'", "''")
    if type(value) == dict:
        return "'%s'" % json.dumps(value).replace("'", "''")
    if type(value) == bool:
        return "%s" % value
    if type(value) == int:
        return "%s" % value
    return value


if __name__ == "__main__":

    my_table_name = 'mytable'
    my_id_column = 'id'
    my_other_columns = ['field1', 'field2']
    my_values_hash = {
        'id': 123,
        'field1': "john",
        'field2': "doe"
    }
    print upsert(my_table_name, my_id_column, my_other_columns, my_values_hash)
Patrick
fonte
-8

A solução é simples, mas não imediata.
Se você quiser usar esta instrução, faça uma alteração no db:

ALTER USER user SET search_path to 'name_of_schema';

após essas alterações, "INSERIR" funcionará corretamente.

el fusor
fonte