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

¿Cómo actualizar todas las columnas con INSERTAR... EN CONFLICTO...?

La UPDATE sintaxis requiere para nombrar explícitamente las columnas de destino. Posibles razones para evitarlo:

  • Tiene muchas columnas y solo desea acortar la sintaxis.
  • No sabes nombres de columna excepto las columnas únicas.

"All columns" tiene que significar "todas las columnas de la tabla de destino" (o al menos "columnas principales de la tabla" ) en orden coincidente y tipo de datos coincidente. De lo contrario, tendría que proporcionar una lista de nombres de columnas de destino de todos modos.

Tabla de prueba:

CREATE TABLE tbl (
   id    int PRIMARY KEY
 , text  text
 , extra text
);

INSERT INTO tbl AS t
VALUES (1, 'foo')
     , (2, 'bar');

1. DELETE &INSERT en una sola consulta en su lugar

Sin conocer ningún nombre de columna excepto id .

Solo funciona para "todas las columnas de la tabla de destino" . Si bien la sintaxis incluso funciona para un subconjunto principal, las columnas sobrantes en la tabla de destino se restablecerían a NULL con DELETE y INSERT .

UPSERT (INSERT ... ON CONFLICT ... ) es necesario para evitar problemas de simultaneidad/bloqueo bajo carga de escritura simultánea, y solo porque no existe una forma general de bloquear filas que aún no existen en Postgres (bloqueo de valor ).

Su requisito especial solo afecta a la UPDATE parte. Las posibles complicaciones no se aplican donde existen las filas se ven afectadas. Esos están bloqueados correctamente. Simplificando un poco más, puede reducir su caso a DELETE y INSERT :

WITH data(id) AS (              -- Only 1st column gets explicit name!
   VALUES
      (1, 'foo_upd', 'a')       -- changed
    , (2, 'bar', 'b')           -- unchanged
    , (3, 'baz', 'c')           -- new
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data d
   WHERE  t.id = d.id
   -- AND    t <> d              -- optional, to avoid empty updates
   )                             -- only works for complete rows
INSERT INTO tbl AS t
TABLE  data                      -- short for: SELECT * FROM data
ON     CONFLICT (id) DO NOTHING
RETURNING t.id;

En el modelo MVCC de Postgres, un UPDATE es básicamente lo mismo que DELETE y INSERT de todos modos (excepto en algunos casos de esquina con concurrencia, actualizaciones HOT y valores de columnas grandes almacenados fuera de línea). Como desea reemplazar todas las filas de todos modos, simplemente elimine las filas en conflicto antes de INSERT . Las filas eliminadas permanecen bloqueadas hasta que se confirma la transacción. El INSERT solo puede encontrar filas en conflicto para valores clave que no existían anteriormente si una transacción simultánea los inserta simultáneamente (después de DELETE , pero antes de INSERT ).

En este caso especial, perdería valores de columna adicionales para las filas afectadas. No se planteó ninguna excepción. Pero si las consultas de la competencia tienen la misma prioridad, eso no es un problema:la otra consulta ganó por algunas filas Además, si la otra consulta es un UPSERT similar, su alternativa es esperar a que esta transacción se confirme y luego se actualice de inmediato. "Ganar" podría ser una victoria pírrica.

Acerca de las "actualizaciones vacías":

  • ¿Cómo puedo (o puedo) SELECCIONAR DISTINTO en varias columnas?

¡No, mi consulta debe ganar!

Vale, tú lo pediste:

WITH data(id) AS (                   -- Only 1st column gets explicit name!
   VALUES                            -- rest gets default names "column2", etc.
     (1, 'foo_upd', NULL)              -- changed
   , (2, 'bar', NULL)                  -- unchanged
   , (3, 'baz', NULL)                  -- new
   , (4, 'baz', NULL)                  -- new
   )
, ups AS (
   INSERT INTO tbl AS t
   TABLE  data                       -- short for: SELECT * FROM data
   ON     CONFLICT (id) DO UPDATE
   SET    id = t.id
   WHERE  false                      -- never executed, but locks the row!
   RETURNING t.id
   )
, del AS (
   DELETE FROM tbl AS t
   USING  data     d
   LEFT   JOIN ups u USING (id)
   WHERE  u.id IS NULL               -- not inserted !
   AND    t.id = d.id
   -- AND    t <> d                  -- avoid empty updates - only for full rows
   RETURNING t.id
   )
, ins AS (
   INSERT INTO tbl AS t
   SELECT *
   FROM   data
   JOIN   del USING (id)             -- conflict impossible!
   RETURNING id
   )
SELECT ARRAY(TABLE ups) AS inserted  -- with UPSERT
     , ARRAY(TABLE ins) AS updated   -- with DELETE & INSERT;

¿Cómo?

  • Los data del 1er CTE solo proporciona datos. Podría ser una mesa en su lugar.
  • El 2º CTE ups :UPSERT. Filas con id en conflicto no se modifican, pero también bloquean .
  • El 3er CTE del elimina las filas en conflicto. Permanecen bloqueados.
  • El 4º CTE ins inserta filas completas . Solo permitido para la misma transacción
  • El SELECT final es solo para que la demostración muestre lo que sucedió.

Para verificar si hay actualizaciones vacías, haga una prueba (antes y después) con:

SELECT ctid, * FROM tbl; -- did the ctid change?

La comprobación (comentada) de cualquier cambio en la fila AND t <> d funciona incluso con valores NULL porque estamos comparando dos valores de fila escritos de acuerdo con el manual:

dos valores de campo NULL se consideran iguales y un NULL se considera mayor que uno que no es NULL

2. SQL dinámico

Esto también funciona para un subconjunto de columnas iniciales, conservando los valores existentes.

El truco es dejar que Postgres construya la cadena de consulta con los nombres de las columnas de los catálogos del sistema de forma dinámica y luego ejecutarla.

Ver respuestas relacionadas para el código:

  • Actualice varias columnas en una función de activación en plpgsql

  • Actualización masiva de todas las columnas

  • SQL actualiza campos de una tabla a partir de campos de otra