¿Cómo usar RETURNING con ON CONFLICT en PostgreSQL?


149

Tengo el siguiente UPSERT en PostgreSQL 9.5:

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

Si no hay conflictos, devuelve algo como esto:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

Pero si hay conflictos, no devuelve ninguna fila:

----------
    | id |
----------

Quiero devolver las nuevas idcolumnas si no hay conflictos o devolver las idcolumnas existentes de las columnas en conflicto.
Se puede hacer esto? Si es así, ¿cómo?


1
Use ON CONFLICT UPDATEpara que haya un cambio en la fila. Luego RETURNINGlo capturará.
Gordon Linoff el

1
@GordonLinoff ¿Qué pasa si no hay nada que actualizar?
Okku,

1
Si no hay nada que actualizar, significa que no hubo conflicto, por lo que solo inserta los nuevos valores y devuelve su id
Zola

1
Encontrarás otras formas aquí . Sin embargo, me encantaría saber la diferencia entre los dos en términos de rendimiento.
Stanislasdrg Restablece a Monica el

Respuestas:


88

Tuve exactamente el mismo problema y lo resolví usando 'hacer actualización' en lugar de 'no hacer nada', aunque no tenía nada que actualizar. En su caso, sería algo como esto:

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

Esta consulta devolverá todas las filas, independientemente de que se hayan insertado o que ya existían anteriormente.


11
Un problema con este enfoque es que el número de secuencia de la clave primaria se incrementa en cada conflicto (actualización falsa), lo que básicamente significa que puede terminar con grandes brechas en la secuencia. ¿Alguna idea de cómo evitar eso?
Mischa

9
@ Mischa: ¿y qué? Las secuencias nunca se garantizarán sin espacios en primer lugar y las brechas no importan (y si lo hacen, una secuencia es lo incorrecto)
a_horse_with_no_name

24
Yo no aconsejaría a utilizar esto en la mayoría de los casos. Agregué una respuesta por qué.
Erwin Brandstetter

44
Esta respuesta no parece lograr el DO NOTHINGaspecto de la pregunta original; para mí, parece actualizar el campo sin conflicto (aquí, "nombre") para todas las filas.
PeterJCLaw

Como se discutió en la larga respuesta a continuación, el uso de "Hacer actualización" para un campo que no ha cambiado no es una solución "limpia" y puede causar otros problemas.
Bill Worthington

202

La respuesta actualmente aceptada parece estar bien para un único objetivo de conflicto, pocos conflictos, pequeñas tuplas y ningún desencadenante. Evita el problema de concurrencia 1 (ver más abajo) con fuerza bruta. La solución simple tiene su atractivo, los efectos secundarios pueden ser menos importantes.

Sin embargo, para todos los demás casos, no actualice filas idénticas sin necesidad. Incluso si no ve diferencias en la superficie, hay varios efectos secundarios :

  • Puede disparar disparadores que no deberían dispararse.

  • Bloquea las filas "inocentes", posiblemente incurriendo en costos por transacciones concurrentes.

  • Puede hacer que la fila parezca nueva, aunque es antigua (marca de tiempo de transacción).

  • Lo más importante , con el modelo MVCC de PostgreSQL, se escribe una nueva versión de fila para cada UPDATE, sin importar si los datos de la fila cambiaron. Esto incurre en una penalización de rendimiento para el UPSERT en sí mismo, hinchazón de tabla, hinchazón de índice, penalización de rendimiento para operaciones posteriores en la mesa, VACUUMcosto. Un efecto menor para algunos duplicados, pero masivo para la mayoría de los engañados.

Además , a veces no es práctico o incluso posible de usar ON CONFLICT DO UPDATE. El manual:

Para ON CONFLICT DO UPDATE, se conflict_targetdebe proporcionar un.

Un único "objetivo de conflicto" no es posible si hay múltiples índices / restricciones involucrados.

Puede lograr (casi) lo mismo sin actualizaciones vacías y efectos secundarios. Algunas de las siguientes soluciones también funcionan con ON CONFLICT DO NOTHING(sin "objetivo de conflicto"), para detectar todos los posibles conflictos que puedan surgir, que pueden o no ser deseables.

Sin carga de escritura concurrente

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

La sourcecolumna es una adición opcional para demostrar cómo funciona esto. Es posible que lo necesite para diferenciar entre ambos casos (otra ventaja sobre las escrituras vacías).

El final JOIN chatsfunciona porque las filas recién insertadas de un archivo adjunto CTE de modificación de datos aún no son visibles en la tabla subyacente. (Todas las partes de la misma instrucción SQL ven las mismas instantáneas de las tablas subyacentes).

Dado que la VALUESexpresión es independiente (no se adjunta directamente a un INSERT) Postgres no puede derivar tipos de datos de las columnas de destino y es posible que deba agregar conversiones de tipos explícitos. El manual:

Cuando VALUESse utiliza en INSERT, todos los valores se convierten automáticamente en el tipo de datos de la columna de destino correspondiente. Cuando se usa en otros contextos, puede ser necesario especificar el tipo de datos correcto. Si todas las entradas son constantes literales citadas, la coerción de la primera es suficiente para determinar el tipo asumido para todos.

La consulta en sí (sin contar los efectos secundarios) puede ser un poco más costosa para algunos engañados, debido a la sobrecarga del CTE y la adicional SELECT(que debería ser barata ya que el índice perfecto está ahí por definición; se implementa una restricción única con un índice).

Puede ser (mucho) más rápido para muchos duplicados. El costo efectivo de escrituras adicionales depende de muchos factores.

Pero hay menos efectos secundarios y costos ocultos en cualquier caso. Es probablemente más barato en general.

Las secuencias adjuntas todavía están avanzadas, ya que los valores predeterminados se completan antes de probar conflictos.

Sobre los CTE:

Con carga de escritura concurrente

Asumiendo el READ COMMITTEDaislamiento de transacción predeterminado . Relacionado:

La mejor estrategia para defenderse contra las condiciones de carrera depende de los requisitos exactos, el número y el tamaño de las filas en la tabla y en los UPSERT, el número de transacciones concurrentes, la probabilidad de conflictos, los recursos disponibles y otros factores ...

Problema de concurrencia 1

Si una transacción concurrente ha escrito en una fila que su transacción ahora intenta UPSERT, su transacción tiene que esperar a que finalice la otra.

Si la otra transacción finaliza con ROLLBACK(o cualquier error, es decir, automático ROLLBACK), su transacción puede continuar normalmente. Efecto secundario menor posible: brechas en números secuenciales. Pero no faltan filas.

Si la otra transacción finaliza normalmente (implícita o explícita COMMIT), INSERTdetectará un conflicto (el UNIQUEíndice / restricción es absoluto) y DO NOTHING, por lo tanto, tampoco devolverá la fila. (Tampoco puede bloquear la fila como se muestra en el problema de concurrencia 2 a continuación, ya que no es visible ). SELECTVe la misma instantánea desde el inicio de la consulta y tampoco puede devolver la fila aún invisible.

¡Faltan tales filas del conjunto de resultados (aunque existan en la tabla subyacente)!

Esto puede estar bien como está . Especialmente si no está devolviendo filas como en el ejemplo y está satisfecho sabiendo que la fila está allí. Si eso no es lo suficientemente bueno, hay varias formas de evitarlo.

Puede verificar el recuento de filas de la salida y repetir la declaración si no coincide con el recuento de filas de la entrada. Puede ser lo suficientemente bueno para el caso raro. El punto es comenzar una nueva consulta (puede estar en la misma transacción), que luego verá las filas recién confirmadas.

O compruebe si faltan filas de resultados dentro de la misma consulta y sobrescriba aquellas con el truco de fuerza bruta demostrado en la respuesta 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;

Es como la consulta anterior, pero agregamos un paso más con el CTE ups, antes de devolver el completo conjunto de resultados . Ese último CTE no hará nada la mayor parte del tiempo. Solo si faltan filas del resultado devuelto, usamos la fuerza bruta.

Más sobrecarga, todavía. Cuantos más conflictos con las filas preexistentes, es más probable que esto supere el enfoque simple.

Un efecto secundario: el segundo UPSERT escribe filas fuera de servicio, por lo que reintroduce la posibilidad de puntos muertos (ver más abajo) si se superponen tres o más transacciones que escriben en las mismas filas. Si eso es un problema, necesita una solución diferente, como repetir toda la declaración como se mencionó anteriormente.

Problema de concurrencia 2

Si las transacciones simultáneas pueden escribir en las columnas involucradas de las filas afectadas, y debe asegurarse de que las filas que encontró todavía estén allí en una etapa posterior en la misma transacción, puede bloquear las filas existentes a bajo costo en el CTE ins(que de lo contrario quedaría desbloqueado) con:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

Y agregue una cláusula de bloqueo SELECTtambién, comoFOR UPDATE .

Esto hace que las operaciones de escritura de la competencia esperen hasta el final de la transacción, cuando se liberan todos los bloqueos. Así que sé breve.

Más detalles y explicación:

Puntos muertos?

Defiéndete de puntos muertos insertando filas en orden consistente . Ver:

Tipos de datos y conversiones

Tabla existente como plantilla para tipos de datos ...

Las conversiones de tipo explícito para la primera fila de datos en la VALUESexpresión independiente pueden ser inconvenientes. Hay formas de evitarlo. Puede usar cualquier relación existente (tabla, vista, ...) como plantilla de fila. La tabla de destino es la opción obvia para el caso de uso. Los datos de entrada se coaccionan a los tipos apropiados automáticamente, como en la VALUEScláusula de un 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')
   )
   ...

Esto no funciona para algunos tipos de datos. Ver:

... y nombres

Esto también funciona para todos los tipos de datos.

Al insertar en todas las columnas (iniciales) de la tabla, puede omitir los nombres de columna. Suponiendo que la tabla chatsen el ejemplo solo consta de las 3 columnas utilizadas en 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: no use palabras reservadas como "user"como identificador. Esa es una pistola cargada. Utilice identificadores legales, en minúsculas y sin comillas. Lo reemplacé con usr.


2
Usted implica que este método no creará lagunas en las publicaciones en serie, pero son: INSERTAR ... EN CONFLICTO NO HAGA NADA que incremente la publicación en serie cada vez de lo que puedo ver
harmic

1
no es que importe tanto, pero ¿por qué se incrementan las series? y no hay forma de evitar esto?
saliente

1
@salient: como agregué anteriormente: los valores predeterminados de la columna se completan antes de probar conflictos y las secuencias nunca se revierten, para evitar conflictos con las escrituras concurrentes.
Erwin Brandstetter

77
Increíble. Funciona como un encanto y fácil de entender una vez que lo miras con cuidado. Sin embargo, todavía deseo ON CONFLICT SELECT...dónde hay algo :)
Roshambo

3
Increíble. Los creadores de Postgres parecen estar torturando a los usuarios. ¿Por qué no simplemente hacer regresar cláusula siempre valores de retorno, independientemente de si hubo o no inserciones?
Anatoly Alekseev

16

Upsert, ser una extensión de la INSERTconsulta se puede definir con dos comportamientos diferentes en caso de conflicto de restricción: DO NOTHINGo DO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

Tenga en cuenta también que RETURNINGno devuelve nada, porque no se han insertado tuplas . Ahora con DO UPDATE, es posible realizar operaciones en la tupla con la que hay un conflicto. Primero tenga en cuenta que es importante definir una restricción que se utilizará para definir que existe un conflicto.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)

2
Una buena manera de obtener siempre la identificación de la fila afectada y saber si se trata de una inserción o inserción. Justo lo que necesitaba.
Moby Duck

Esto todavía está utilizando la "Actualización", que las desventajas ya se han discutido.
Bill Worthington

4

Para las inserciones de un solo elemento, probablemente usaría una fusión al devolver la identificación:

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

El propósito principal de usar ON CONFLICT DO NOTHINGes evitar errores de lanzamiento, pero no causará retornos de fila. Entonces necesitamos otro SELECTpara obtener la identificación existente.

En este SQL, si falla en conflictos, no devolverá nada, entonces el segundo SELECTobtendrá la fila existente; si se inserta con éxito, habrá dos registros iguales, entonces necesitamos UNIONfusionar el resultado.


¡Esta solución funciona bien y evita realizar una escritura (actualización) innecesaria en la base de datos! ¡Agradable!
Simon C

0

Modifiqué la sorprendente respuesta de Erwin Brandstetter, que no incrementará la secuencia y tampoco bloqueará ninguna fila. Soy relativamente nuevo en PostgreSQL, así que no dude en avisarme si ve algún inconveniente en este método:

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

Esto supone que la tabla chatstiene una restricción única en las columnas.(usr, contact) .

Actualización: se agregaron las revisiones sugeridas de spatar (a continuación). ¡Gracias!


1
En lugar de CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existssolo escribir r.id IS NOT NULL as row_exists. En lugar de WHERE row_exists=FALSEsolo escribir WHERE NOT row_exists.
Spatar
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.