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

Cómo incluir filas excluidas en RETURNING from INSERT... ON CONFLICT

El error que obtienes:

El comando ON CONFLICT DO UPDATE no puede afectar la fila por segunda vez

indica que está intentando cambiar la misma fila más de una vez en un solo comando. En otras palabras:tiene duplicados en (name, url, email) en tus VALUES lista. Doble los duplicados (si esa es una opción) y debería funcionar. Pero tendrá que decidir qué fila elegir de cada conjunto de duplicados.

INSERT INTO feeds_person (created, modified, name, url, email)
SELECT DISTINCT ON (name, url, email) *
FROM  (
   VALUES
   ('blah', 'blah', 'blah', 'blah', 'blah')
   -- ... more
   ) v(created, modified, name, url, email)  -- match column list
ON     CONFLICT (name, url, email) DO UPDATE
SET    url = feeds_person.url
RETURNING id;

Dado que usamos un VALUES independiente expresión ahora, debe agregar conversiones de tipo explícitas para tipos no predeterminados. Me gusta:

VALUES
    (timestamptz '2016-03-12 02:47:56+01'
   , timestamptz '2016-03-12 02:47:56+01'
   , 'n3', 'u3', 'e3')
   ...

Tu timestamptz las columnas necesitan una conversión de tipo explícita, mientras que los tipos de cadena pueden operar con text predeterminado . (Aún podría enviar a varchar(n) inmediatamente).

Hay formas de determinar qué fila elegir de cada conjunto de duplicados:

  • ¿Seleccionar la primera fila en cada grupo GROUP BY?

Tiene razón, no hay (actualmente) ninguna forma de ser excluido filas en RETURNING cláusula. Cito la wiki de Postgres:

Tenga en cuenta que RETURNING no hace visible el "EXCLUDED.* " alias de UPDATE (solo el genérico "TARGET.* " alias es visible allí). Se cree que hacerlo crea una ambigüedad molesta para los casos simples y comunes [30] con poco o ningún beneficio. En algún momento en el futuro, podemos buscar una forma de exponer si RETURNING -las tuplas proyectadas se insertaron y actualizaron, pero esto probablemente no necesite convertirse en la primera iteración confirmada de la característica [31].

Sin embargo , no debería estar actualizando filas que no deberían actualizarse. Las actualizaciones vacías son casi tan caras como las regulares y pueden tener efectos secundarios no deseados. Para empezar, no necesita estrictamente UPSERT, su caso se parece más a "SELECCIONAR o INSERTAR". Relacionado:

  • ¿Es SELECCIONAR o INSERTAR en una función propensa a condiciones de carrera?

Uno una forma más limpia de insertar un conjunto de filas sería con CTE de modificación de datos:

WITH val AS (
   SELECT DISTINCT ON (name, url, email) *
   FROM  (
      VALUES 
      (timestamptz '2016-1-1 0:0+1', timestamptz '2016-1-1 0:0+1', 'n', 'u', 'e')
    , ('2016-03-12 02:47:56+01', '2016-03-12 02:47:56+01', 'n1', 'u3', 'e3')
      -- more (type cast only needed in 1st row)
      ) v(created, modified, name, url, email)
   )
, ins AS (
   INSERT INTO feeds_person (created, modified, name, url, email)
   SELECT created, modified, name, url, email FROM val
   ON     CONFLICT (name, url, email) DO NOTHING
   RETURNING id, name, url, email
   )
SELECT 'inserted' AS how, id FROM ins  -- inserted
UNION  ALL
SELECT 'selected' AS how, f.id         -- not inserted
FROM   val v
JOIN   feeds_person f USING (name, url, email);

La complejidad añadida debería pagar por tablas grandes donde INSERT es la regla y SELECT la excepción.

Originalmente, había agregado un NOT EXISTS predicado en el último SELECT para evitar duplicados en el resultado. Pero eso era redundante. Todas las CTE de una sola consulta ven las mismas instantáneas de las tablas. El conjunto devolvió con ON CONFLICT (name, url, email) DO NOTHING es mutuamente excluyente con el conjunto devuelto después de INNER JOIN en las mismas columnas.

Desafortunadamente, esto también abre una pequeña ventana para una condición de carrera . Si...

  • una transacción concurrente inserta filas en conflicto
  • no se ha comprometido todavía
  • pero finalmente se compromete

... algunas filas pueden perderse.

Podría simplemente INSERT .. ON CONFLICT DO NOTHING , seguido de un SELECT separado consulta para todas las filas, dentro de la misma transacción para superar esto. Lo que a su vez abre otra pequeña ventana para una condición de carrera si las transacciones simultáneas pueden realizar escrituras en la tabla entre INSERT y SELECT (por defecto READ COMMITTED nivel de aislamiento). Se puede evitar con REPEATABLE READ aislamiento de transacciones (o más estricto). O con un bloqueo de escritura (posiblemente costoso o incluso inaceptable) en toda la tabla. Puede obtener cualquier comportamiento que necesite, pero puede haber un precio que pagar.

Relacionado:

  • ¿Cómo usar RETURNING con ON CONFLICT en PostgreSQL?
  • Retornar filas desde INSERT con ON CONFLICT sin necesidad de actualizar