web-dev-qa-db-fra.com

Ligne en double avec clé primaire dans PostgreSQL

Supposons que j'ai une table comme suit nommée people, où id est une clé primaire:

+-----------+---------+---------+
|  id       |  fname  |  lname  |
| (integer) | (text)  | (text)  |
+===========+=========+=========+
|  1        | Daniel  | Edwards |
|  2        | Fred    | Holt    |
|  3        | Henry   | Smith   |
+-----------+---------+---------+

J'essaie d'écrire une requête de duplication de lignes qui est suffisamment robuste pour prendre en compte les modifications de schéma de la table. Chaque fois que j'ajoute une colonne à la table, je ne veux pas avoir à revenir en arrière et modifier la requête de duplication.

Je sais que je peux le faire, ce qui dupliquera l'identifiant d'enregistrement 2 et donnera à l'enregistrement dupliqué un nouvel identifiant:

INSERT INTO people (fname, lname) SELECT fname, lname FROM people WHERE id = 2;

Cependant, si j'ajoute une colonne age, je devrai modifier la requête pour tenir également compte de la colonne age.

Évidemment, je ne peux pas faire ce qui suit, car cela dupliquera également la clé primaire, résultant en un duplicate key value violates unique constraint - Et, je ne veux pas qu'ils partagent le même identifiant de toute façon:

INSERT INTO people SELECT * FROM people WHERE id = 2

Cela dit, quelle serait une approche raisonnable pour résoudre ce problème? Je préférerais rester loin des procédures stockées, mais je ne suis pas à 100% contre, je suppose ...

7
Joshua Burns

Simple avec hstore

Si vous avez le module supplémentaire hstore installé ( instructions dans le lien ci-dessous ), il y a un étonnamment simple façon de remplacer la ou les valeurs de champ (s) individuel (s) sans rien savoir des autres colonnes:

Exemple de base: dupliquez la ligne avec id = 2 Mais remplacez 2 Par 3:

INSERT INTO people
SELECT (p #= hstore('id', '3')).* FROM people p WHERE id = 2;

Détails:

En supposant (puisque ce n'est pas défini dans la question) que people.id Est un serial colonne avec une séquence attachée, vous voudrez la prochaine valeur de la séquence. Nous pouvons déterminer le nom de la séquence avec pg_get_serial_sequence(). Détails:

Ou vous pouvez simplement coder en dur le nom de la séquence si cela ne change pas.
Nous le ferions avons cette requête:

INSERT INTO people
SELECT (p #= hstore('id', nextval(pg_get_serial_sequence('people', 'id'))::text)).*
FROM people p WHERE id = 2;

Ce qui fonctionne, mais souffre d'une faiblesse dans le planificateur de requêtes Postgres: l'expression est évaluée séparément pour chaque colonne de la ligne, ce qui gaspille les numéros de séquence et les performances. Pour éviter cela, déplacez l'expression dans une sous-requête et décomposez la ligne une fois uniquement:

INSERT INTO people
SELECT (p1).*
FROM  (
   SELECT p #= hstore('id', nextval(pg_get_serial_sequence('people', 'id'))::text) AS p1
   FROM   people p WHERE id = 2
   ) sub;

Probablement le plus rapide pour une (ou quelques) rangée (s) à la fois.

json/jsonb

Si vous n'avez pas hstore installé et ne pouvez pas installer de modules supplémentaires, vous pouvez faire une astuce similaire avec json_populate_record() ou jsonb_populate_record(), mais cette capacité n'est pas documentée et peut ne pas être fiable.

Table temporaire transitoire

Une autre solution simple serait d'utiliser un transitoire temporaire comme celui-ci:

BEGIN;
CREATE TEMP TABLE people_tmp ON COMMIT DROP AS
SELECT * FROM people WHERE id = 2;
UPDATE people_tmp SET id = nextval(pg_get_serial_sequence('people', 'id'));
INSERT INTO people TABLE people_tmp;
COMMIT;

J'ai ajouté ON COMMIT DROP Pour supprimer automatiquement le tableau à la fin de la transaction. Par conséquent, j'ai également encapsulé l'opération dans une transaction qui lui est propre. Ni l'un ni l'autre n'est strictement nécessaire.

Cela offre un large éventail d'options supplémentaires - vous pouvez faire n'importe quoi avec la ligne avant l'insertion, mais cela va être un peu plus lent en raison de la surcharge de création et de suppression d'une table temporaire.

Cette solution fonctionne pour une seule ligne ou pour n'importe quel nombre de lignes à la fois. Chaque ligne obtient automatiquement une nouvelle valeur par défaut de la séquence.

Utilisation de la notation courte (standard SQL) TABLE people .

SQL dynamique

Pour plusieurs lignes à la fois, le SQL dynamique va être le plus rapide. Concaténez les colonnes de la table système pg_attribute Ou du schéma d'information et exécutez-la dynamiquement dans une instruction DO ou écrivez une fonction pour une utilisation répétée:

CREATE OR REPLACE FUNCTION f_row_copy(_tbl regclass, _id int, OUT row_ct int) AS
$func$
BEGIN
   EXECUTE (
      SELECT format('INSERT INTO %1$s(%2$s) SELECT %2$s FROM %1$s WHERE id = $1',
                    _tbl, string_agg(quote_ident(attname), ', '))
      FROM   pg_attribute
      WHERE  attrelid = _tbl
      AND    NOT attisdropped  -- no dropped (dead) columns
      AND    attnum > 0        -- no system columns
      AND    attname <> 'id'   -- exclude id column
      )
   USING _id;

   GET DIAGNOSTICS row_ct = ROW_COUNT;  -- directly assign OUT parameter
END
$func$  LANGUAGE plpgsql;

Appel:

SELECT f_row_copy('people', 9);

Fonctionne pour n'importe quelle table avec une colonne entière nommée id. Vous pouvez aussi facilement rendre le nom de la colonne dynamique ...

Ce n'est peut-être pas votre premier choix puisque vous vouliez stay away from stored procedures, Mais là encore, c'est pas une "procédure stockée" de toute façon ...

En relation:

Solution avancée

Une colonne serial est un cas particulier. Si vous souhaitez remplir plus ou toutes les colonnes avec leurs valeurs par défaut respectives, cela devient plus sophistiqué. Considérez cette réponse connexe:

15
Erwin Brandstetter

Essayez de créer un trigger lors de l'insertion:

CREATE TRIGGER name BEFORE INSERT

Dans ce déclencheur, vous définissez l'ID NULL. Lorsque le déclenchement est terminé, l'insertion est terminée et Postgres fournira un ID. Je suppose que vous avez défini l'ID comme DEFAULT NEXTVAL('A_SEQUENCE'::REGCLASS).

0
Marco