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
oIMMUTABLE
expresiones predeterminadas . MásVOLATILE
funciones funcionarán igual de bien, pero no hay garantías. Elcurrent_timestamp
familia de funciones califica como estable, ya que sus valores no cambian dentro de una transacción.
En particular, esto tiene efectos secundarios enserial
columnas (o cualquier otro dibujo predeterminado de una secuencia). Pero eso no debería ser un problema, porque normalmente no escribes enserial
columnas directamente. Esos no deberían estar listados enINSERT
declaraciones en absoluto.
Defecto restante paraserial
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 enserial
columnas.
Se pueden resolver dos problemas más:
-
Si tiene columnas definidas
NOT NULL
, debe insertar valores ficticios y reemplazarlos conNULL
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 laUPDATE
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.