sql >> Base de Datos >  >> RDS >> PostgreSQL

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

La respuesta actualmente aceptada parece estar bien para un solo objetivo de conflicto, pocos conflictos, tuplas pequeñas y sin desencadenantes. Evita el problema de concurrencia 1 (ver 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 actualizar filas idénticas sin necesidad. Incluso si no ve ninguna diferencia en la superficie, hay varios efectos secundarios :

  • Podría activar activadores que no deberían activarse.

  • Bloquea las filas "inocentes" contra escritura, lo que posiblemente incurra en costos por transacciones concurrentes.

  • Puede hacer que la fila parezca nueva, aunque es antigua (marca de tiempo de la 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 propio UPSERT, aumento de tabla, aumento de índice, penalización de rendimiento para operaciones posteriores en la tabla, VACUUM costo. Un efecto menor para unos pocos duplicados, pero masivo para la mayoría de los engañados.

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

Para ON CONFLICT DO UPDATE , un conflict_target debe proporcionarse.

Un soltero El "objetivo de conflicto" no es posible si hay varios índices/restricciones involucrados. Pero aquí hay una solución relacionada para múltiples índices parciales:

  • UPSERT basado en restricción ÚNICA con valores NULL

Volviendo al tema, 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 capturar todos posibles conflictos que puedan surgir, que pueden o no ser deseables.

Sin carga de escritura 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

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

Los últimos JOIN chats funciona porque las filas recién insertadas de una CTE de modificación de datos adjunta aún no están 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 los VALUES expresió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ícitas. El manual:

Cuando VALUES se usa en INSERT , todos los valores se coaccionan automáticamente al 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 entre comillas, forzar la primera es suficiente para determinar el tipo asumido para todas.

La consulta en sí (sin contar los efectos secundarios) puede ser un poco más costosa para pocos engañados, debido a la sobrecarga del CTE y el SELECT adicional (que debería ser barato 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 todo caso. Probablemente sea más barato en general.

Las secuencias adjuntas aún son avanzadas, ya que los valores predeterminados se completan antes pruebas de conflictos.

Acerca de las CTE:

  • ¿Son las consultas de tipo SELECT el único tipo que se puede anidar?
  • Deduplicar sentencias SELECT en división relacional

Con carga de escritura simultánea

Suponiendo READ COMMITTED predeterminada aislamiento de transacciones. Relacionado:

  • Las transacciones simultáneas dan como resultado una condición de carrera con una restricción única en la inserción

La mejor estrategia para defenderse de las condiciones de carrera depende de los requisitos exactos, la cantidad y el tamaño de las filas en la tabla y en los UPSERT, la cantidad de transacciones simultáneas, la probabilidad de conflictos, los recursos disponibles y otros factores...

Problema de simultaneidad 1

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

Si la otra transacción termina con ROLLBACK (o cualquier error, es decir, ROLLBACK automático ), su transacción puede proceder normalmente. Posible efecto secundario menor:lagunas en los números secuenciales. Pero no faltan filas.

Si la otra transacción termina normalmente (implícito o explícito COMMIT ), su INSERT detectará un conflicto (el UNIQUE índice / restricción es absoluta) y DO NOTHING , por lo tanto, tampoco devuelve la fila. (Tampoco se puede bloquear la fila como se muestra en problema de concurrencia 2 a continuación, ya que no es visible .) El SELECT ve la misma instantánea desde el inicio de la consulta y tampoco puede devolver la fila aún invisible.

¡Faltan filas de este tipo en el conjunto de resultados (aunque existan en la tabla subyacente)!

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

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

O comprobar si faltan filas de resultados dentro la misma consulta y sobrescribir aquellos con el truco de la 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 los ups de CTE , antes de devolver el completo conjunto resultante. Ese último CTE no hará nada la mayor parte del tiempo. Solo si faltan filas en el resultado devuelto, usamos la fuerza bruta.

Más gastos generales, todavía. Cuantos más conflictos haya con las filas preexistentes, más probable será que esto supere el enfoque simple.

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

Problema de simultaneidad 2

Si las transacciones concurrentes 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 barato en el CTE ins (que de otro modo irí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 al SELECT también, como FOR UPDATE .

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

Más detalles y explicación:

  • Cómo incluir filas excluidas en RETURNING from INSERT... ON CONFLICT
  • ¿Es SELECCIONAR o INSERTAR en una función propensa a condiciones de carrera?

¿Interbloqueos?

Defiéndete de interbloqueos insertando filas en orden coherente . Ver:

  • Punto muerto con INSERCIONES de varias filas a pesar de EN CONFLICTO NO HACER NADA

Tipos de datos y conversiones

Tabla existente como plantilla para tipos de datos...

Conversiones de tipo explícitas para la primera fila de datos en el VALUES independiente la expresión puede ser inconveniente. Hay maneras 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 automáticamente a los tipos apropiados, como en VALUES clá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:

  • Conversión de tipo NULL al actualizar varias filas

... y nombres

Esto también funciona para todos tipos de datos.

Al insertar en todas las columnas (principales) de la tabla, puede omitir los nombres de las columnas. Suponiendo tabla chats en el ejemplo solo consta de las 3 columnas utilizadas en el 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 .