PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Générer des valeurs DEFAULT dans un CTE UPSERT à l'aide de PostgreSQL 9.3

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'un INSERT , uneexpression peut être remplacée par DEFAULT pour indiquer que la valeur par défaut de la colonne de destination doit être insérée. DEFAULT ne peut pas être utilisé lorsque VALUES 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 ou IMMUTABLE expressions par défaut . La plupart des VOLATILE fonctionneront aussi bien, mais il n'y a aucune garantie. Le current_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 sur serial 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 sur serial colonnes directement. Ceux-ci ne doivent pas être répertoriés dans INSERT du tout.
    Défaut restant pour serial 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 en serial 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 par NULL 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 et WHERE clauses dans ON 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écial excluded 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 à la UPDATE 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.