Beste Weg, um eine neue Spalte in einer großen Tabelle zu füllen?


33

Wir haben eine 2,2 GB-Tabelle in Postgres mit 7.801.611 Zeilen. Wir fügen eine uuid / guid-Spalte hinzu, und ich frage mich, wie diese Spalte am besten ausgefüllt werden kann (da wir ihr eine NOT NULLEinschränkung hinzufügen möchten ).

Wenn ich Postgres richtig verstehe, ist ein Update technisch ein Löschen und Einfügen, so dass im Grunde die gesamte 2,2-GB-Tabelle neu erstellt wird. Wir haben auch einen Sklaven am Laufen, damit dieser nicht zurückbleibt.

Gibt es einen besseren Weg, als ein Skript zu schreiben, das es mit der Zeit langsam füllt?


2
Hast du schon einen ALTER TABLE .. ADD COLUMN ...oder soll dieser Teil auch beantwortet werden?
Ypercubeᵀᴹ

Haben noch keine Tabellenänderungen vorgenommen, nur in der Planungsphase. Ich habe dies zuvor getan, indem ich die Spalte hinzugefügt, sie aufgefüllt und dann die Einschränkung oder den Index hinzugefügt habe. Diese Tabelle ist jedoch erheblich größer und ich
Collin Peters

Antworten:


45

Es kommt sehr auf die Details Ihrer Anforderungen an.

Wenn Sie über ausreichend freien Speicherplatz (mindestens 110% von pg_size_pretty((pg_total_relation_size(tbl))) auf der Festplatte verfügen und sich eine Freigabesperre für einige Zeit und eine exklusive Sperre für eine sehr kurze Zeit leisten können , erstellen Sie eine neue Tabelle mit der uuidSpalte using CREATE TABLE AS. Warum?

Der folgende Code verwendet eine Funktion aus dem uuid-ossZusatzmodul .

  • Sperren Sie die Tabelle gegen gleichzeitige Änderungen im SHAREModus (gleichzeitige Lesevorgänge sind weiterhin zulässig). Versuche, in die Tabelle zu schreiben, warten und schlagen schließlich fehl. Siehe unten.

  • Kopieren Sie die gesamte Tabelle, während Sie die neue Spalte im Handumdrehen füllen, und ordnen Sie dabei möglicherweise die Zeilen günstig an.
    Wenn Sie Zeilen neu anordnen möchten, stellen Sie sicher, dass Sie work_memso hoch wie möglich einstellen (nur für Ihre Sitzung, nicht global).

  • Fügen Sie dann Einschränkungen, Fremdschlüssel, Indizes, Trigger usw. zur neuen Tabelle hinzu. Wenn Sie große Teile einer Tabelle aktualisieren, ist es viel schneller, Indizes von Grund auf neu zu erstellen, als Zeilen iterativ hinzuzufügen.

  • Wenn die neue Tabelle fertig ist, löschen Sie die alte und benennen Sie die neue um, um sie als Drop-In-Ersatz zu verwenden. Nur dieser letzte Schritt erhält eine exklusive Sperre für den alten Tisch für den Rest der Transaktion - die jetzt sehr kurz sein sollte.
    Es erfordert auch, dass Sie jedes Objekt abhängig vom Tabellentyp (Ansichten, Funktionen, die den Tabellentyp in der Signatur verwenden, ...) löschen und anschließend neu erstellen.

  • Machen Sie alles in einer Transaktion, um unvollständige Status zu vermeiden.

BEGIN;
LOCK TABLE tbl IN SHARE MODE;

SET LOCAL work_mem = '???? MB';  -- just for this transaction

CREATE TABLE tbl_new AS 
SELECT uuid_generate_v1() AS tbl_uuid, <list of all columns in order>
FROM   tbl
ORDER  BY ??;  -- optionally order rows favorably while being at it.

ALTER TABLE tbl_new
   ALTER COLUMN tbl_uuid SET NOT NULL
 , ALTER COLUMN tbl_uuid SET DEFAULT uuid_generate_v1()
 , ADD CONSTRAINT tbl_uuid_uni UNIQUE(tbl_uuid);

-- more constraints, indices, triggers?

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME tbl;

-- recreate views etc. if any
COMMIT;

Dies sollte am schnellsten sein. Jede andere Methode zum Aktualisieren an Ort und Stelle muss auch die gesamte Tabelle neu schreiben, nur auf eine teurere Art und Weise. Sie würden diesen Weg nur gehen, wenn Sie nicht genügend freien Speicherplatz auf der Festplatte haben oder es sich nicht leisten können, die gesamte Tabelle zu sperren oder Fehler für gleichzeitige Schreibversuche zu generieren.

Was passiert mit gleichzeitigen Schreibvorgängen?

Andere Transaktionen (in anderen Sitzungen), die versuchen, nach dem Aufheben der Sperre in derselben Tabelle zu INSERT/ UPDATE/ zu landen, warten, bis die Sperre aufgehoben wird oder eine Zeitüberschreitung eintritt, je nachdem, was zuerst eintritt. Sie schlagen in beiden Fällen fehl , da die Tabelle, in die sie schreiben wollten, unter ihnen gelöscht wurde.DELETESHARE

Die neue Tabelle hat eine neue Tabellen-OID, aber die gleichzeitige Transaktion hat den Tabellennamen bereits in die OID der vorherigen Tabelle aufgelöst . Wenn die Sperre endlich aufgehoben wird, versuchen sie, die Tabelle selbst zu sperren, bevor sie darauf schreibt, und stellen fest, dass sie weg ist. Postgres wird antworten:

ERROR: could not open relation with OID 123456

Wo 123456ist die OID der alten Tabelle. Sie müssen diese Ausnahme abfangen und Abfragen in Ihrem App-Code wiederholen, um sie zu vermeiden.

Wenn Sie sich das nicht leisten können, müssen Sie Ihren Originaltisch behalten .

Zwei Alternativen, um die vorhandene Tabelle beizubehalten

  1. Aktualisieren Sie vor dem Hinzufügen der NOT NULLEinschränkung an Ort und Stelle (möglicherweise wird die Aktualisierung für kleine Segmente gleichzeitig ausgeführt) . Das Hinzufügen einer neuen Spalte mit NULL-Werten und ohne NOT NULLEinschränkung ist kostengünstig.
    Seit Postgres 9.2 können Sie auch eine CHECKEinschränkungNOT VALID erstellen mit :

    Die Einschränkung wird weiterhin für nachfolgende Einfügungen oder Aktualisierungen erzwungen

    Auf diese Weise können Sie Zeilen peu à peu aktualisieren - in mehreren separaten Transaktionen . Dadurch wird vermieden, dass Zeilensperren zu lange beibehalten werden, und tote Zeilen können wiederverwendet werden. (Sie müssen VACUUMmanuell ausgeführt werden, wenn zwischen den einzelnen Schritten nicht genügend Zeit liegt , damit das automatische Absaugen einsetzt.) Fügen Sie schließlich die NOT NULLEinschränkung hinzu und entfernen Sie die NOT VALID CHECKEinschränkung:

    ALTER TABLE tbl ADD CONSTRAINT tbl_no_null CHECK (tbl_uuid IS NOT NULL) NOT VALID;
    
    -- update rows in multiple batches in separate transactions
    -- possibly run VACUUM between transactions
    
    ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL;
    ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;

    Verwandte Antwort, die NOT VALIDausführlicher bespricht:

  2. Bereiten Sie den neuen Status in einer temporären Tabelle vor , TRUNCATEund füllen Sie das Original aus der temporären Tabelle nach. Alles in einer Transaktion . Bevor Sie die neue Tabelle vorbereiten, müssen Sie noch eine SHARESperre aktivieren, um zu verhindern, dass gleichzeitige Schreibvorgänge verloren gehen.

    Details in dieser verwandten Antwort auf SO:


Fantastische Antwort! Genau die Infos, die ich gesucht habe. Zwei Fragen 1. Haben Sie eine Idee, wie Sie auf einfache Weise testen können, wie lange eine solche Aktion dauern würde? 2. Was passiert mit Aktionen, die während dieser 5 Minuten versuchen, eine Zeile in dieser Tabelle zu aktualisieren, wenn dies beispielsweise 5 Minuten dauert?
Collin Peters

@CollinPeters: 1. Der Löwenanteil der Zeit würde in das Kopieren des großen Tisches fließen - und möglicherweise Indizes und Einschränkungen neu erstellen (das hängt davon ab). Löschen und Umbenennen ist billig. Zum Testen können Sie Ihr vorbereitetes SQL-Skript ohne das LOCKbis und ohne das ausführen DROP. Ich konnte nur wilde und nutzlose Vermutungen anstellen. Bezüglich 2. beachten Sie bitte den Nachtrag zu meiner Antwort.
Erwin Brandstetter

@ErwinBrandstetter Fahre fort, Ansichten neu zu erstellen, wenn ich also ein Dutzend Ansichten habe, die nach dem Umbenennen der Tabelle noch alte Tabellen (oid) verwenden. Gibt es eine Möglichkeit, das vollständige Ersetzen durchzuführen, anstatt die gesamte Ansichtsaktualisierung / -erstellung erneut auszuführen?
CodeFarmer

@CodeFarmer: Wenn Sie eine Tabelle nur umbenennen, arbeiten Ansichten weiterhin mit der umbenannten Tabelle. Damit Ansichten stattdessen die neue Tabelle verwenden, müssen Sie sie basierend auf der neuen Tabelle neu erstellen. (Auch damit die alte Tabelle gelöscht werden kann.) Nein (praktisch).
Erwin Brandstetter

14

Ich habe keine "beste" Antwort, aber ich habe eine "am wenigsten schlechte" Antwort, mit der Sie die Dinge einigermaßen schnell erledigen können.

Meine Tabelle hatte 2-MM-Zeilen und die Aktualisierungsleistung war fehlerhaft, als ich versuchte, eine sekundäre Zeitstempelspalte hinzuzufügen, die standardmäßig der ersten entspricht.

ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;

Nachdem es 40 Minuten lang hängen geblieben war, versuchte ich es mit einer kleinen Menge, um eine Vorstellung davon zu bekommen, wie lange dies dauern könnte - die Prognose lag bei ungefähr 8 Stunden.

Die akzeptierte Antwort ist definitiv besser - aber diese Tabelle wird in meiner Datenbank häufig verwendet. Es gibt ein paar Dutzend Tische, die darauf FKEY; Ich wollte vermeiden, FOREIGN KEYS an so vielen Tabellen zu wechseln. Und dann gibt es Ansichten.

Ein bisschen nach Dokumenten, Fallstudien und StackOverflow suchen, und ich hatte das "A-Ha!" Moment. Der Drain lag nicht auf dem Kern-UPDATE, sondern auf allen INDEX-Operationen. Meine Tabelle enthielt 12 Indizes - einige für eindeutige Einschränkungen, einige für die Beschleunigung des Abfrageplaners und einige für die Volltextsuche.

Jede Zeile, die AKTUALISIERT wurde, arbeitete nicht nur an einem DELETE / INSERT, sondern auch an dem Aufwand, die einzelnen Indizes zu ändern und Einschränkungen zu überprüfen.

Meine Lösung bestand darin, jeden Index und jede Einschränkung zu löschen, die Tabelle zu aktualisieren und dann alle Indizes / Einschränkungen wieder hinzuzufügen.

Es dauerte ungefähr 3 Minuten, um eine SQL-Transaktion zu schreiben, die Folgendes ausführte:

  • START;
  • gelöschte Indizes / Konstanten
  • Tabelle aktualisieren
  • Fügen Sie Indizes / Einschränkungen erneut hinzu
  • VERPFLICHTEN;

Die Ausführung des Skripts dauerte 7 Minuten.

Die akzeptierte Antwort ist definitiv besser und richtiger ... und macht Ausfallzeiten praktisch überflüssig. In meinem Fall hätte die Verwendung dieser Lösung erheblich mehr "Entwicklerarbeit" in Anspruch genommen, und wir hatten ein 30-minütiges Zeitfenster für geplante Ausfallzeiten, in dem dies möglich war. Unsere Lösung hat sich in 10 Minuten damit befasst.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.