Bei der komplexen Verarbeitung groĂer DatensĂ€tze (verschiedene : Importe, Konvertierungen und Synchronisationen mit externen Quellen) entsteht oft die Notwendigkeit temporĂ€r "zu speichern" und sofort schnell zu verarbeiten etwas Umfangreiches.
Eine typische Aufgabe dieser Art lautet im Allgemeinen etwa so: "Hier hat die Buchhaltung die Doch wenn das Volumen dieses "etwas" in Hunderten von Megabytes gemessen wird und der Dienst dabei 24 Ă 7 mit der Datenbank arbeiten muss, treten viele Nebenwirkungen auf, die Ihr Leben erschweren.
Um mit ihnen in PostgreSQL (und nicht nur darin) umzugehen, kann man einige Optimierungsfunktionen nutzen, die eine schnellere Verarbeitung bei geringerem Ressourcenaufwand ermöglichen.

1. Wo hochladen?
ZunÀchst sollten wir klÀren, wohin wir die Daten hochladen können, die wir "verarbeiten" wollen.
1.1. TemporÀre Tabellen (TEMPORARY TABLE)
Prinzipiell sind temporÀre Tabellen in PostgreSQL ganz normale Tabellen wie alle anderen auch. Daher sind Aberglauben wie
"dort wird alles nur im Speicher gespeichert, und der kann ausgehen" nicht zutreffend. Aber es gibt auch einige wesentliche Unterschiede.Eine eigene "Namespaces" fĂŒr jede Verbindung zur Datenbank
Wenn zwei Verbindungen gleichzeitig versuchen,
CREATE TABLE x , dann wird einer von ihnen definitiveinen Fehler der Nicht-Eindeutigkeit von DB-Objekten erhalten. Wenn jedoch beide versuchen,
, wird jeder dies ordnungsgemÀà durchfĂŒhren und jeder wird CREATE VORĂBERGEHEND TABELLE xseinen eigenen Instanz der Tabelle erhalten. Und es wird nichts Gemeinsames zwischen ihnen geben. "Selbstzerstörung" bei der Trennung
Beim SchlieĂen der Verbindung werden alle temporĂ€ren Tabellen automatisch gelöscht, daher macht es "von Hand"
DROP TABLE x keinen Sinn, auĂer ... Wenn Sie ĂŒber
pgbouncer im Transaktionsmodus , dann denkt die Datenbank weiterhin, dass diese Verbindung noch aktiv ist, und in ihr existiert diese temporĂ€re Tabelle nach wie vor.Deshalb wird der Versuch, sie erneut aus einer anderen Verbindung zu pgbouncer zu erstellen, zu einem Fehler fĂŒhren. Aber das lĂ€sst sich umgehen, indem man
Es ist jedoch besser, so etwas zu vermeiden, da man dann "unerwartet" Daten finden könnte, die vom "vorherigen Besitzer" ĂŒbrig geblieben sind. Stattdessen ist es viel besser, tatsĂ€chlich das Handbuch zu lesen und zu sehen, dass man bei der Erstellung einer Tabelle die Möglichkeit hat, diese zu ergĂ€nzen. TEMPORĂRE TABELLE ERSTELLEN WENN NICHT EXISTIERT x.
NatĂŒrlich ist es besser, dies so nicht zu machen, da man dann âplötzlichâ Daten findet, die vom âvorherigen Besitzerâ ĂŒbrig geblieben sind. Stattdessen ist es viel besser, das Handbuch zu lesen und zu sehen, dass beim Erstellen einer Tabelle die Möglichkeit besteht, ergĂ€nzende Informationen hinzuzufĂŒgen. BEI BESTĂTIGUNG DROP â das heiĂt, nach Abschluss der Transaktion wird die Tabelle automatisch gelöscht.
Nicht-Replikation
Aufgrund der Zugehörigkeit zu einer bestimmten Verbindung werden temporĂ€re Tabellen nicht repliziert. DafĂŒr befreit es von der Notwendigkeit der doppelten Datenspeicherung in Heap + WAL, weshalb INSERT/UPDATE/DELETE darin erheblich schneller ist.
Aber da eine temporÀre Tabelle dennoch eine "fast gewöhnliche" Tabelle ist, kann sie auch auf der Replik nicht erstellt werden. Zumindest im Moment, obwohl der entsprechende Patch schon lange existiert.
1.2. Nicht-protokollierte Tabellen (UNLOGGED TABLE)
Was ist jedoch zu tun, wenn Sie beispielsweise einen ziemlich schwerfÀlligen ETL-Prozess haben, den Sie nicht innerhalb einer einzigen Transaktion realisieren können, und Sie dennoch , dann denkt die Datenbank weiterhin, dass diese Verbindung noch aktiv ist, und in ihr existiert diese temporÀre Tabelle nach wie vor.?..
Oder wenn der Datenstrom so groĂ ist, dass die Bandbreite einer einzigen Verbindung zur DB (sprich, eines Prozesses auf der CPU) nicht ausreicht? ..
Oder wenn Teile der Operationen asynchron in verschiedenen Verbindungen durchgefĂŒhrt werden? ..
Hier gibt es nur eine Möglichkeit â vorĂŒbergehend eine Nicht-TemporĂ€re Tabelle zu erstellen. Wortspiel, ja. Das heiĂt:
- Ich habe "meine" Tabellen mit maximal zufÀlligen Namen erstellt, um nicht mit jemandem in Konflikt zu geraten
- Extraktion: Ich habe Daten aus einer externen Quelle in sie geladen
- Transformation: Ich habe sie umgewandelt und die SchlĂŒsselfelder gefĂŒllt
- mit dem ausgewĂ€hlten Testplan: Ich habe die fertigen Daten in die Zieltabellen ĂŒbertragen
- und meine Tabellen gelöscht
Und jetzt â ein Wermutstropfen. Im Wesentlichen werden alle SchreibvorgĂ€nge in PostgreSQL zweimal durchgefĂŒhrt â , dann in die Tabellen- / Indexkörper. All dies wurde zur UnterstĂŒtzung von ACID und zur korrekten Sichtbarkeit der Daten zwischen COMMITden âgeöffneten und ROLLBACKâgeöffneten Transaktionen.
Aber das brauchen wir nicht! Bei uns ist der gesamte Prozess entweder vollstĂ€ndig erfolgreich oder nicht. Es spielt keine Rolle, wie viele Zwischentransaktionen darin stattfinden â uns interessiert nicht, âden Prozess von der Mitte aus fortzusetzenâ, besonders wenn unklar ist, wo sie war.
DafĂŒr haben die Entwickler von PostgreSQL bereits in Version 9.1 eine Funktion eingefĂŒhrt, wie :
Mit diesem Hinweis wird die Tabelle als nicht-protokolliert erstellt. Die Daten, die in nicht-protokollierte Tabellen geschrieben werden, durchlaufen nicht das Protokoll der Voraufzeichnung (siehe Kapitel 29), wodurch solche Tabellen deutlich schneller arbeiten als gewöhnliche.Sie sind jedoch nicht vor einem Ausfall geschĂŒtzt; bei einem Ausfall oder einem plötzlichen Serverabbruch wird die nicht-protokollierte Tabelle automatisch abgeschnitten.DarĂŒber hinaus wird der Inhalt der nicht-protokollierten Tabelle nicht repliziert. auf die verwalteten Server. Alle Indizes, die fĂŒr eine nicht protokollierte Tabelle erstellt werden, werden automatisch nicht protokolliert.
Kurz gesagt, wird es deutlich schneller sein,, aber wenn der DB-Server âausfĂ€lltâ, wird es unangenehm. Kommt das hĂ€ufig vor, und ist Ihr ETL-Prozess in der Lage, dies korrekt âaus der Mitteâ nach âBelebungâ der DB zu verarbeiten?..
Wenn nicht, und der oben genannte Fall Ihrem Ă€hnelt - verwenden Sie UNLOGGED, aber aktivieren Sie dieses Attribut niemals fĂŒr reale Tabellen , deren Daten Ihnen wichtig sind.1.3. ON COMMIT { DELETE ROWS | DROP }
Diese Konstruktion ermöglicht es, beim Erstellen einer Tabelle ein automatisches Verhalten beim Abschluss der Transaktion festzulegen.
Ich habe bereits oben geschrieben, dass er generiert,
Ăber BEI BESTĂTIGUNG DROP , aber die Situation mit DROP TABLEinteressanter - hier wird erzeugt BEI BESTĂTIGUNG ZEILE LĂSCHEN TRUNCATE TABLE Da die gesamte Infrastruktur zur Speicherung der Metadaten der temporĂ€ren Tabelle genau die gleiche ist wie die einer normalen Tabelle, fĂŒhrt.
das stĂ€ndige Erstellen und Löschen temporĂ€rer Tabellen zu einem starken "AufblĂ€hen" der Systemtabellen pg_class, pg_attribute, pg_attrdef, pg_depend,⊠Stellen Sie sich jetzt vor, Sie haben einen Worker, der ĂŒber eine direkte Verbindung zur DB jede Sekunde eine neue Transaktion öffnet, eine temporĂ€re Tabelle erstellt, fĂŒllt, verarbeitet und löscht⊠Der MĂŒll in den Systemtabellen wird sich gehĂ€uft ansammeln, und das fĂŒhrt zu zusĂ€tzlichen Verzögerungen bei jeder Operation.
Im Allgemeinen sollte man das vermeiden! In diesem Fall ist es viel effizienter,
CREATE TEMPORARY TABLE x ... ON COMMIT DELETE ROWS auĂerhalb des Transaktionszyklus zu halten - dann wird die Tabelle zu Beginn jeder neuen Transaktion bereits existieren (wir sparen den Aufruf ), aber CREATEwird leer sein , dank(auch diesen Aufruf haben wir gespart) beim Abschluss der vorherigen Transaktion. TRUNCATE 1.4. LIKE⊠INCLUDING âŠ
Ich habe zu Beginn erwĂ€hnt, dass einer der typischen AnwendungsfĂ€lle fĂŒr temporĂ€re Tabellen verschiedene Arten von Importen sind - und der Entwickler kopiert und fĂŒgt mĂŒde die Liste der Felder der Zieltabelle in die Deklaration seiner temporĂ€ren Tabelle ein...
Aber Faulheit ist der Motor des Fortschritts! Daher
kann eine neue Tabelle ânach Vorbildâ viel einfacher erstellt werden: CREATE TEMPORARY TABLE import_table( LIKE target_table );
Da in diese Tabelle spĂ€ter ziemlich viele Daten eingefĂŒgt werden können, wird die Suche darin alles andere als schnell sein. Aber dagegen gibt es eine traditionelle Lösung - Indizes! Und ja,auch temporĂ€re Tabellen können Indizes haben. Da oft die benötigten Indizes mit den Indizes der Zieltabelle ĂŒbereinstimmen, kann man einfach schreiben,.
Wenn Sie auch LIKE Ziel_Tabelle EINSCHLIESSLICH INDEXEN.
DEFAULT STANDARD-Werte (zum Beispiel zur BefĂŒllung der Werte des PrimĂ€rschlĂŒssels) kann man nutzen LIKE Ziel_Tabelle EINSCHLIESSLICH DER STANDARDWERT. Oder einfach â LIKE Ziel_Tabelle EINSCHLIESSLICH ALLER â kopiert die Standardwerte, Indizes, Constraints,âŠ
Aber hier muss man schon verstehen, dass wenn Sie die Importtabelle sofort mit Indizes erstellt haben, die Daten lĂ€nger benötigt werden, als wenn man zuerst alles importiert und dann die Indizes hinzufĂŒgt â siehe als Beispiel, wie das .
Im Allgemeinen, !
2. Wie schreiben?
Ich sage einfach â verwenden Sie -Stream statt "Batch" INSERT, . Man kann sogar direkt aus einer vorher formatierten Datei.
3. Wie verarbeiten?
Also, nehmen wir an, dass unser Input ungefÀhr so aussieht:
- Sie haben in Ihrer Datenbank eine Tabelle mit Kundendaten von 1M DatensÀtzen
- jeden Tag sendet der Kunde Ihnen ein neues volles "Abbild"
- Aus Erfahrung wissen Sie, dass sich von Mal zu Mal nicht mehr als 10K DatensÀtze Àndern
Ein klassisches Beispiel fĂŒr eine solche Situation ist â es gibt viele Adressen, aber in jedem wöchentlichen Update an Ănderungen (Umbenennungen von StĂ€dten, Fusionen von StraĂen, das Auftauchen neuer HĂ€user) gibt es insgesamt sehr wenige, selbst im MaĂstab des gesamten Landes.
3.1. Algorithmus zur vollstÀndigen Synchronisierung
Zur Vereinfachung nehmen wir an, dass es nicht nötig ist, die Daten neu zu strukturieren â bringen Sie einfach die Tabelle in die gewĂŒnschte Form, das heiĂt:
- löschen alles, was nicht mehr vorhanden ist
- das System aus dem p9-Zweig des Repositories âSisyphusâ aktualisieren. FĂŒr neue Unternehmenskunden besteht die Möglichkeit, Testversionen zu erhalten, und Privatnutzern wird traditionell angeboten, die benötigte Version des OS âAltâ kostenlos von der Website âBasalt SPOâ oder von der neuen Download-Seite alles, was bereits war und aktualisiert werden muss
- einfĂŒgen alles, was noch nicht vorhanden war
Warum sollte man die Operationen genau in dieser Reihenfolge durchfĂŒhren? Weil die TabellengröĂe auf diese Weise minimal wachsen wird ().
DELETE FROM dst
Nein, natĂŒrlich kann man mit nur zwei Operationen auskommen:
- löschen (
DELETE) tatsĂ€chlich alles - einfĂŒgen alles aus dem neuen Abbild
Aber dabei wird, dank MVCC, die TabellengröĂe genau um das Zweifache wachsen! +1M Abbilden von DatensĂ€tzen in der Tabelle aufgrund von 10K-Updates ist ziemlich ĂŒbertriebenâŠ
TRUNCATE dst
Ein erfahrener Entwickler weiĂ, dass man die gesamte Tabelle relativ kostengĂŒnstig leeren kann:
- bereinigen (
TRUNCATE) die gesamte Tabelle - einfĂŒgen alles aus dem neuen Abbild
Die Methode ist effektiv, , aber es gibt ein Problem⊠1M DatensĂ€tze wird viel Zeit in Anspruch nehmen, sodass wir es uns nicht erlauben können, die Tabelle fĂŒr all die Zeit leer zu lassen (wie es ohne UmschlieĂung in eine einzige Transaktion geschehen wĂŒrde).
Das bedeutet:
- wir beginnen eine lange Transaktion
TRUNCATElegt AccessExclusive- die Sperrung- wir machen lange EinfĂŒgungen, wĂ€hrend alle anderen in der Zwischenzeit nicht einmal
SELECT
Es sieht nicht gut ausâŠ
ALTER TABLE⊠UMBENENNEN⊠/ TABELLE LĂSCHEN âŠ
Eine Möglichkeit wÀre, alles in eine separate neue Tabelle zu laden und dann einfach auf den Platz der alten zu benennen. Ein paar unangenehme Kleinigkeiten:
- doch auch AccessExclusive, wenn auch erheblich weniger zeitaufwendig
- alle AbfrageplĂ€ne / Statistiken dieser Tabelle werden zurĂŒckgesetzt,
- alle FremdschlĂŒssel (FK) auf die Tabelle
Es gab einen WIP-Patch von Simon Riggs, der vorschlug, eine ALTER-Operation fĂŒr den Austausch des Tabellenkörpers auf Dateiebene vorzunehmen, ohne die Statistiken und FK zu berĂŒhren, aber er konnte kein quorum erreichen.
DELETE, UPDATE, INSERT
Also entscheiden wir uns fĂŒr die nicht-blockierende Variante aus drei Operationen. Fast drei⊠Wie lĂ€sst sich das am effizientesten umsetzen?
-- alles im Rahmen einer Transaktion durchfĂŒhren, damit niemand die "zwischenzeitlichen" ZustĂ€nde sieht
BEGIN;
-- erstellen einer temporÀren Tabelle mit importierten Daten
CREATE TEMPORARY TABLE tmp(
LIKE dst INCLUDING INDEXES -- nach dem Vorbild, einschlieĂlich Indizes
) ON COMMIT DROP; -- auĂerhalb der Transaktion benötigen wir sie nicht
-- schnell neuen Datensatz per COPY einfĂŒgen
COPY tmp FROM STDIN;
-- ...
-- .
-- fehlende DatensÀtze entfernen
DELETE FROM
dst D
USING
dst X
LEFT JOIN
tmp Y
USING(pk1, pk2) -- Felder des PrimĂ€rschlĂŒssels
WHERE
(D.pk1, D.pk2) = (X.pk1, X.pk2) AND
Y IS NOT DISTINCT FROM NULL; -- "Anti-Join"
-- verbleibende aktualisieren
UPDATE
dst D
SET
(f1, f2, f3) = (T.f1, T.f2, T.f3)
FROM
tmp T
WHERE
(D.pk1, D.pk2) = (T.pk1, T.pk2) AND
(D.f1, D.f2, D.f3) IS DISTINCT FROM (T.f1, T.f2, T.f3); -- unnötig Ăbereinstimmungen zu aktualisieren
-- fehlende einfĂŒgen
INSERT INTO
dst
SELECT
T.*
FROM
tmp T
LEFT JOIN
dst D
USING(pk1, pk2)
WHERE
D IS NOT DISTINCT FROM NULL;
COMMIT;
3.2. Nachverarbeitung des Imports
Im gleichen KLRAD mĂŒssen alle geĂ€nderten EintrĂ€ge zusĂ€tzlich durch die Nachbearbeitung laufen â normalisiert, SchlĂŒsselwörter extrahiert, in die erforderlichen Strukturen gebracht. Aber wie kann man herausfinden â was genau geĂ€ndert wurde, ohne dabei den Synchronisierungscode zu verkomplizieren, idealerweise ĂŒberhaupt nicht zu berĂŒhren?
Wenn nur Ihr Prozess zur Synchronisierungszeit Schreibzugriff hat, können Sie einen Trigger verwenden, der alle Ănderungen fĂŒr uns erfasst:
-- Zieltabelle
CREATE TABLE kladr(...);
CREATE TABLE kladr_house(...);
-- Tabellen mit Ănderungsprotokoll
CREATE TABLE kladr$log(
ro kladr, -- hier liegen vollstÀndige Abbilder der alten/neuen EintrÀge
rn kladr
);
CREATE TABLE kladr_house$log(
ro kladr_house,
rn kladr_house
);
-- allgemeine Funktion zur Protokollierung von Ănderungen
CREATE OR REPLACE FUNCTION diff$log() RETURNS trigger AS $$
DECLARE
dst varchar = TG_TABLE_NAME || '$log';
stmt text = '';
BEGIN
-- ĂberprĂŒfen, ob Protokollierung bei der Aktualisierung des Eintrags erforderlich ist
IF TG_OP = 'UPDATE' THEN
IF NEW IS NOT DISTINCT FROM OLD THEN
RETURN NEW;
END IF;
END IF;
-- Eintrag im Protokoll erstellen
stmt = 'INSERT INTO ' || dst::text || '(ro,rn)VALUES(';
CASE TG_OP
WHEN 'INSERT' THEN
EXECUTE stmt || 'NULL,$1)' USING NEW;
WHEN 'UPDATE' THEN
EXECUTE stmt || '$1,$2)' USING OLD, NEW;
WHEN 'DELETE' THEN
EXECUTE stmt || '$1,NULL)' USING OLD;
END CASE;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Jetzt können wir die Trigger vor Beginn der Synchronisierung anwenden (oder aktivieren ĂŒber ALTER TABLE ... ENABLE TRIGGER ...):
CREATE TRIGGER log
AFTER INSERT OR UPDATE OR DELETE
ON kladr
FOR EACH ROW
EXECUTE PROCEDURE diff$log();
CREATE TRIGGER log
AFTER INSERT OR UPDATE OR DELETE
ON kladr_house
FOR EACH ROW
EXECUTE PROCEDURE diff$log();
Und dann extrahieren wir einfach aus den log-Tabellen alle erforderlichen Ănderungen und schicken sie an zusĂ€tzliche Bearbeitungsmechanismen.
3.3. Import von verknĂŒpften DatensĂ€tzen
Wir haben oben FĂ€lle betrachtet, in denen die Datenstrukturen von Quelle und EmpfĂ€nger ĂŒbereinstimmen. Aber was tun, wenn der Export aus dem externen System ein Format hat, das von der Speicherung in unserer Datenbank abweicht?
Nehmen wir als Beispiel die Speicherung von Kunden und deren Rechnungen, den klassischen Fall «viele-zu-eins»:
CREATE TABLE client(
client_id
serial
PRIMARY KEY
, inn
varchar
UNIQUE
, name
varchar
);
CREATE TABLE invoice(
invoice_id
serial
PRIMARY KEY
, client_id
integer
REFERENCES client(client_id)
, number
varchar
, dt
date
, sum
numeric(32,2)
);Und die Daten von der externen Quelle kommen bei uns im Format 'alles in einem':
CREATE TEMPORARY TABLE invoice_import(
client_inn
varchar
, client_name
varchar
, invoice_number
varchar
, invoice_dt
date
, invoice_sum
numeric(32,2)
);Offensichtlich können die Kundendaten in diesem Format dupliziert werden, wobei die Hauptzeile 'Rechnung' ist:
0123456789;Wasi;A-01;2020-03-16;1000.00
9876543210;Petja;A-02;2020-03-16;666.00
0123456789;Wasi;B-03;2020-03-16;9999.00
FĂŒr das Modell fĂŒgen wir einfach unsere Testdaten ein, aber denken wir daran â COPY effizienter!
INSERT INTO invoice_import
VALUES
('0123456789', 'Wasi', 'A-01', '2020-03-16', 1000.00)
, ('9876543210', 'Petja', 'A-02', '2020-03-16', 666.00)
, ('0123456789', 'Wasi', 'B-03', '2020-03-16', 9999.00);Zuerst identifizieren wir die 'Schnitte', auf die sich unsere 'Fakten' beziehen. In unserem Fall beziehen sich Rechnungen auf Kunden:
ERSTELLE TEMPORĂRE TABELLE client_import WIE
WĂHLE Eindeutig AUF(client_inn)
-- man kann einfach SELECT DISTINCT verwenden, wenn die Daten bereits konsistent sind
client_inn inn
, client_name "name"
FROM
invoice_import;Um die Rechnungen korrekt mit den Kunden-IDs zu verknĂŒpfen, mĂŒssen wir diese Identifikatoren zunĂ€chst herausfinden oder generieren. Lassen Sie uns dafĂŒr Felder hinzufĂŒgen:
ALTER TABLE invoice_import HINZUFĂGEN SPALTE client_id integer;
ALTER TABLE client_import HINZUFĂGEN SPALTE client_id integer;Wir werden die oben beschriebene Methode zur Synchronisierung der Tabellen mit einer kleinen Anpassung verwenden â wir werden nichts in der Zieltabelle aktualisieren oder löschen, da der Import von Kunden bei uns âappend-onlyâ ist:
-- wir setzen in der Importtabelle die IDs der bereits vorhandenen DatensÀtze
UPDATE
client_import T
SET
client_id = D.client_id
FROM
client D
WHERE
T.inn = D.inn; -- eindeutiger SchlĂŒssel
-- fĂŒgen Sie die fehlenden DatensĂ€tze ein und setzen Sie deren IDs
MIT ins ALS (
INSERT INTO client(
inn
, name
)
WĂHLE
inn
, name
FROM
client_import
WHERE
client_id IS NULL -- wenn die ID nicht gesetzt wurde
RETURNING *
)
UPDATE
client_import T
SET
client_id = D.client_id
FROM
ins D
WHERE
T.inn = D.inn; -- eindeutiger SchlĂŒssel
-- wir setzen die Kunden-IDs bei den RechnungsdatensÀtzen
UPDATE
invoice_import T
SET
client_id = D.client_id
FROM
client_import D
WHERE
T.client_inn = D.inn; -- anwendungsbezogener SchlĂŒssel
Im Grunde ist alles â in invoice_import ist jetzt das VerknĂŒpfungsfeld ausgefĂŒllt client_id, mit dem wir die Rechnung einfĂŒgen werden.
Quelle: habr.com
