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

Genere valores DEFAULT en un CTE UPSERT usando PostgreSQL 9.3

Postgres 9.5 implementó UPSERT . Ver más abajo.

Postgres 9.4 o anterior

Este es un problema complicado. Te encuentras con esta restricción (según la documentación):

En un VALUES lista que aparece en el nivel superior de un INSERT , una expresión se puede reemplazar por DEFAULT para indicar que se debe insertar el valor predeterminado de la columna de destino. DEFAULT no se puede usar cuando VALUES aparece en otros contextos.

Énfasis en negrita mío. Los valores predeterminados no se definen sin una tabla para insertar. Entonces no hay directo solución a su pregunta, pero hay varias rutas alternativas posibles, dependiendo de los requisitos exactos .

¿Obtener valores predeterminados del catálogo del sistema?

podrías recuperarlos del catálogo del sistema pg_attrdef como comentó @Patrick o desde information_schema.columns . Complete las instrucciones aquí:

  • ¿Obtener los valores predeterminados de las columnas de la tabla en Postgres?

Pero entonces todavía solo tiene una lista de filas con una representación de texto de la expresión para cocinar el valor predeterminado. Tendría que construir y ejecutar sentencias dinámicamente para obtener valores con los que trabajar. Tedioso y desordenado. En su lugar, podemos dejar que la función integrada de Postgres lo haga por nosotros :

Atajo sencillo

Inserte una fila ficticia y haga que vuelva a usar los valores predeterminados generados:

INSERT INTO playlist_items DEFAULT VALUES RETURNING *;

Problemas / alcance de la solución

  • Esto solo se garantiza que funcione para STABLE o IMMUTABLE expresiones predeterminadas . Más VOLATILE funciones funcionarán igual de bien, pero no hay garantías. El current_timestamp familia de funciones califica como estable, ya que sus valores no cambian dentro de una transacción.
    En particular, esto tiene efectos secundarios en serial columnas (o cualquier otro dibujo predeterminado de una secuencia). Pero eso no debería ser un problema, porque normalmente no escribes en serial columnas directamente. Esos no deberían estar listados en INSERT declaraciones en absoluto.
    Defecto restante para serial columnas:la secuencia sigue avanzando por la única llamada para obtener una fila predeterminada, lo que produce un espacio en la numeración. Nuevamente, eso no debería ser un problema, porque las brechas generalmente son de esperar en serial columnas.

Se pueden resolver dos problemas más:

  • Si tiene columnas definidas NOT NULL , debe insertar valores ficticios y reemplazarlos con NULL en el resultado.

  • En realidad, no queremos insertar la fila ficticia . Podríamos eliminar más tarde (en la misma transacción), pero eso puede tener más efectos secundarios, como activadores ON DELETE . Hay una mejor manera:

Evite fila ficticia

Clonar una tabla temporal incluyendo valores predeterminados de columna e insertar en eso :

BEGIN;
CREATE TEMP TABLE tmp_playlist_items (LIKE playlist_items INCLUDING DEFAULTS)
   ON COMMIT DROP;  -- drop at end of transaction

INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *;
...

Mismo resultado, menos efectos secundarios. Dado que las expresiones predeterminadas se copian palabra por palabra, el clon se basa en las mismas secuencias, si las hay. Pero otros efectos secundarios de la fila o desencadenantes no deseados se evitan por completo.

Crédito a Igor por la idea:

  • Postgresql, seleccione una fila "falsa"

Eliminar NOT NULL restricciones

Tendría que proporcionar valores ficticios para NOT NULL columnas, porque (por documentación):

Las restricciones no nulas siempre se copian en la nueva tabla.

Cualquiera acomodar para aquellos en el INSERT declaración o (mejor) eliminar las restricciones:

ALTER TABLE tmp_playlist_items
   ALTER COLUMN foo DROP NOT NULL
 , ALTER COLUMN bar DROP NOT NULL;

Hay una manera rápida y sucia con privilegios de superusuario:

UPDATE pg_attribute
SET    attnotnull = FALSE
WHERE  attrelid = 'tmp_playlist_items'::regclass
AND    attnotnull
AND    attnum > 0;

Es solo una tabla temporal sin datos y sin otro propósito, y se descarta al final de la transacción. Así que el atajo es tentador. Aún así, la regla básica es:nunca manipule directamente los catálogos del sistema.

Entonces, busquemos una manera limpia :Automatizar con SQL dinámico en un DO declaración. Solo necesita los privilegios normales está garantizado que tendrá desde que el mismo rol creó la tabla temporal.

DO $$BEGIN
EXECUTE (
   SELECT 'ALTER TABLE tmp_playlist_items ALTER '
       || string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
       || ' DROP NOT NULL'
   FROM   pg_catalog.pg_attribute
   WHERE  attrelid = 'tmp_playlist_items'::regclass
   AND    attnotnull
   AND    attnum > 0
   );
END$$

Mucho más limpio y aún muy rápido. Ejecute con cuidado los comandos dinámicos y desconfíe de la inyección SQL. Esta declaración es segura. He publicado varias respuestas relacionadas con más explicaciones.

Solución general (9.4 y anteriores)

BEGIN;

CREATE TEMP TABLE tmp_playlist_items
   (LIKE playlist_items INCLUDING DEFAULTS) ON COMMIT DROP;

DO $$BEGIN
EXECUTE (
   SELECT 'ALTER TABLE tmp_playlist_items ALTER '
       || string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
       || ' DROP NOT NULL'
   FROM   pg_catalog.pg_attribute
   WHERE  attrelid = 'tmp_playlist_items'::regclass
   AND    attnotnull
   AND    attnum > 0
   );
END$$;

LOCK TABLE playlist_items IN EXCLUSIVE MODE;  -- forbid concurrent writes

WITH default_row AS (
   INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *
   )
, new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
   VALUES
      (651, 21, 30012, 'a', 30, 1, FALSE)
    , (NULL, 21, 1, 'b', 34, 2, NULL)
    , (668, 21, 30012, 'c', 30, 3, FALSE)
    , (7428, 21, 23068, 'd', 0, 4, FALSE)
   )
, upsert AS (  -- *not* replacing existing values in UPDATE (?)
   UPDATE playlist_items m
   SET   (  playlist,   item,   group_name,   duration,   sort,   legacy)
       = (n.playlist, n.item, n.group_name, n.duration, n.sort, n.legacy)
   --                                   ..., COALESCE(n.legacy, m.legacy)  -- see below
   FROM   new_values n
   WHERE  n.id = m.id
   RETURNING m.id
   )
INSERT INTO playlist_items
        (playlist,   item,   group_name,   duration,   sort, legacy)
SELECT n.playlist, n.item, n.group_name, n.duration, n.sort
                                   , COALESCE(n.legacy, d.legacy)
FROM   new_values n, default_row d   -- single row can be cross-joined
WHERE  NOT EXISTS (SELECT 1 FROM upsert u WHERE u.id = n.id)
RETURNING id;

COMMIT;

Solo necesitas el LOCK si tiene transacciones simultáneas que intentan escribir en la misma tabla.

Según lo solicitado, esto solo reemplaza los valores NULL en la columna legacy en las filas de entrada para INSERT caso. Se puede ampliar fácilmente para trabajar con otras columnas o en UPDATE caso también. Por ejemplo, podría UPDATE condicionalmente también:solo si el valor de entrada es NOT NULL . Agregué una línea comentada a UPDATE arriba.

Aparte:no es necesario lanzar valores en cualquier fila excepto el primero en un VALUES expresión, ya que los tipos se derivan del primero fila.

Postgres 9.5

implementa UPSERT con INSERT .. ON CONFLICT .. DO NOTHING | UPDATE . Esto simplifica en gran medida la operación:

INSERT INTO playlist_items AS m (id, playlist, item, group_name, duration, sort, legacy)
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
,      (DEFAULT, 21, 1, 'b', 34, 2, DEFAULT)  -- !
,      (668, 21, 30012, 'c', 30, 3, FALSE)
,      (7428, 21, 23068, 'd', 0, 4, FALSE)
ON CONFLICT (id) DO UPDATE
SET (playlist, item, group_name, duration, sort, legacy)
 = (EXCLUDED.playlist, EXCLUDED.item, EXCLUDED.group_name
  , EXCLUDED.duration, EXCLUDED.sort, EXCLUDED.legacy)
-- (...,  COALESCE(l.legacy, EXCLUDED.legacy))  -- see below
RETURNING m.id;

Podemos adjuntar los VALUES cláusula a INSERT directamente, lo que permite el DEFAULT palabra clave. En el caso de infracciones únicas en (id) , las actualizaciones de Postgres en su lugar. Podemos usar filas excluidas en UPDATE . El manual:

El SET y WHERE cláusulas en ON CONFLICT DO UPDATE tener acceso a la fila existente usando el nombre de la tabla (o un alias), y a las filas propuestas para la inserción usando el excluded especial mesa.

Y:

Tenga en cuenta que los efectos de todos los BEFORE INSERT por fila los disparadores se reflejan en los valores excluidos, ya que esos efectos pueden haber contribuido a que la fila se excluya de la inserción.

Caja de esquina restante

Tiene varias opciones para UPDATE :Puedes...

  • ... no actualizar en absoluto:agregue un WHERE cláusula a la UPDATE para escribir solo en las filas seleccionadas.
  • ... solo actualizar las columnas seleccionadas.
  • ... actualizar solo si la columna actualmente es NULL:COALESCE(l.legacy, EXCLUDED.legacy)
  • ... actualizar solo si el nuevo valor es NOT NULL :COALESCE(EXCLUDED.legacy, l.legacy)

Pero no hay forma de discernir DEFAULT valores y valores realmente proporcionados en el INSERT . Solo los EXCLUDED resultantes las filas son visibles. Si necesita la distinción, recurra a la solución anterior, donde tiene ambas a nuestra disposición.