it-swarm.com.de

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

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 gefüllt werden kann (da wir ihr eine NOT NULL - Einschränkung hinzufügen möchten).

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

Gibt es eine bessere Möglichkeit, als ein Skript zu schreiben, das es im Laufe der Zeit langsam auffüllt?

35
Collin Peters

Es hängt sehr stark von den Details Ihrer Anforderungen ab.

Wenn Sie ausreichend freien Speicherplatz haben (mindestens 110% von - pg_size_pretty((pg_total_relation_size(tbl)) ) auf der Festplatte und kann sich für einige Zeit eine Freigabesperre leisten und eine exklusive Sperre für eine sehr kurze Zeit , dann erstellen Sie eine neue Tabelle einschließlich der uuid Spalte mit CREATE TABLE AS. Warum?

Der folgende Code verwendet eine Funktion aus dem zusätzlichen uuid-oss Modul .

  • Sperren Sie die Tabelle gegen gleichzeitige Änderungen im Modus SHARE (erlaubt weiterhin gleichzeitige Lesevorgänge). 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 laufenden Betrieb füllen, und ordnen Sie möglicherweise Zeilen günstig an, während Sie gerade dabei sind.
    Wenn Sie werden Zeilen neu anordnen, stellen Sie sicher, dass Sie work_mem so hoch wie Sie es sich leisten können (nur für Ihre Sitzung, nicht global).

  • Dann Fügen Sie der neuen Tabelle Einschränkungen, Fremdschlüssel, Indizes, Trigger usw. 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 Ersatz für die Drop-In-Tabelle 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.
    Außerdem müssen Sie alle Objekte 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 Zustände 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. Bei jeder anderen Aktualisierungsmethode muss auch die gesamte Tabelle neu geschrieben werden, nur auf teurere 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, INSERT/UPDATE/DELETE in derselben Tabelle zu verwenden, nachdem Ihre Transaktion die Sperre SHARE übernommen hat, warten bis Die Sperre wird aufgehoben oder es tritt eine Zeitüberschreitung ein, je nachdem, was zuerst eintritt. Sie werden in beiden Fällen fehlschlagen , da die Tabelle, in die sie schreiben wollten, unter ihnen gelöscht wurde.

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

ERROR: could not open relation with OID 123456

Wo 123456 ist 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 behalten Ihre ursprüngliche Tabelle.

Zwei Alternativen, die die vorhandene Tabelle beibehalten

  1. Update an Ort und Stelle (möglicherweise wird das Update jeweils für kleine Segmente ausgeführt), bevor Sie das NOT NULL Einschränkung. Hinzufügen einer neuen Spalte mit NULL-Werten und ohne NOT NULL Einschränkung ist billig.
    Seit Postgres 9.2 können Sie auch ein CHECK erstellen Einschränkung mit NOT VALID :

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

    Auf diese Weise können Sie Zeilen peu à peu - in mehreren separaten Transaktionen aktualisieren. . Dadurch wird vermieden, dass die Zeilensperren zu lange beibehalten werden, und es können auch tote Zeilen wiederverwendet werden. (Sie müssen VACUUM manuell ausführen, wenn dazwischen nicht genügend Zeit für das Einschalten des Autovakuums vorhanden ist.) Fügen Sie abschließend das NOT NULL Einschränkung und entfernen Sie die NOT VALID CHECK Einschrä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 über NOT VALID ausführlicher:

  2. Bereiten Sie den neuen Status in einer temporären Tabelle vor , TRUNCATE das Original und füllen aus der temporären Tabelle nach. Alles in einer Transaktion . Sie müssen noch eine SHARE Sperre vorher vorbereiten, um zu verhindern, dass gleichzeitige Schreibvorgänge verloren gehen .

    Details in dieser verwandten Antwort auf SO:

47

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 schlecht, als ich versuchte, eine sekundäre Zeitstempelspalte hinzuzufügen, die standardmäßig die erste war.

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 aufgehängt hatte, versuchte ich es an einer kleinen Charge, um eine Vorstellung davon zu bekommen, wie lange dies dauern könnte - die Vorhersage lag bei 8 Stunden.

Die akzeptierte Antwort ist definitiv besser - aber diese Tabelle wird in meiner Datenbank häufig verwendet. Es gibt ein paar Dutzend Tische, auf denen FKEY steht; 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 befand sich nicht im Kern-UPDATE, sondern in 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, jeden Index 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;
  • indizes/Konstanten gelöscht
  • 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 korrekter ... und macht Ausfallzeiten praktisch überflüssig. In meinem Fall hätte die Verwendung dieser Lösung jedoch erheblich mehr "Entwicklerarbeit" gekostet, und wir hatten ein 30-minütiges Fenster mit geplanten Ausfallzeiten, in denen dies erreicht werden konnte. Unsere Lösung hat dies in 10 Minuten behoben.

17