Moderne relationale Datenbanksysteme bieten die Möglichkeit Tabellen horizontal zu Partitionieren. Bei geeigneter Nutzung können kürzeren Laufzeiten bei SQL -Statements auf großen Tabellen erreicht werden. Hierzu tragen vor allem zwei Mechanismen bei die direkt von Table Partitioning profitieren: Partition Pruning und Partition Joins
Was ist horizontales Table Partitioning?
Horizontales Table Partitioning bedeutet, das eine Tabelle mit vielen Datenzeilen in mehrere kleinere disjunkte Partitionen zerlegt wird die jeweils einen Teil der Zeilen aufnehmen. Jede Partition enthält für die in ihr enthaltenen Zeilen alle Spalten. Eine Zeile ist immer in genau einer Partition. Zur Verteilung der Zeilen auf die Partitionen gibt es eine definierte deterministische Regel, welche jede Zeile auf Grund der Werte einer oder mehrerer Attribute einer Partition zuweist. Diese Zuweisung erfolgt nicht nur beim einfügen neuer Zeilen, sondern wird auch bei jeder Änderung an einem der zur Partitionierung verwendeten Attribute in bereits vorhandenen Zeilen überprüft (z.B. Update). Sollte durch eine Änderung die Datenzeile in eine anderen Partition gehören als die in der sie sich befindet, so wird sie in die neue Partition migriert. Das heißt in die neue Partition eingefügt und aus der alten entfernt. Hierdurch können Datenänderungen aufwändiger werden als in nicht partitionierten Tabellen. Es ist deshalb erstrebenswert zur Partitionierung möglichst Spalten zu verwenden die sich nicht oder nur selten ändern. Die in der Partitionierungsregel verwendeten Spalten werden als Partitioning Column oder Partitioning Key bezeichnet, die Regel manchmal als Partitioning Function. Statt von horizontalem Table Partitioning wird im Kontext relationaler Datenbanken oft vereinfachend von Table Partitioning oder nur Partitioning gesprochen. Nach diesem kurzen Überblick nun zu den beiden Arten wie Selects von Partitioning profitieren können:
Partition Pruning
Partition Pruning (manchmal auch Partition Elimination) bedeutet, dass vor Ausführung der Abfrage erkannt wird welche Partitionen für die Abfrage irrelevant sind. Diese müssen dann nicht gelesen werden. Hierdurch kann bereits vor dem Lesen der Daten die Datenmengen reduziert werden, so dass bei großen Tabellen die Abfragelaufzeit spürbar sinken kann. Dies hilft besonders bei Full Table Scans, die ohne Partitioning die gesamte Tabelle vom Storage lesen müssten. Mit geeignetem Partitioning müssen nur eine oder wenige Partitionen statt der gesamten Tabelle gelesen werden. Aber auch bei der Nutzung von Indizes kann dies Vorteile bieten, da auch diese partitioniert werden können und dann vor allem bei Index-Scans weniger Daten lesen müssen. Bei Partitionierten Indizes ist auf Grund der geringeren Indexgröße je Partition meist auch die Pflege einfacher wovon DML-Operationen (Insert, Update und Delete) profitieren können. Um Partition Pruning effektiv zu nutzen ist es wichtig darauf zu achten, dass der Optimizer der Datenbank beim Generieren von Ausführungsplänen erkennen kann welche Partitionen für die Abfrage nicht relevant sind. Dazu ist zunächst wichtig, dass die Attribute über die die Partitionen definiert sind und die Attribute welche die Abfrage zum Filtern nutzt zusammenpassen. Es ist darüber hinaus erforderlich, dass der Optimizer des Datenbanksystems aus den Filter-Kriterien der Abfrage und den Partitionsgrenzen formal logisch erschließen kann, dass eine Partition keine Sätze enthalten kann die den angegebenen Filter erfüllen. Hierzu werden in der Regel auch alle weiteren Metadaten die dem DBMS bekannt sind wie z.B. Constraints verwendet. Leicht kann es jedoch passieren, dass man der Meinung ist das eine Partition erkennbar nicht relevant ist, die Datenbank dies aber nicht erschließen kann. Dies liegt in der Regel daran, dass man bei der Erkennung zusätzliche Informationen (z.B. Wissen über Anwendungslogik oder ETL-Strecken) herangezogen hat die der Datenbank nicht vorliegen und somit von dieser nicht zum logischen Ausschließen der Partition genutzt werden können.
Beispiel:
Aus der Tabelle Produkt_Preise werden häufig die aktuell gültigen Preise und sehr selten ältere Preise abgefragt. Die Alten Preis werden zur Nachvollziehbarkeit dauerhaft gespeichert:
Die Tabelle Produkt_Preise Die Abfrage um den aktuellen Preis zu ermitteln hat die folgende Form:
CREATE INDEX IX_Vorgangsnummer_nur_Status_N ON Vorgang (Vorgangsnummer) WHERE Vorgangsstatus = 'N';
Um dies zu unterstützen könnte die Tabelle in die beiden folgenden Partitionen zerteilt werden: Dabei ist es offensichtlich, dass die eine alle aktuellen und die andere alle alten Preise enthält. Leider ist das für die Datenbank nicht so offensichtlich. Damit diese dies erkennen kann muss die Partitionsgrenze geeignet definiert sein.
Die partitionierte Tabelle Produkt_Preise
Wenn die Grenze so definiert ist, dass in die zweite Partition alle Sätze gelangen die Gültig_Bis >= 31.12.9999 haben, was vielleicht naheliegend ist, wird die Datenbank immer beide Partitionen verwenden. In diesem Fall könnten sich in der ersten Partition formal auch Datensätze befinden die bis 21.12.2015 gültig sind, da auch diese in die erste Partition kommen. Das geplante Preisänderungen in einer anderen Tabelle verwaltet werden weiß der Optimizer nicht. Damit die Datenbank diese Partition ausschließen kann muss die Definition so angelegt sein, dass die Partitionsgrenze für die nicht zu lesenden Partition die Bedingung der Abfrage nicht erfüllt. In unserem Beispiel könnten wir die Grenze für die erste Partition auf Gültig_Bis < 1.7.2015 legen. Da dies vor dem 21.12.2015 liegt kann kein Satz in dieser Partition die Bedingung Gültig_Bis > 21.12.2015 erfüllen. Logisch ist die Aussage Gültig_Bis < 1.7.2015 AND Gültig_Bis > 21.12.2015 immer unwahr, darüber kann der Optimizer erkennen das diese Partition für die Abfrage nicht relevant ist und die Partition frühzeitig ausschließen.
Praktisch bedeutet dies jedoch, dass die Partitionsgrenze immer wieder verschoben werden muss. Sonst liegen die am 1.1.2016 ungültig werdenden Sätze mit in der Partition mit den gültigen Sätzen. Alternativ könnten auch weitere Partitionen dazwischen angelegt werden, z.B. Jahres Partitionen. Solange diese leer sind belasten sie die Abfrageausführung nur gering. Wenn sie auf Grund des gewählten Datums nicht für die Abfrage relevant sind werden sie automatisch per Partition Pruning aus der Abfrage ausgeschlossen. Sie werden also nur bei den Abfragen Laufzeit kosten für die sie erforderlich sind. Es wird jedoch meistens Fälle geben bei denen trotz Partition Pruning Sätze gelesen werden, die für die Abfrage eigentlich irrelevant sind. Ein Beispiel hierfür sind unterjährige ungültig gewordene Preise, welche am Jahresende immer noch mitgelesen werden, weil sie in der gleichen Jahrespartition liegen. In der Regel ist der hierdurch entstehende geringe Laufzeitnachteil jedoch unproblematisch und eine perfekte Lösung ist oft nicht oder nur aufwändig zu erreichen.
Partition Joins
Der zweite Mechanismus durch den die Partitionierung zu besseren Abfragelaufzeiten beitragen kann sind Partition Joins, welche auch als Partitionwise Joins bezeichnet werden. Bei Partition Joins handelt es sich um eine Optimierung bei der statt komplette Tabellen miteinander zu Joinen, die einzelnen Partitionen der Tabellen gejoined werden. Dadurch werde aus einem großen Join mit vielen Zeilen auf beiden Seiten, mehrere kleine Joins mit weniger Zeilen. Diese lassen sich effizienter ausführen als ein großer Join. Der Effizienzgewinn funktioniert jedoch nur wenn die Zeilenzahl je Partition innerhalb jeder der beiden beteiligten Tabellen möglichst gleich hoch ist. Die Zeilen in den Tabellen also möglichst gleichmäßig auf die Partition verteilt sind. Damit ein Partition Join durchgeführt werden kann müssen beide beteiligten Tabellen über mindestens eines der Join-Attribute unter Verwendung der gleichen Partitionierungs-Funktion partitioniert sein.
Beispiel:
Um den monatlichen Umsatz je Produkt zu ermitteln wird die Tabelle Produkt_Preis mit der Tabelle Bestell_Position gejoined und die Anzahl aus Bestell_Position mit dem zum Bestellzeitpunkt gültigen Preis multipliziert:
SELECT Produkt_Preis.Produkt , sum(Produkt_Preis.Preis * Bestell_Position.Anzahl) AS Umsatz FROM Bestell_Position INNER JOIN Produkt_Preis ON Bestell_Position.Produkt = Produkt_Preis.Produkt AND Bestell_Position.Bestelldatum <= Produkt_Preis.Gültig_Bis AND Bestell_Position.Bestelldatum > Produkt_Preis.Gültig_Von GROUP BY Produkt_Preis.Produkt;
Um die Abfrage zu beantworten müssen die 10 Zeilen aus der Tabelle Bestell_Position mit den 9 Zeilen aus der Tabelle Produkt_Preis gejoined werden. Beim einfachen Nested Loops Join würde dies z.B. 9 * 10 Iterationen = 90 Iterationen bedeuten.
Join zwischen Produkt_Preise und Bestell_Position Ein Partition Join kann erreich werden indem beide Tabellen über das Produkt Partitioniert werden. Da beide Tabellen den gleichen Partition-Key und die gleichen Partitionsgrenzen verwenden kann ein Partitionwise Join für die Abfrage genutzt werden. Es werden dann jeweils die Partitionen mit dem gleichen Produkt aus der einen Tabelle und die mit dem gleichen Produkt aus der anderen Tabelle miteinander gejoined. Also für Schwarzer Tee 3 Sätze aus Produkt_Preis mit 4 Sätzen aus Bestell_Position, für Grüner Tee 4 Sätze aus Produkt_Preis mit 3 Sätzen aus Bestell_Position und für Früchte Tee 2 Sätze aus Produkt_Preis mit 3 Sätzen aus Bestell_Position.
Partitionierter Join zwischen Produkt_Preise und Bestell_Position Bei Verwendung eines Nested Loops Joins wären dass entsprechend drei Joins mit 3 * 4, 4 * 3 und 2 * 3 Iterationen, also 3 * 4 + 4 * 3 + 2 * 3 = 12 + 12 + 6 = 30 Iterationen. Was wesentliche weniger Aufwand ist als die 90 Iterationen für die nicht partitionierte Tabelle. Ein Vorteil entsteht auch bei Hash Join (kleinere Hash-Tabelle) und Sorted-Merge-Join (weniger Sätze im Sort). Diese sind jedoch weniger anschaulich zu erklären, weshalb ich hier den Nestes Loops Join als Beispiel verwendet habe, auch wenn dieser in der Praxis bei großen Datenmengen quasi nicht vorkommt.
Fazit
Durch die Verwendung von geeignetem Partitioning können Selects auf großen Datenmengen durch Partition Pruning oder Partition Joins beschleunigt werden. Dies kann helfen, wenn Indizes z.B. mangels Selektivität nicht mehr sinnvoll beim Select genutzt werden können. Vor der Partitionierung sollten jedoch andere Maßnahmen wie z.B. Indizierung geprüft werden, da Partitioning oft nur in den großen teuren Editionen oder gegen Aufpreis verfügbar ist und auch Nachteile hat wie erhöhten Aufwand bei Updates auf den Spalten die zur Partitionierung verwendet werden.