Doppelte Zeile mit Primärschlüssel in PostgreSQL

7

Angenommen, ich habe eine Tabelle mit dem folgenden Namen people, in der idsich ein Primärschlüssel befindet:

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

Ich versuche, eine Abfrage zur Zeilenduplizierung zu schreiben, die robust genug ist, um Schemaänderungen an der Tabelle zu berücksichtigen. Jedes Mal, wenn ich der Tabelle eine Spalte hinzufüge, möchte ich nicht zurückgehen und die Duplizierungsabfrage ändern müssen.

Ich weiß, dass ich dies tun kann, wodurch die Datensatz-ID 2 dupliziert und dem duplizierten Datensatz eine neue ID zugewiesen wird:

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

Wenn ich jedoch eine ageSpalte hinzufüge , muss ich die Abfrage ändern, um auch die Altersspalte zu berücksichtigen.

Offensichtlich kann ich Folgendes nicht tun, da dadurch auch der Primärschlüssel dupliziert wird, was zu einem duplicate key value violates unique constraint- führt. Und ich möchte nicht, dass sie trotzdem dieselbe ID haben:

INSERT INTO people SELECT * FROM people WHERE id = 2

Was wäre ein vernünftiger Ansatz zur Lösung dieser Herausforderung? Ich würde es vorziehen, mich von gespeicherten Prozeduren fernzuhalten, aber ich bin nicht zu 100% gegen sie, nehme ich an ...

Joshua Burns
quelle
1
Nebenbei: Vielleicht sollten Sie ein anderes Beispiel verwenden, da dies ageeine Art Anti-Muster für eine Spalte ist. (Man sollte das lieber aufbewahren birthday.)
Erwin Brandstetter

Antworten:

15

Einfach mit hstore

Wenn Sie das zusätzliche Modul hstoreinstalliert haben ( Anweisungen im Link unten ), gibt es eine überraschend einfache Möglichkeit, die Werte einzelner Felder zu ersetzen, ohne etwas über andere Spalten zu wissen:

Einfaches Beispiel: duplizieren Sie die Zeile mit , id = 2aber ersetzen 2mit 3:

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

Einzelheiten:

Angenommen (es ist nicht in der Frage definiert), dasspeople.ides sich um eineserialSpalte mit einer angehängten Sequenz handelt, möchten Sie den nächsten Wert aus der Sequenz. Wir können den Sequenznamen mit bestimmenpg_get_serial_sequence(). Einzelheiten:

Oder Sie können den Sequenznamen einfach fest codieren, wenn er sich nicht ändert.
Wir hätten diese Frage:

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

Was funktioniert , aber unter einer Schwäche im Postgres-Abfrageplaner leidet: Der Ausdruck wird für jede einzelne Spalte in der Zeile separat ausgewertet, wodurch Sequenznummern und Leistung verschwendet werden. Um dies zu vermeiden, verschieben Sie den Ausdruck in eine Unterabfrage und zerlegen Sie die Zeile nur einmal :

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;

Wahrscheinlich am schnellsten für eine einzelne (oder wenige) Zeile (n) gleichzeitig.

json / jsonb

Wenn Sie keine hstorezusätzlichen Module installiert haben und nicht installieren können, können Sie einen ähnlichen Trick mit json_populate_record()oder jsonb_populate_record()ausführen. Diese Funktion ist jedoch nicht dokumentiert und möglicherweise unzuverlässig.

Vorübergehende temporäre Tabelle

Eine andere einfache Lösung wäre, ein vorübergehendes temporäres wie dieses zu verwenden:

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;

Ich habe hinzugefügt ON COMMIT DROP, um die Tabelle am Ende der Transaktion automatisch zu löschen. Folglich habe ich die Operation auch in eine eigene Transaktion verpackt. Beides ist nicht unbedingt erforderlich.

Dies bietet eine Vielzahl zusätzlicher Optionen - Sie können vor dem Einfügen alles mit der Zeile tun, sie wird jedoch aufgrund des Overheads beim Erstellen und Löschen einer temporären Tabelle etwas langsamer.

Diese Lösung funktioniert für eine einzelne Zeile oder für eine beliebige Anzahl von Zeilen gleichzeitig . Jede Zeile erhält automatisch einen neuen Standardwert aus der Sequenz.

Verwenden der Kurznotation (SQL-Standard)TABLE people .

Dynamisches SQL

Für viele Zeilen gleichzeitig wird dynamisches SQL am schnellsten sein. Verketten Sie die Spalten aus der Systemtabelle pg_attributeoder aus dem Informationsschema und führen Sie sie dynamisch in einer DOAnweisung aus oder schreiben Sie eine Funktion zur wiederholten Verwendung:

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;

Anruf:

SELECT f_row_copy('people', 9);

Funktioniert für jede Tabelle mit einer Ganzzahlspalte mit dem Namen id. Sie können den Spaltennamen auch leicht dynamisch gestalten ...

Vielleicht nicht Ihre erste Wahl, seit Sie wollten stay away from stored procedures, aber andererseits ist es sowieso keine "gespeicherte Prozedur" ...

Verbunden:

Erweiterte Lösung

Eine serialSpalte ist ein Sonderfall. Wenn Sie mehr oder alle Spalten mit ihren jeweiligen Standardwerten füllen möchten, wird dies komplexer. Betrachten Sie diese verwandte Antwort:

Erwin Brandstetter
quelle
hstoreDer Ansatz funktioniert großartig, aber ich denke, ich werde mit dem jsonbAnsatz herumspielen, da ich mich bereits stark darauf verlasse. Vielen Dank für das tolle Schreiben, Erwin!
Joshua Burns
Die vorübergehende temporäre Tabelle funktionierte sehr gut für mich, wo ich einen Block von Datensätzen kopieren musste, während ich ein anderes Feld änderte. ... WHERE fieldA=1 ... SET ... fieldA=2 ...
Chris Nelson
0

Versuchen Sie, eine triggerEinfügung zu erstellen :

CREATE TRIGGER name BEFORE INSERT

In diesem Trigger machen Sie die ID NULL. Wenn der Trigger beendet ist, ist das Einfügen abgeschlossen und Postgres wird eine ID bereitstellen. Ich gehe davon aus, dass Sie die ID als definiert haben DEFAULT NEXTVAL('A_SEQUENCE'::REGCLASS).

Marco
quelle
2
Dies wird funktionieren, aber es ist eine ziemlich "hinterhältige" Lösung, von der ich denke, dass sie auf lange Sicht Probleme verursachen würde. Ich persönlich würde dies nach Möglichkeit vermeiden ... Wenn er sich dafür entscheidet, hoffe ich, dass er ZUERST eine SELECT-Funktion im Trigger ausführt, um festzustellen, ob die angegebene ID vorhanden ist. und erst dann setzen Sie die NEW.id auf NULL ..
Joishi Bodio
Er kann das tun, aber wenn Sie sich dafür entscheiden, eine zu verwenden NEXTVAL('A_SEQUENCE'::REGCLASS), geben Sie niemals selbst eine ID für einen neuen Eintrag an.
Marco
1
Dies hängt davon ab, wie Code und / oder externe Bibliotheken in Ihrem Code die Datenbank verwenden. Einige fragen die SEQ.NEXTVAL möglicherweise manuell ab und senden dann die generierte ID in einer INSERT-Anweisung. Ich würde einfach nicht darauf vertrauen, dass sich das Tabellen- / Sequenz- / Trigger-Trio die ganze Zeit "wie erwartet" verhält. Also mein erster Kommentar.
Joishi Bodio
-2

Dynamic SQL Arbeitet super, ich suche das seit ein paar Jahren,

Wenn Sie mehr als eine ausgeschlossene Spalte haben, versuchen Sie es einfach.

AND    attname <> 'id'   -- exclude id column
AND    attname <> 'second_col_name'   -- exclude second_col_name 
gut
quelle