Postgres: INSERTAR si aún no existe

362

Estoy usando Python para escribir en una base de datos postgres:

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

Pero debido a que algunas de mis filas son idénticas, aparece el siguiente error:

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

¿Cómo puedo escribir una instrucción SQL 'INSERT a menos que esta fila ya exista'?

He visto declaraciones complejas 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

Pero, en primer lugar, ¿es esto excesivo para lo que necesito y, en segundo lugar, cómo puedo ejecutar uno de esos como una cadena simple?

AP257
fuente
56
Independientemente de cómo resuelva este problema, no debe generar su consulta de esa manera. Use los parámetros en su consulta y pase los valores por separado; ver stackoverflow.com/questions/902408/…
Thomas Wouters el
3
¿Por qué no atrapar la excepción e ignorarla?
Matthew Mitchell
55
A partir de Posgres 9.5 (actualmente en beta2) hay una nueva característica como upsert, ver: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno
2
¿Has considerado aceptar una respuesta para esto? =]
Relequestual

Respuestas:

514

Postgres 9.5 (lanzado desde 2016-01-07) ofrece un comando "upsert" , también conocido como una cláusula ON CONFLICT para INSERTAR :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Resuelve muchos de los problemas sutiles con los que se puede encontrar al usar la operación concurrente, que algunas otras respuestas proponen.

Arie
fuente
14
9.5 fue liberado.
luckydonald
2
@TusharJain antes de PostgreSQL 9.5 puede hacer un UPSERT "anticuado" (con CTE) pero puede experimentar problemas con las condiciones de la carrera y no funcionará como el estilo 9.5. Hay un buen detalle sobre upsert en este blog (en el área actualizada en la parte inferior) que incluye algunos enlaces si desea leer más sobre los detalles.
Skyguard
17
Para aquellos necesarios, aquí hay dos ejemplos simples. (1) INSERTAR si no existe más NADA - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) INSERTAR si no existe más ACTUALIZAR - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Estos ejemplos son del manual - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan
13
Hay una advertencia / efecto secundario. En una tabla con columna de secuencia (serial o bigserial), incluso si no se inserta una fila, la secuencia se incrementa en cada intento de inserción.
Grzegorz Luczywo
2
Sería mejor vincular a la documentación INSERT en lugar de apuntar a la publicación. Enlace al documento: postgresql.org/docs/9.5/static/sql-insert.html
borjagvo
379

¿Cómo puedo escribir una instrucción SQL 'INSERT a menos que esta fila ya exista'?

Hay una buena manera de hacer INSERT condicional en PostgreSQL:

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

Sin embargo, este enfoque no es 100% confiable para operaciones de escritura concurrentes . Hay una condición de carrera muy pequeña entre SELECTel NOT EXISTSanti-semi-join y el INSERTpropio. Se puede fallar bajo tales condiciones.

John Doe
fuente
¿Qué tan seguro es esto asumiendo que el campo "nombre" tiene una restricción ÚNICA? ¿Alguna vez fallará con una violación única?
agnsaft
2
Esto funciona bien El único problema es el acoplamiento, supongo: ¿qué pasa si uno modifica la tabla de modo que más columnas sean únicas? En ese caso, todos los scripts deben modificarse. Sería bueno si hubiera una forma más genérica de hacer esto ...
Willem Van Onsem
1
¿Es posible usarlo, RETURNS idpor ejemplo, para obtener idsi se ha insertado o no?
Olivier Pons
2
@ OlivierPons sí, es posible. Agregue RETURNING idal y de la consulta y devolverá una nueva identificación de fila o nada, si no se ha insertado ninguna fila.
AlexM
44
He encontrado que esto no es confiable. Parece que Postgres a veces ejecuta la inserción antes de que haya ejecutado la selección y termino con una violación de clave duplicada a pesar de que el registro aún no se ha insertado. Intente usar version => 9.5 con ON CONFLICT.
Michael Silver
51

Un enfoque sería crear una tabla no restringida (sin índices únicos) para insertar todos sus datos y hacer una selección distinta de esa para hacer su inserción en su tabla cien.

Tan alto nivel sería. Supongo que las tres columnas son distintas en mi ejemplo, por lo que para el paso 3, cambie la unión NO SALIR para unir solo en las columnas únicas en la tabla cien.

  1. Crear tabla temporal. Ver documentos aquí .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. INSERTAR datos en la tabla temporal.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Agregue cualquier índice a la tabla temporal.

  4. Hacer la inserción de la mesa 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
fuente
3
Esta es la forma más rápida que he encontrado para hacer inserciones masivas cuando no sé si la fila ya existe.
Nate c
seleccione 'X'? alguien puede aclarar? Esto es simplemente una declaración de selección correcta: SELECT name,name_slug,statuso*
roberthuttinger
3
Buscar subconsulta correlacionada. 'X' podría cambiarse a 1 o incluso 'SadClown'. SQL requiere que haya algo y 'X' es algo común de usar. Es pequeño y hace obvio que se está utilizando una subconsulta correlacionada y cumple con los requisitos de lo que requiere SQL.
Kuberchaun
Usted mencionó "insertar todos sus datos en (suponiendo una tabla temporal) y hacer una selección distinta de eso". En ese caso, ¿no debería ser así SELECT DISTINCT name, name_slug, status FROM temp_data?
gibbz00
17

Por desgracia, PostgreSQLsoportes ni MERGEtampoco ON DUPLICATE KEY UPDATE, por lo que tendrá que hacerlo en dos estados:

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

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

Puedes envolverlo en una función:

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';

y solo llámalo:

SELECT  fn_upd_invoices('12345', 'TRUE')
Quassnoi
fuente
1
En realidad, esto no funciona: puedo llamar INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);cualquier cantidad de veces y sigue insertando la fila.
AP257
1
@ 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. Hay un registro
Quassnoi
12

Puede hacer uso de VALUES - disponible en Postgres:

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;
crististm
fuente
12
SELECCIONE el nombre DE Persona <--- ¿y si hay mil millones de filas en persona?
Henley Chiu
1
Creo que esta es una buena forma rápida de resolver el problema, pero solo cuando esté seguro de que la tabla fuente nunca crecerá enormemente. Tengo una tabla que nunca tendrá más de 1000 filas, por lo que puedo usar esta solución.
Leonard
WOW, esto es exactamente lo que necesitaba. Me preocupaba tener que crear una función o una tabla temporal, pero esto excluye todo eso, ¡gracias!
Amalgovinus
8

Sé que esta pregunta es de hace un tiempo, pero pensé que podría ayudar a alguien. Creo que la forma más fácil de hacerlo es a través de un disparador. P.ej:

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();

Ejecute este código desde un indicador de psql (o como quiera ejecutar consultas directamente en la base de datos). Luego puede insertar de forma normal desde Python. P.ej:

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

Tenga en cuenta que, como @Thomas_Wouters ya mencionó, el código anterior aprovecha los parámetros en lugar de concatenar la cadena.

ktr
fuente
Si alguien más se preguntaba también, de los documentos : "Los disparadores de nivel de fila disparados ANTES pueden devolver un valor nulo para indicar al administrador de disparadores que omita el resto de la operación para esta fila (es decir, los disparadores posteriores no se disparan, y el INSERT / UPDATE / DELETE no se produce para esta fila). Si se devuelve un valor no nulo, la operación continúa con ese valor de fila ".
Pete
Exactamente esta respuesta que estaba buscando. Código limpio, usando la función + disparador en lugar de la instrucción select. +1
Jacek Krawczyk
Me encanta esta respuesta, uso function y trigger. Ahora encuentro otra forma de romper el punto muerto usando funciones y disparadores ...
Sukma Saputra
7

Hay una buena manera de hacer INSERT condicional en PostgreSQL usando la consulta WITH: como:

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
fuente
7

Este es exactamente el problema que enfrento y mi versión es la 9.5

Y lo resuelvo con la consulta SQL a continuación.

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 ayude a alguien que tiene el mismo problema con la versión> = 9.5.

Gracias por leer.

tuanngocptn
fuente
5

INSERTAR .. DONDE NO EXISTE es un buen enfoque. Y las condiciones de carrera se pueden evitar mediante la transacción "sobre":

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

Es fácil con las reglas:

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

Pero falla con escrituras concurrentes ...


fuente
1

El enfoque con la mayoría de los votos positivos (de John Doe) de alguna manera funciona para mí, pero en mi caso de las 422 filas esperadas, obtengo solo 180. No pude encontrar nada malo y no hay ningún error, así que busqué otro Enfoque simple.

Usar IF NOT FOUND THENafter a SELECTjust me funciona perfectamente.

(descrito en la documentación de PostgreSQL )

Ejemplo de documentación:

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

La clase de cursor psycopgs tiene el atributo rowcount .

Este atributo de solo lectura especifica el número de filas que la última ejecución * () produjo (para sentencias DQL como SELECT) o afectadas (para sentencias DML como UPDATE o INSERT).

Por lo tanto, puede intentar ACTUALIZAR primero e INSERTAR solo si el recuento de filas es 0.

Pero dependiendo de los niveles de actividad en su base de datos, puede alcanzar una condición de carrera entre ACTUALIZAR e INSERTAR, donde otro proceso puede crear ese registro mientras tanto.

Johnbaum
fuente
Presumiblemente, envolver estas consultas en una transacción aliviaría la condición de la carrera.
Daniel Lyons
Gracias, solución realmente simple y limpia
Alexander Malfait
1

Su columna "cien" parece estar definida como clave principal y, por lo tanto, debe ser única, lo que no es el caso. El problema no es con sus datos.

Le sugiero que inserte una identificación como tipo de serie para manejar la clave primaria.

Boodoo
fuente
1

Si dice que muchas de sus filas son idénticas, finalizará la comprobación muchas veces. Puede enviarlos y la base de datos determinará si lo inserta o no con la cláusula ON CONFLICT de la siguiente manera

  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);
abre un
fuente
0

Estaba buscando una solución similar, tratando de encontrar SQL que funcione en PostgreSQL y HSQLDB. (HSQLDB fue lo que hizo esto difícil). Usando su ejemplo como base, este es el formato que encontré en otro 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
fuente
-1

Aquí hay una función genérica de Python que, dado un nombre de tabla, columnas y valores, genera el equivalente upsert para postgresql.

importar json

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)
Patricio
fuente
-8

La solución es simple, pero no inmediata.
Si desea utilizar esta instrucción, debe hacer un cambio en la base de datos:

ALTER USER user SET search_path to 'name_of_schema';

después de estos cambios "INSERTAR" funcionará correctamente.

el fuser
fuente