Was soll ich tun (und definitiv nicht), wenn Sie
eine groĂe Anzahl von DatensĂ€tzen in der aktiv genutzten PostgreSQL-Tabelle "multi-million"
aktualisieren mĂŒssen - den Wert des neuen Felds initialisieren oder Fehler in vorhandenen DatensĂ€tzen korrigieren? Gleichzeitig sparen Sie Zeit und verlieren kein Firmengeld durch Ausfallzeiten.

Bereiten Sie die Testdaten vor:
CREATE TABLE tbl(k text, v integer); INSERT INTO tbl SELECT chr(ascii('a'::text) + (random() * 26)::integer) k , (random() * 100)::integer v FROM generate_series(1, 1000000) i;
Angenommen, wir möchten nur den Wert von v fĂŒr alle DatensĂ€tze mit k im Bereich 'q' .. 'z' um 1 erhöhen.
Bevor Sie jedoch mit den Experimenten beginnen, speichern Sie den Originaldatensatz, um jedes Mal "saubere" Ergebnisse zu erzielen:
CREATE TABLE _tbl AS TABLE tbl;
UPDATE: Einer fĂŒr alle und alle fĂŒr einen
Die einfachste Option, die einem sofort in den Sinn kommt, ist, alles in einem UPDATE zu erledigen:
UPDATE tbl SET v = v + 1 WHERE k BETWEEN 'q' AND 'z';
[siehe EXPLAIN.TENSOR.RU]Ein recht einfacher Vorgang auf ganz âkurzenâ Leitungen dauerte anscheinend lĂ€nger als 2,5 Sekunden. Und wenn Ihr Ausdruck komplizierter ist, ist die Linie authentischer, es gibt mehr Aufzeichnungen und sogar einige Auslöser greifen ein - die Zeit kann sogar bis zu Minuten, aber bis zu Stunden ansteigen. Angenommen, Sie sind bereit zu warten, und der Rest Ihres Systems ist an diese Basis gebunden, wenn eine aktive OLTP-Last vorliegt.
Das Problem ist, dass UPDATE, sobald es zu einem bestimmten Datensatz
gelangt, diesen bis zum Ende der AusfĂŒhrung blockiert . Wenn er gleichzeitig mit demselben Datensatz an einem parallel gestarteten âSpotâ -UPDATE arbeiten möchte,
wartet er nach wie
vor auf einen Block fĂŒr die Aktualisierungsanforderung und setzt sich bis zum Ende seiner Arbeit durch.

©
wumo.com/wumoDas schlimmste Szenario sind Web-Systeme, bei denen Verbindungen zur Datenbank nach Bedarf hergestellt werden. SchlieĂlich sammeln sich solche "baumelnden" Verbindungen an und verschlingen die Ressourcen sowohl der Datenbank als auch des Clients, wenn Sie keinen davon getrennten Schutzmechanismus einrichten.
Transaktionen aufteilen
Im Allgemeinen ist nicht alles gut, wenn alles auf einmal erledigt wird. Ja, und selbst wenn wir ein groĂes UPDATE in mehrere kleine UPDATE aufteilen, aber alles
in einer Transaktion funktionieren lassen, bleibt das Problem beim Sperren gleich, da verÀnderbare DatensÀtze bis zum Ende der gesamten Transaktion gesperrt sind.
Wir mĂŒssen also eine groĂe Transaktion in mehrere aufteilen. Dazu können wir entweder externe Mittel verwenden und eine Art Skript schreiben, das separate Transaktionen generiert, oder die Möglichkeiten nutzen, die uns die Datenbank selbst bietet.
CALL und Transaktionsmanagement
Ab PostgreSQL 11 können
Transaktionen direkt im prozeduralen Code verwaltet werden:
In Prozeduren, die vom CALL-Befehl aufgerufen werden, sowie in anonymen Codeblöcken (im DO-Befehl) können Sie Transaktionen abschlieĂen, indem Sie COMMIT und ROLLBACK ausfĂŒhren. Nachdem die Transaktion mit diesen Befehlen abgeschlossen ist, wird automatisch eine neue gestartet.
Aber diese Version ist bei weitem nicht jedermanns Sache und die Arbeit mit CALL hat ihre Grenzen. Deshalb werden wir versuchen, unser Problem ohne externe Mittel zu lösen, damit es auf allen aktuellen Versionen und auch mit minimalen Ănderungen auf dem Server selbst funktioniert - so dass es nicht notwendig wĂ€re, irgendetwas zu kompilieren und neu zu starten.
Aus dem gleichen Grund werden wir die Möglichkeit,
autonome Transaktionen ĂŒber pg_background zu organisieren, nicht in Betracht
ziehen .
Verwalten von Verbindungen "innerhalb" der Basis
PostgreSQL hat in der
Vergangenheit verschiedene Methoden verwendet,
um autonome Transaktionen zu
emulieren und separate zusĂ€tzliche Verbindungen zu generieren - ĂŒber zusĂ€tzliche prozedurale Sprachen oder das Standard-
Dblink-Modul . Letzteres hat den Vorteil, dass es standardmĂ€Ăig in den meisten Distributionen enthalten ist und nur ein Befehl erforderlich ist, um es in der Datenbank zu aktivieren:
CREATE EXTENSION dblink;
"... und viele, viele ekelhafte Kinder mitgebracht"
Bevor Sie jedoch eine dblink-Bindung erstellen, sollten Sie zunĂ€chst herausfinden, wie ein ânormaler Entwicklerâ einen groĂen Datensatz, den er aktualisieren muss, in kleine aufteilt.
Naive LIMIT ... OFFSET
Die erste Idee besteht darin, eine "Paginierungs" -Suche durchzufĂŒhren:
" WĂ€hlen
wir jedes Mal die nÀchsten tausend DatensÀtze aus ", indem Sie den OFFSET-Wert in jeder neuen Anforderung erhöhen:
UPDATE tbl T SET v = Tv + 1 FROM ( SELECT k , v FROM tbl WHERE k BETWEEN 'q' AND 'z' ORDER BY
Vor dem Testen der Leistung dieser Lösung stellen wir den Datensatz wieder her:
TRUNCATE TABLE tbl; INSERT INTO tbl TABLE _tbl;
Wie wir im obigen Plan gesehen haben, mĂŒssen wir ungefĂ€hr 384.000 DatensĂ€tze aktualisieren. Lassen Sie uns daher gleich sehen, wie die Aktualisierungen nĂ€her am Ende durchgefĂŒhrt werden -
im Bereich der 300. Iteration von 1000 EintrÀgen :
[siehe EXPLAIN.TENSOR.RU]Oh ... Die Aktualisierung des Samples am Ende der
gesamten 1K-DatensÀtze kostet uns fast so viel Zeit wie die
gesamte Originalversion !
Dies ist nicht unsere Wahl. Es kann trotzdem verwendet werden, wenn Sie nur wenige Iterationen und kleine OFFSET-Werte erhalten. Weil das
LIMIT X OFFSET Y fĂŒr die Datenbank gleichbedeutend ist mit "
subtrahieren / auswĂ€hlen / bilden von ersten X + Y-DatensĂ€tzen und dann werfen Sie das erste Y in den Papierkorb ", was fĂŒr groĂe Werte von Y tragisch aussieht.
TatsÀchlich kann diese Methode
ĂŒberhaupt nicht angewendet werden ! Wir verlassen uns nicht nur auf aktualisierte Werte fĂŒr die Auswahl, sondern riskieren auch, einen Teil der DatensĂ€tze zu ĂŒberspringen und den anderen Teil zweimal zu aktualisieren, wenn Blöcke mit denselben SchlĂŒsseln an den Seitenrand gelangen:

In diesem Beispiel haben wir den grĂŒnen Datensatz zweimal und den roten Datensatz nie aktualisiert. Nur weil bei gleichen Werten der SortierschlĂŒssel die Reihenfolge der DatensĂ€tze selbst in einem solchen Block nicht festgelegt ist.
Traurig BESTELLEN BEI ... LIMIT
Lassen Sie uns die Aufgabe etwas modifizieren - fĂŒgen Sie ein neues Feld hinzu, in das wir unseren Wert v + 1 schreiben:
ALTER TABLE tbl ADD COLUMN x integer;
Bitte beachten Sie, dass dieses Design fast sofort funktioniert, ohne die gesamte Tabelle neu zu schreiben. Wenn Sie jedoch einen DEFAULT-Wert hinzufĂŒgen, gilt dies - erst
ab der 11. Version .
Lassen Sie uns sofort einen Index erstellen, in dem nur nicht initialisierte EintrÀge verbleiben:
CREATE INDEX CONCURRENTLY ON tbl(k, v) WHERE x IS NULL;
Der CONCURRENTLY-Index blockiert nicht die Lese- und Schreibarbeit mit der Tabelle, wĂ€hrend er sich langsam sogar auf einen riesigen Datensatz ĂŒbertrĂ€gt.
Nun lautet die Idee:
"WÀhlen wir jedes Mal nur die ersten tausend DatensÀtze aus diesem Index aus." UPDATE tbl T SET x = Tv + 1 FROM ( SELECT k, v FROM tbl WHERE k BETWEEN 'q' AND 'z' AND x IS NULL ORDER BY k, v LIMIT 1000
[siehe EXPLAIN.TENSOR.RU]Schon viel besser - die Dauer jeder einzelnen Transaktion verkĂŒrzt sich jetzt um das Sechsfache.
Aber lassen Sie uns noch einmal sehen, wie der Plan fĂŒr die 200. Iteration aussehen wird:
Update on tbl t (actual time=530.591..530.591 rows=0 loops=1) Buffers: shared hit=789337 read=1 dirtied=1
Die Zeit verschlechterte sich erneut (allerdings nur um 25%), und die Puffer nahmen zu - aber warum?
Tatsache ist, dass
MVCC in PostgreSQL "tote Seelen" im Index hinterlĂ€sst - Versionen bereits aktualisierter DatensĂ€tze, die jetzt nicht mehr fĂŒr den Index geeignet sind. Das heiĂt, wir
scannen immer noch die ersten 1000 DatensÀtze bei der 200. Iteration, obwohl wir spÀter die vorherigen 199K-Versionen von Tupeln verwerfen, die bereits geÀndert wurden.
Wenn bei uns nicht mehrere Hundert, sondern mehrere Hunderttausend Iterationen erforderlich sind, macht sich die Verschlechterung bei jeder nachfolgenden AbfrageausfĂŒhrung stĂ€rker bemerkbar.
UPDATE nach Segment
Warum hĂ€ngen wir eigentlich so an diesem Wert von â1000 DatensĂ€tzenâ? SchlieĂlich haben wir
keinen Grund, genau 1000 oder eine andere bestimmte Zahl
zu wĂ€hlen . Wir wollten lediglich den gesamten Datensatz in einige, nicht unbedingt gleich groĂe, nicht
zusammenhĂ€ngende Segmente "schneiden". Verwenden wir also unseren vorhandenen Index fĂŒr den beabsichtigten Zweck.
Ein
indiziertes Paar (k, v) eignet sich hervorragend fĂŒr unsere Aufgabe. Lassen Sie uns eine Abfrage erstellen, damit sie auf dem zuletzt verarbeiteten Paar aufbauen kann:
WITH kv AS ( SELECT k, v FROM tbl WHERE (k, v) > ($1, $2) AND k BETWEEN 'q' AND 'z' AND x IS NULL ORDER BY k, v LIMIT 1 ) , upd AS ( UPDATE tbl T SET x = Tv + 1 WHERE (Tk, Tv) = (TABLE kv) AND Tx IS NULL RETURNING k, v ) TABLE upd LIMIT 1;
Bei der ersten Iteration genĂŒgt es, die Abfrageparameter auf den
Wert "Null" ('', 0) zu setzen , und bei jeder nÀchsten Iteration wird das
Ergebnis der vorherigen Abfrage abgerufen .
[siehe EXPLAIN.TENSOR.RU]Die Transaktions- / Sperrzeit betrĂ€gt weniger als eine Millisekunde. Die Anzahl der Iterationen wird nicht beeintrĂ€chtigt. Ein vollstĂ€ndiger vorlĂ€ufiger Scan aller Daten in der Tabelle ist nicht erforderlich. GroĂartig!
Setzen Sie die endgĂŒltige Version mit Dblink DO $$ DECLARE k text = ''; v integer = 0; BEGIN PERFORM dblink_connect('dbname=' || current_database() || ' port=' || current_setting('port'));
Ein zusĂ€tzlicher Vorteil dieser Methode ist die Möglichkeit, die AusfĂŒhrung dieses Skripts jederzeit zu unterbrechen und dann an der gewĂŒnschten Stelle fortzusetzen.
Komplexe Berechnungen in UPDATE
Ich werde die Situation mit der schwierigen Berechnung des zugewiesenen Werts separat erwĂ€hnen - wenn Sie etwas aus den verknĂŒpften Tabellen berechnen mĂŒssen.
Die fĂŒr die Berechnung aufgewendete Zeit erhöht auch die Dauer der Transaktion. Daher ist es am besten
, diese Werte nach UPDATE zu
berechnen .
Zum Beispiel möchten wir unser neues Feld x mit der Anzahl der DatensĂ€tze fĂŒllen, die denselben Wert haben (k, v). Erstellen wir eine "temporĂ€re" Tabelle, deren Generierung keine zusĂ€tzlichen Sperren erfordert:
CREATE TABLE tmp AS SELECT k, v, count(*) x FROM tbl GROUP BY 1, 2; CREATE INDEX ON tmp(k, v);
Nun können wir gemÀà dem oben beschriebenen Modell gemÀà dieser Tabelle iterieren und das Ziel aktualisieren:
UPDATE tbl T SET x = Sx FROM tmp S WHERE (Tk, Tv) = (Sk, Sv) AND (Sk, Sv) = ($1, $2);
Wie Sie sehen, sind keine komplizierten Berechnungen erforderlich.
Denken Sie daran, die Hilfstabelle spÀter zu löschen.