PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Eine Anleitung zum Partitionieren von Daten in PostgreSQL

Was ist Datenpartitionierung?

Bei Datenbanken mit extrem großen Tabellen ist die Partitionierung ein wunderbarer und ausgeklügelter Trick für Datenbankdesigner, um die Datenbankleistung zu verbessern und die Wartung erheblich zu vereinfachen. Die maximal zulässige Tabellengröße in einer PostgreSQL-Datenbank beträgt 32 TB. Wenn sie jedoch nicht auf einem noch nicht erfundenen Computer aus der Zukunft ausgeführt wird, können Leistungsprobleme bei einer Tabelle mit nur einem Hundertstel dieses Speicherplatzes auftreten.

Die Partitionierung teilt eine Tabelle in mehrere Tabellen auf und erfolgt im Allgemeinen so, dass Anwendungen, die auf die Tabelle zugreifen, keinen Unterschied bemerken, außer dass sie schneller auf die benötigten Daten zugreifen. Durch das Aufteilen der Tabelle in mehrere Tabellen soll ermöglicht werden, dass die Ausführung der Abfragen viel kleinere Tabellen und Indizes scannen muss, um die benötigten Daten zu finden. Unabhängig davon, wie effizient eine Indexstrategie ist, wird das Scannen eines Indexes für eine Tabelle mit 50 GB immer viel schneller sein als ein Index für eine Tabelle mit 500 GB. Dies gilt auch für Tabellenscans, denn manchmal sind Tabellenscans einfach unvermeidlich.

Wenn Sie eine partitionierte Tabelle in den Abfrageplaner einführen, müssen Sie einige Dinge über den Abfrageplaner selbst wissen und verstehen. Bevor eine Abfrage tatsächlich ausgeführt wird, nimmt der Abfrageplaner die Abfrage und plant, wie er am effizientesten auf die Daten zugreift. Indem die Daten auf verschiedene Tabellen aufgeteilt werden, kann der Planer entscheiden, auf welche Tabellen zugegriffen werden soll und welche Tabellen vollständig ignoriert werden sollen, je nachdem, was jede Tabelle enthält.

Dies geschieht durch Hinzufügen von Einschränkungen zu den aufgeteilten Tabellen, die definieren, welche Daten in jeder Tabelle zulässig sind, und mit einem guten Design können wir den Abfrageplaner eine kleine Teilmenge von Daten scannen lassen, anstatt das Ganze.

Sollte eine Tabelle partitioniert werden?

Partitionierung kann die Leistung einer Tabelle drastisch verbessern, wenn sie richtig gemacht wird, aber wenn sie falsch gemacht wird oder nicht benötigt wird, kann sie die Leistung verschlechtern oder sogar unbrauchbar machen.

Wie groß ist der Tisch?

Es gibt keine wirkliche feste Regel dafür, wie groß eine Tabelle sein muss, bevor Partitionierung eine Option ist, aber basierend auf Datenbankzugriffstrends werden Datenbankbenutzer und Administratoren feststellen, dass die Leistung einer bestimmten Tabelle abnimmt, wenn sie größer wird. Im Allgemeinen sollte eine Partitionierung nur in Betracht gezogen werden, wenn jemand sagt „Ich kann X nicht, weil die Tabelle zu groß ist“. Für einige Hosts könnten 200 GB der richtige Zeitpunkt für die Partitionierung sein, für andere ist es möglicherweise an der Zeit, zu partitionieren, wenn 1 TB erreicht werden.

Wenn festgestellt wird, dass die Tabelle „zu groß“ ist, ist es an der Zeit, sich die Zugriffsmuster anzusehen. Entweder indem wir die Anwendungen kennen, die auf die Datenbank zugreifen, oder indem wir Protokolle überwachen und Abfrageberichte mit etwas wie pgBadger erstellen, können wir sehen, wie auf eine Tabelle zugegriffen wird, und je nachdem, wie darauf zugegriffen wird, können wir Optionen für eine gute Partitionierungsstrategie haben /P>

Um mehr über pgBadger und seine Verwendung zu erfahren, lesen Sie bitte unseren vorherigen Artikel über pgBadger.

Ist Tabellenaufblähung ein Problem?

Aktualisierte und gelöschte Zeilen führen zu toten Tupeln, die letztendlich bereinigt werden müssen. Das Staubsaugen von Tischen, ob manuell oder automatisch, geht jede Zeile in der Tabelle durch und bestimmt, ob sie zurückgefordert oder in Ruhe gelassen werden soll. Je größer die Tabelle, desto länger dauert dieser Vorgang und desto mehr Systemressourcen werden verwendet. Selbst wenn 90 % einer Tabelle aus unveränderlichen Daten bestehen, müssen sie bei jeder Bereinigung gescannt werden. Das Partitionieren der Tabelle kann dazu beitragen, die zu bereinigende Tabelle auf kleinere zu reduzieren, die Menge der zu scannenden unveränderlichen Daten zu reduzieren, insgesamt weniger Zeit für das Beräumen zu haben und mehr Systemressourcen für den Benutzerzugriff statt für die Systemwartung freizugeben.

Wie werden Daten gelöscht, wenn überhaupt?

Wenn Daten nach einem Zeitplan gelöscht werden, z. B. wenn Daten älter als 4 Jahre gelöscht und archiviert werden, kann dies zu schwerwiegenden Löschanweisungen führen, deren Ausführung einige Zeit in Anspruch nehmen kann, und wie bereits erwähnt, zu toten Zeilen, die bereinigt werden müssen. Wenn eine gute Partitionierungsstrategie implementiert wird, könnte eine mehrstündige DELETE-Anweisung mit anschließender Vakuumwartung in eine einminütige DROP TABLE-Anweisung auf einer alten Monatstabelle ohne Vakuumwartung umgewandelt werden.

Wie soll die Tabelle partitioniert werden?

Die Schlüssel für Zugriffsmuster befinden sich in der WHERE-Klausel und den JOIN-Bedingungen. Jedes Mal, wenn eine Abfrage Spalten in den WHERE- und JOIN-Klauseln angibt, teilt sie der Datenbank mit, „das sind die Daten, die ich will“. Ähnlich wie beim Entwerfen von Indizes, die auf diese Klauseln abzielen, verlassen sich Partitionierungsstrategien darauf, diese Spalten als Ziel zu verwenden, um Daten zu trennen und die Abfrage auf so wenige Partitionen wie möglich zugreifen zu lassen.

Beispiele:

  1. Eine Transaktionstabelle mit einer Datumsspalte, die immer in einer Where-Klausel verwendet wird.
  2. Eine Kundentabelle mit Standortspalten, wie z. B. dem Wohnsitzland, das immer in where-Klauseln verwendet wird.

Die gebräuchlichsten Spalten, auf die man sich bei der Partitionierung konzentrieren sollte, sind in der Regel Zeitstempel, da es sich bei einem großen Datenblock normalerweise um historische Informationen handelt, die wahrscheinlich ziemlich vorhersehbare Daten aufweisen, die über verschiedene Zeitgruppen verteilt sind.

Bestimmen Sie die Datenverteilung

Sobald wir identifiziert haben, auf welche Spalten partitioniert werden soll, sollten wir uns die Verteilung der Daten ansehen, mit dem Ziel, Partitionsgrößen zu erstellen, die die Daten so gleichmäßig wie möglich über die verschiedenen untergeordneten Partitionen verteilen.

severalnines=# SELECT DATE_TRUNC('year', view_date)::DATE, COUNT(*) FROM website_views GROUP BY 1 ORDER BY 1;
 date_trunc |  count
------------+----------
 2013-01-01 | 11625147
 2014-01-01 | 20819125
 2015-01-01 | 20277739
 2016-01-01 | 20584545
 2017-01-01 | 20777354
 2018-01-01 |   491002
(6 rows)

In diesem Beispiel kürzen wir die timestamp-Spalte auf eine Jahrestabelle, was zu etwa 20 Millionen Zeilen pro Jahr führt. Wenn alle unsere Abfragen ein Datum oder mehrere Datumsbereiche angeben und die angegebenen normalerweise Daten innerhalb eines einzelnen Jahres abdecken, kann dies eine gute Startstrategie für die Partitionierung sein, da dies zu einer einzigen Tabelle pro Jahr führen würde , mit einer überschaubaren Anzahl von Zeilen pro Tabelle.

Laden Sie noch heute das Whitepaper PostgreSQL-Verwaltung und -Automatisierung mit ClusterControl herunterErfahren Sie, was Sie wissen müssen, um PostgreSQL bereitzustellen, zu überwachen, zu verwalten und zu skalierenLaden Sie das Whitepaper herunter

Erstellen einer partitionierten Tabelle

Es gibt ein paar Möglichkeiten, partitionierte Tabellen zu erstellen, wir konzentrieren uns jedoch hauptsächlich auf den funktionsreichsten Typ, der verfügbar ist, die Trigger-basierte Partitionierung. Dies erfordert eine manuelle Einrichtung und ein wenig Codierung in der prozeduralen Sprache plpgsql, um zu funktionieren.

Es funktioniert, indem es eine übergeordnete Tabelle hat, die letztendlich leer wird (oder leer bleibt, wenn es sich um eine neue Tabelle handelt), und untergeordnete Tabellen, die die übergeordnete Tabelle erben. Wenn die übergeordnete Tabelle abgefragt wird, werden die untergeordneten Tabellen aufgrund des auf die untergeordneten Tabellen angewendeten INHERIT ebenfalls nach Daten durchsucht. Da untergeordnete Tabellen jedoch nur Teilmengen der übergeordneten Daten enthalten, fügen wir der Tabelle eine EINSCHRÄNKUNG hinzu, die eine Prüfung durchführt und überprüft, ob die Daten mit den in der Tabelle zulässigen Daten übereinstimmen. Dies bewirkt zwei Dinge:Erstens weist es Daten zurück, die nicht dazugehören, und zweitens teilt es dem Abfrageplaner mit, dass nur Daten, die mit dieser CHECK CONSTRAINT übereinstimmen, in dieser Tabelle zulässig sind. Wenn Sie also nach Daten suchen, die nicht mit der Tabelle übereinstimmen, tun Sie dies nicht Suchen Sie nicht einmal danach.

Zuletzt wenden wir einen Trigger auf die übergeordnete Tabelle an, der eine gespeicherte Prozedur ausführt, die entscheidet, welche untergeordnete Tabelle die Daten ablegen soll.

Tabelle erstellen

Das Erstellen der übergeordneten Tabelle ist wie jede andere Tabellenerstellung.

severalnines=# CREATE TABLE data_log (data_log_sid SERIAL PRIMARY KEY,
  date TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
  event_details VARCHAR);
CREATE TABLE

Untergeordnete Tabellen erstellen

Das Erstellen der untergeordneten Tabellen ist ähnlich, erfordert jedoch einige Ergänzungen. Aus organisatorischen Gründen lassen wir unsere untergeordneten Tabellen in einem separaten Schema existieren. Tun Sie dies für jede untergeordnete Tabelle und ändern Sie die Details entsprechend.

HINWEIS:Der Name der Sequenz, die in nextval() verwendet wird, stammt von der Sequenz, die das übergeordnete Element erstellt hat. Dies ist entscheidend, damit alle untergeordneten Tabellen die gleiche Sequenz verwenden.

severalnines=# CREATE SCHEMA part;
CREATE SCHEMA

severalnines=# CREATE TABLE part.data_log_2018 (data_log_sid integer DEFAULT nextval('public.data_log_data_log_sid_seq'::regclass),
  date TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
  event_details VARCHAR)
 INHERITS (public.data_log);
CREATE TABLE

severalnines=# ALTER TABLE ONLY part.data_log_2018
    ADD CONSTRAINT data_log_2018_pkey PRIMARY KEY (data_log_sid);
ALTER TABLE

severalnines=# ALTER TABLE part.data_log_2018 ADD CONSTRAINT data_log_2018_date CHECK (date >= '2018-01-01' AND date < '2019-01-01');
ALTER TABLE

Funktion und Trigger erstellen

Schließlich erstellen wir unsere gespeicherte Prozedur und fügen den Trigger unserer übergeordneten Tabelle hinzu.

severalnines=# CREATE OR REPLACE FUNCTION 
 public.insert_trigger_table()
  RETURNS trigger
  LANGUAGE plpgsql
 AS $function$
 BEGIN
     IF NEW.date >= '2018-01-01' AND NEW.date < '2019-01-01' THEN
         INSERT INTO part.data_log_2018 VALUES (NEW.*);
         RETURN NULL;
     ELSIF NEW.date >= '2019-01-01' AND NEW.date < '2020-01-01' THEN
         INSERT INTO part.data_log_2019 VALUES (NEW.*);
         RETURN NULL;
     END IF;
 END;
 $function$;
CREATE FUNCTION

severalnines=# CREATE TRIGGER insert_trigger BEFORE INSERT ON data_log FOR EACH ROW EXECUTE PROCEDURE insert_trigger_table();
CREATE TRIGGER

Testen Sie es aus

Nun, da alles erstellt ist, testen wir es. In diesem Test habe ich weitere Jahrestabellen für die Jahre 2013–2020 hinzugefügt.

Hinweis:Die nachstehende Einfügungsantwort lautet „INSERT 0 0“, was darauf hindeuten würde, dass nichts eingefügt wurde. Darauf wird später in diesem Artikel eingegangen.

severalnines=# INSERT INTO data_log (date, event_details) VALUES ('2018-08-20 15:22:14', 'First insert');
INSERT 0 0

severalnines=# SELECT * FROM data_log WHERE date >= '2018-08-01' AND date < '2018-09-01';
 data_log_sid |            date            | event_details
--------------+----------------------------+---------------
            1 | 2018-08-17 23:01:38.324056 | First insert
(1 row)

Es existiert, aber schauen wir uns den Abfrageplaner an, um sicherzustellen, dass die Zeile aus der richtigen untergeordneten Tabelle stammt und die übergeordnete Tabelle überhaupt keine Zeilen zurückgibt.

severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log;
                                                    QUERY PLAN
------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..130.12 rows=5813 width=44) (actual time=0.016..0.019 rows=1 loops=1)
   ->  Seq Scan on data_log  (cost=0.00..1.00 rows=1 width=44) (actual time=0.007..0.007 rows=0 loops=1)
   ->  Seq Scan on data_log_2015  (cost=0.00..21.30 rows=1130 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2013  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2014  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2016  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2017  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2018  (cost=0.00..1.02 rows=2 width=44) (actual time=0.005..0.005 rows=1 loops=1)
   ->  Seq Scan on data_log_2019  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
   ->  Seq Scan on data_log_2020  (cost=0.00..17.80 rows=780 width=44) (actual time=0.001..0.001 rows=0 loops=1)
 Planning time: 0.373 ms
 Execution time: 0.069 ms
(12 rows)

Gute Nachrichten, die einzelne Zeile, die wir eingefügt haben, ist in der Tabelle 2018 gelandet, wo sie hingehört. Aber wie wir sehen können, gibt die Abfrage keine where-Klausel unter Verwendung der Datumsspalte an. Um also alles abzurufen, haben der Abfrageplaner und die Ausführung einen sequenziellen Scan für jede einzelne Tabelle durchgeführt.

Als Nächstes testen wir die Verwendung einer where-Klausel.

severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log WHERE date >= '2018-08-01' AND date < '2018-09-01';
                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..2.03 rows=2 width=44) (actual time=0.013..0.014 rows=1 loops=1)
   ->  Seq Scan on data_log  (cost=0.00..1.00 rows=1 width=44) (actual time=0.007..0.007 rows=0 loops=1)
         Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
   ->  Seq Scan on data_log_2018  (cost=0.00..1.03 rows=1 width=44) (actual time=0.006..0.006 rows=1 loops=1)
         Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
 Planning time: 0.591 ms
 Execution time: 0.041 ms
(7 rows)

Hier können wir sehen, dass der Abfrageplaner und die Ausführung einen sequenziellen Scan an zwei Tabellen durchgeführt haben, der übergeordneten und der untergeordneten Tabelle für 2018. Es gibt untergeordnete Tabellen für die Jahre 2013 bis 2020, aber auf andere als 2018 wurde nie zugegriffen, weil die where-Klausel hat einen Bereich, der nur innerhalb von 2018 liegt. Der Abfrageplaner hat alle anderen Tabellen ausgeschlossen, da die CHECK CONSTRAINT es für unmöglich hält, dass die Daten in diesen Tabellen vorhanden sind.

Arbeitende Partitionen mit strengen ORM-Tools oder eingefügter Zeilenvalidierung

Wie bereits erwähnt, gibt das von uns erstellte Beispiel „INSERT 0 0“ zurück, obwohl wir eine Zeile eingefügt haben. Wenn die Anwendungen, die Daten in diese partitionierten Tabellen einfügen, darauf angewiesen sind, zu überprüfen, ob die eingefügten Zeilen korrekt sind, schlagen diese fehl. Es gibt eine Lösung, die der partitionierten Tabelle jedoch eine weitere Ebene der Komplexität hinzufügt und daher ignoriert werden kann, wenn dieses Szenario kein Problem für die Anwendungen darstellt, die die partitionierte Tabelle verwenden.

Eine Ansicht anstelle der übergeordneten Tabelle verwenden.

Die Lösung für dieses Problem besteht darin, eine Ansicht zu erstellen, die die übergeordnete Tabelle abfragt, und INSERT-Anweisungen an die Ansicht weiterzuleiten. Das Einfügen in eine Ansicht mag verrückt klingen, aber genau hier kommt der Auslöser für die Ansicht ins Spiel.

severalnines=# CREATE VIEW data_log_view AS 
 SELECT data_log.data_log_sid,
     data_log.date,
     data_log.event_details
    FROM data_log;
CREATE VIEW

severalnines=# ALTER VIEW data_log_view ALTER COLUMN data_log_sid SET default nextval('data_log_data_log_sid_seq'::regclass);
ALTER VIEW

Das Abfragen dieser Ansicht sieht genauso aus wie das Abfragen der Haupttabelle, und WHERE-Klauseln sowie JOINS funktionieren wie erwartet.

Spezifische Funktion und Auslöser anzeigen

Anstatt die zuvor definierte Funktion und den Trigger zu verwenden, unterscheiden sich beide geringfügig. Änderungen in Fettdruck.

CREATE OR REPLACE FUNCTION public.insert_trigger_view()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
    IF NEW.date >= '2018-01-01' AND NEW.date < '2019-01-01' THEN
        INSERT INTO part.data_log_2018 VALUES (NEW.*);
        RETURN NEW;

    ELSIF NEW.date >= '2019-01-01' AND NEW.date < '2020-01-01' THEN
        INSERT INTO part.data_log_2019 VALUES (NEW.*);
        RETURN NEW;

    END IF;
END;
$function$;

severalnines=# CREATE TRIGGER insert_trigger INSTEAD OF INSERT ON data_log_view FOR EACH ROW EXECUTE PROCEDURE insert_trigger_view();

Die „INSTEAD OF“-Definition übernimmt den Einfügebefehl für die Ansicht (was sowieso nicht funktionieren würde) und führt stattdessen die Funktion aus. Die von uns definierte Funktion hat eine sehr spezifische Anforderung, ein „RETURN NEW;“ auszuführen, nachdem das Einfügen in die untergeordneten Tabellen abgeschlossen ist. Ohne dies (oder wie zuvor mit „RETURN NULL“) wird „INSERT 0 0“ anstelle von „INSERT 0 1“ ausgegeben, wie wir es erwarten würden.

Beispiel:

severalnines=# INSERT INTO data_log_view (date, event_details) VALUES ('2018-08-20 18:12:48', 'First insert on the view');
INSERT 0 1

severalnines=# EXPLAIN ANALYZE SELECT * FROM data_log_view WHERE date >= '2018-08-01' AND date < '2018-09-01';
                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Append  (cost=0.00..2.03 rows=2 width=44) (actual time=0.015..0.017 rows=2 loops=1)
   ->  Seq Scan on data_log  (cost=0.00..1.00 rows=1 width=44) (actual time=0.009..0.009 rows=0 loops=1)
         Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
   ->  Seq Scan on data_log_2018  (cost=0.00..1.03 rows=1 width=44) (actual time=0.006..0.007 rows=2 loops=1)
         Filter: ((date >= '2018-08-01 00:00:00'::timestamp without time zone) AND (date < '2018-09-01 00:00:00'::timestamp without time zone))
 Planning time: 0.633 ms
 Execution time: 0.048 ms
(7 rows)

severalnines=# SELECT * FROM data_log_view WHERE date >= '2018-08-01' AND date < '2018-09-01';
 data_log_sid |        date         |      event_details
--------------+---------------------+--------------------------
            1 | 2018-08-20 15:22:14 | First insert
            2 | 2018-08-20 18:12:48 | First insert on the view
(2 rows)

Anwendungen, die auf Richtigkeit des eingefügten „rowcount“ testen, werden feststellen, dass dieser Fix wie erwartet funktioniert. In diesem Beispiel haben wir _view an unsere Ansicht und gespeicherte Prozedur angehängt, aber wenn die Tabelle partitioniert werden soll, ohne dass Benutzer etwas davon wissen/Anwendung ändern, dann würden wir die übergeordnete Tabelle in data_log_parent umbenennen und die Ansicht mit der alten aufrufen Name der übergeordneten Tabelle.

Aktualisieren einer Zeile und Ändern des partitionierten Spaltenwerts

Beachten Sie Folgendes:Wenn Sie eine Aktualisierung der Daten in der partitionierten Tabelle durchführen und den Wert der Spalte auf einen Wert ändern, der von der Einschränkung nicht zugelassen wird, führt dies zu einem Fehler. Wenn diese Art von Aktualisierung nie stattfindet, kann sie ignoriert werden, aber wenn es möglich ist, sollte ein neuer Trigger für UPDATE-Prozesse geschrieben werden, der die Zeile effektiv aus der alten untergeordneten Partition löscht und eine neue in die einfügt neue untergeordnete Zielpartition.

Zukünftige Partitionen erstellen

Das Erstellen zukünftiger Partitionen kann auf verschiedene Arten erfolgen, jede mit ihren Vor- und Nachteilen.

Zukünftiger Partitionsersteller

Ein externes Programm kann geschrieben werden, um zukünftige Partitionen X-mal zu erstellen, bevor sie benötigt werden. In einem Partitionierungsbeispiel, das an einem Datum partitioniert ist, könnte die nächste zu erstellende Partition (in unserem Fall 2019) so eingestellt werden, dass sie irgendwann im Dezember erstellt wird. Dies kann ein manuelles Skript sein, das vom Datenbankadministrator ausgeführt wird, oder so eingestellt werden, dass Cron es bei Bedarf ausführt. Jährliche Partitionen würden bedeuten, dass es einmal im Jahr ausgeführt wird, tägliche Partitionen sind jedoch üblich, und ein täglicher Cron-Job sorgt für einen glücklicheren DBA.

Automatischer Partitionsersteller

Mit der Leistungsfähigkeit von plpgsql können wir Fehler erfassen, wenn wir versuchen, Daten in eine untergeordnete Partition einzufügen, die nicht existiert, und die benötigte Partition im Handumdrehen erstellen und dann das Einfügen erneut versuchen. Diese Option funktioniert gut, außer wenn viele verschiedene Clients gleichzeitig ähnliche Daten einfügen und eine Race-Condition verursachen könnten, bei der ein Client die Tabelle erstellt, während ein anderer versucht, dieselbe Tabelle zu erstellen, und eine Fehlermeldung erhält, dass sie bereits vorhanden ist. Clevere und fortschrittliche plpgsql-Programmierung kann dies beheben, aber ob es den Aufwand wert ist oder nicht, steht zur Debatte. Wenn diese Rennbedingung aufgrund der Einfügemuster nicht eintritt, brauchen Sie sich keine Sorgen zu machen.

Partitionen löschen

Wenn Datenaufbewahrungsregeln vorschreiben, dass Daten nach einer bestimmten Zeit gelöscht werden, wird dies mit partitionierten Tabellen einfacher, wenn sie nach einer Datumsspalte partitioniert werden. Wenn wir Daten löschen sollen, die 10 Jahre alt sind, könnte es so einfach sein wie:

severalnines=# DROP TABLE part.data_log_2007;
DROP TABLE

Dies ist viel schneller und effizienter als eine 'DELETE'-Anweisung, da es nicht zu toten Tupeln führt, die mit einem Vakuum aufgeräumt werden müssen.

Hinweis:Wenn Sie Tabellen aus dem Partitions-Setup entfernen, sollte der Code in den Trigger-Funktionen ebenfalls geändert werden, um das Datum nicht an die gelöschte Tabelle weiterzuleiten.

Was Sie vor der Partitionierung wissen sollten

Das Partitionieren von Tabellen kann die Leistung drastisch verbessern, aber auch verschlechtern. Vor dem Pushen auf Produktionsserver sollte die Partitionierungsstrategie ausgiebig getestet werden, auf Datenkonsistenz, Leistungsgeschwindigkeit, alles. Das Partitionieren einer Tabelle hat einige bewegliche Teile, die alle getestet werden sollten, um sicherzustellen, dass es keine Probleme gibt.

Wenn es um die Festlegung der Anzahl der Partitionen geht, wird dringend empfohlen, die Anzahl der untergeordneten Tabellen unter 1000 Tabellen zu halten und wenn möglich sogar noch niedriger. Sobald die Anzahl der untergeordneten Tabellen über ~1000 steigt, beginnt die Leistung zu sinken, da der Abfrageplaner selbst viel länger braucht, um nur den Abfrageplan zu erstellen. Es ist nicht ungewöhnlich, dass ein Abfrageplan viele Sekunden dauert, während die eigentliche Ausführung nur wenige Millisekunden dauert. Wenn Tausende von Abfragen pro Minute verarbeitet werden, können mehrere Sekunden Anwendungen zum Erliegen bringen.

Die gespeicherten Prozeduren des plpgsql-Triggers können ebenfalls kompliziert werden und, wenn sie zu kompliziert sind, auch die Leistung verlangsamen. Die gespeicherte Prozedur wird einmal für jede in die Tabelle eingefügte Zeile ausgeführt. Wenn es am Ende zu viel Verarbeitung für jede Zeile macht, könnten Einfügungen zu langsam werden. Durch Leistungstests wird sichergestellt, dass es sich immer noch im akzeptablen Bereich befindet.

Werden Sie kreativ

Das Partitionieren von Tabellen in PostgreSQL kann so weit fortgeschritten sein wie nötig. Anstelle von Datumsspalten können Tabellen nach einer „Länder“-Spalte partitioniert werden, mit einer Tabelle für jedes Land. Die Partitionierung kann für mehrere Spalten erfolgen, z. B. sowohl für eine „Datum“- als auch für eine „Land“-Spalte. Dadurch wird die gespeicherte Prozedur zur Handhabung der Einfügungen komplexer, aber es ist zu 100 % möglich.

Denken Sie daran, dass das Ziel der Partitionierung darin besteht, extrem große Tabellen in kleinere aufzuteilen, und zwar auf eine gut durchdachte Weise, damit der Abfrageplaner schneller auf die Daten zugreifen kann, als dies in der größeren Originaltabelle der Fall gewesen wäre.

Deklarative Partitionierung

In PostgreSQL 10 und höher wurde eine neue Partitionierungsfunktion „Declarative Partitioning“ eingeführt. Es ist eine einfachere Methode zum Einrichten von Partitionen, weist jedoch einige Einschränkungen auf. Wenn die Einschränkungen akzeptabel sind, wird es wahrscheinlich schneller ausgeführt als die manuelle Einrichtung von Partitionen, aber zahlreiche Tests werden dies bestätigen.

Die offizielle postgresql-Dokumentation enthält Informationen zur deklarativen Partitionierung und wie sie funktioniert. Es ist neu in PostgreSQL 10, und mit Version 11 von PostgreSQL zum Zeitpunkt des Schreibens dieses Artikels sind einige der Einschränkungen behoben, aber nicht alle. Mit der Weiterentwicklung von PostgreSQL kann die deklarative Partitionierung ein vollständiger Ersatz für die komplexere Partitionierung werden, die in diesem Artikel behandelt wird. Bis dahin kann die deklarative Partitionierung eine einfachere Alternative sein, wenn keine der Einschränkungen die Partitionierungsanforderungen einschränkt.

Einschränkungen der deklarativen Partitionierung

Die PostgreSQL-Dokumentation befasst sich mit allen Einschränkungen bei dieser Art der Partitionierung in PostgreSQL 10, aber eine großartige Übersicht finden Sie im offiziellen PostgreSQL-Wiki, das die Einschränkungen in einem leichter lesbaren Format auflistet und angibt, welche behoben wurden das kommende PostgreSQL 11.

Fragen Sie die Community

Datenbankadministratoren auf der ganzen Welt entwerfen seit langem fortschrittliche und benutzerdefinierte Partitionierungsstrategien, und viele von uns hängen im IRC und in Mailinglisten herum. Wenn Sie Hilfe bei der Entscheidung für die beste Strategie benötigen oder einfach nur einen Fehler in einer gespeicherten Prozedur beheben möchten, ist die Community hier, um zu helfen.

  • IRC
    Freenode hat einen sehr aktiven Kanal namens #postgres, in dem Benutzer sich gegenseitig helfen, Konzepte zu verstehen, Fehler zu beheben oder andere Ressourcen zu finden.
  • Mailinglisten
    PostgreSQL hat eine Handvoll Mailinglisten, denen man beitreten kann. Längere Fragen / Probleme können hier gesendet werden und können zu jedem Zeitpunkt viel mehr Menschen als IRC erreichen. Die Listen sind auf der PostgreSQL-Website zu finden, und die Listen pgsql-general oder pgsql-admin sind gute Ressourcen.