Postgres 9.5 a implémenté UPSERT
. Voir ci-dessous.
Postgres 9.4 ou version antérieure
C'est un problème délicat. Vous rencontrez cette restriction (selon la documentation) :
Dans un
VALUES
liste apparaissant au niveau supérieur d'unINSERT
, uneexpression peut être remplacée parDEFAULT
pour indiquer que la valeur par défaut de la colonne de destination doit être insérée.DEFAULT
ne peut pas être utilisé lorsqueVALUES
apparaît dans d'autres contextes.
Bold emphase mienne. Les valeurs par défaut ne sont pas définies sans un tableau dans lequel les insérer. Il n'y a donc pas de direct solution à votre question, mais il existe un certain nombre de itinéraires alternatifs possibles, en fonction des besoins exacts .
Récupérer les valeurs par défaut du catalogue système ?
Vous pourriez récupérer ceux du catalogue système pg_attrdef
comme @Patrick a commenté ou de information_schema.columns
. Instructions complètes ici :
- Obtenir les valeurs par défaut des colonnes de table dans Postgres ?
Mais alors vous toujours n'avoir qu'une liste de lignes avec une représentation textuelle de l'expression pour cuisiner la valeur par défaut. Vous devrez créer et exécuter des instructions de manière dynamique pour obtenir des valeurs avec lesquelles travailler. Ennuyeux et désordonné. Au lieu de cela, nous pouvons laisser la fonctionnalité Postgres intégrée le faire pour nous :
Raccourci simple
Insérez une ligne factice et renvoyez-la pour utiliser les valeurs par défaut générées :
INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Problèmes/portée de la solution
- Ceci n'est garanti que pour
STABLE
ouIMMUTABLE
expressions par défaut . La plupart desVOLATILE
fonctionneront aussi bien, mais il n'y a aucune garantie. Lecurrent_timestamp
famille de fonctions est qualifiée de stable, car leurs valeurs ne changent pas au sein d'une transaction.
En particulier, cela a des effets secondaires surserial
colonnes (ou toute autre valeur par défaut tirée d'une séquence). Mais cela ne devrait pas être un problème, car vous n'écrivez normalement pas surserial
colonnes directement. Ceux-ci ne doivent pas être répertoriés dansINSERT
du tout.
Défaut restant pourserial
colonnes :la séquence est toujours avancée par l'appel unique pour obtenir une ligne par défaut, produisant un écart dans la numérotation. Encore une fois, cela ne devrait pas poser de problème, car des écarts sont généralement à prévoir enserial
colonnes.
Deux autres problèmes peuvent être résolus :
-
Si vous avez des colonnes définies
NOT NULL
, vous devez insérer des valeurs factices et les remplacer parNULL
dans le résultat. -
Nous ne voulons pas réellement insérer la ligne factice . Nous pourrions supprimer plus tard (dans la même transaction), mais cela peut avoir plus d'effets secondaires, comme les déclencheurs
ON DELETE
. Il existe un meilleur moyen :
Éviter les lignes factices
Cloner une table temporaire y compris les valeurs par défaut des colonnes et insérer dans cela :
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 *;
...
Même résultat, moins d'effets secondaires. Étant donné que les expressions par défaut sont copiées textuellement, le clone puise dans les mêmes séquences, le cas échéant. Mais d'autres effets secondaires de la rangée ou des déclencheurs indésirables sont complètement évités.
Crédit à Igor pour l'idée :
- Postgresql, sélectionnez une "fausse" ligne
Supprimer NOT NULL
contraintes
Vous devrez fournir des valeurs factices pour NOT NULL
colonnes, car (selon la documentation) :
Les contraintes non nulles sont toujours copiées dans la nouvelle table.
Soit accueillir pour ceux dans le INSERT
déclaration ou (mieux) éliminer les contraintes :
ALTER TABLE tmp_playlist_items
ALTER COLUMN foo DROP NOT NULL
, ALTER COLUMN bar DROP NOT NULL;
Il existe un moyen rapide et sale avec des privilèges de superutilisateur :
UPDATE pg_attribute
SET attnotnull = FALSE
WHERE attrelid = 'tmp_playlist_items'::regclass
AND attnotnull
AND attnum > 0;
C'est juste une table temporaire sans données et sans autre but, et elle est supprimée à la fin de la transaction. Alors le raccourci est tentant. Néanmoins, la règle de base est la suivante :ne modifiez jamais directement les catalogues système.
Alors, examinons une manière propre :Automatiser avec SQL dynamique dans un DO
déclaration. Vous avez juste besoin des privilèges normaux vous êtes assuré d'avoir puisque le même rôle a créé la table temporaire.
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$$
Beaucoup plus propre et toujours très rapide. Soyez prudent avec les commandes dynamiques et méfiez-vous des injections SQL. Cette déclaration est sans danger. J'ai posté plusieurs réponses connexes avec plus d'explications.
Solution générale (9.4 et versions antérieures)
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;
Vous n'avez besoin que du LOCK
si vous avez des transactions simultanées essayant d'écrire dans la même table.
Comme demandé, cela ne remplace que les valeurs NULL dans la colonne legacy
dans les lignes d'entrée pour le INSERT
Cas. Peut facilement être étendu pour fonctionner avec d'autres colonnes ou dans la UPDATE
cas aussi. Par exemple, vous pouvez UPDATE
conditionnellement également :uniquement si la valeur d'entrée est NOT NULL
. J'ai ajouté une ligne commentée à la UPDATE
ci-dessus.
À part :vous n'avez pas besoin de lancer valeurs dans n'importe quelle ligne sauf la première dans un VALUES
expression, puisque les types sont dérivés du premier rangée.
Postgres 9.5
implémente UPSERT avec INSERT .. ON CONFLICT .. DO NOTHING | UPDATE
. Cela simplifie grandement l'opération :
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;
Nous pouvons joindre les VALUES
clause à INSERT
directement, ce qui permet au DEFAULT
mot-clé. En cas de violations uniques sur (id)
, Postgres met à jour à la place. Nous pouvons utiliser des lignes exclues dans le UPDATE
. Le manuel :
Le
SET
etWHERE
clauses dansON CONFLICT DO UPDATE
avoir accès à la ligne existante en utilisant le nom de la table (ou un alias), et aux lignes proposées pour insertion en utilisant le spécialexcluded
tableau.
Et :
Notez que les effets de tous les
BEFORE INSERT
par ligne les déclencheurs sont reflétés dans les valeurs exclues, car ces effets peuvent avoir contribué à l'exclusion de la ligne de l'insertion.
Cas d'angle restant
Vous avez différentes options pour la UPDATE
:Vous pouvez ...
- ... pas mis à jour du tout :ajoutez un
WHERE
clause à laUPDATE
pour n'écrire que dans les lignes sélectionnées. - ... ne mettre à jour que les colonnes sélectionnées.
- ... ne mettre à jour que si la colonne est actuellement NULL :
COALESCE(l.legacy, EXCLUDED.legacy)
- ... mettre à jour uniquement si la nouvelle valeur est
NOT NULL
:COALESCE(EXCLUDED.legacy, l.legacy)
Mais il n'y a aucun moyen de discerner DEFAULT
les valeurs et les valeurs réellement fournies dans le INSERT
. Uniquement EXCLUDED
résultant les lignes sont visibles. Si vous avez besoin de la distinction, revenez à la solution précédente, où vous avez les deux à notre disposition.