Exasol ist eine Datenbank, die auf die Performance von Queries ausgelegt ist und den Anspruch hat, wartungsarm zu sein.
In diesem Blogbeitrag sollen Tipps zur Optimierung (Teil 1) und zum guten Umgang mit Exasol (Teil 2) gegeben werden. Vieles davon lässt sich auch auf andere Datenbanken anwenden.
SQL-Befehle werden in diesem Beitrag in Blockbuchstaben geschrieben.
Teil1: Query-Optimierung
Bei der Ausführung von Datenbankabfragen nimmt Exasol eigenständig Optimierungen vor. In manchen Fällen ist diese Optimierung nicht perfekt und kann händisch nachgebessert werden. Im Folgenden wird erklärt, wie diese Nachbesserung möglich ist und was für eine gute Performance in SQL-Statements beachtet werden kann.
Join-Reihenfolge erzwingen
Die selbstständigen Optimierungsmaßnahmen von Exasol umfassen u. a. die Wahl der Join-Reihenfolge. Grundsätzlich gilt, dass mit der kleineren Tabelle als Stammtabelle eine bessere Performance erzielt werden kann (sie also nach vorne gestellt werden sollte). Dann müssen technisch gesehen nur wenige Zeilen verarbeitet werden. In seltenen Fällen kann es passieren, dass Exasol hierbei keine optimale Wahl trifft. Ein Grund dafür kann sein, dass nicht berücksichtigt wird, dass es bei bestimmten Joins in einer der beiden Tabellen nur wenige Join-Partner gibt. Dadurch können beim Join viele Zeilen dieser Tabelle rausfallen, was dazu führt, dass die größere Tabelle effektiv die kleinere Tabelle ist.
Zur Optimierung gibt es eine Möglichkeit die Join-Reihenfolge bzw. die Wahl der Stammtabelle zu erzwingen. Das geschieht am besten, indem man einen LEFT JOIN anstelle eines (INNER) JOINS verwendet und die WHERE-Bedingung um ein IS NOT NULL der Join-Spalte der zweiten Tabelle ergänzt. Wenn es sicher ist, dass es immer einen Join-Partner gibt, kann die WHERE-Bedingung auch weggelassen werden.
Codebeispiel
Vorher:
SELECT table1.column1 FROM table1 -- effektiv kleinere Tabelle
JOIN table2 -- effektiv größere Tabelle
ON table1.column2 = table2.column2;
Nachher:
SELECT table1.column1 FROM table1 -- effektiv kleinere Tabelle
LEFT JOIN table2 -- effektiv größere Tabelle
ON table1.column2 = table2.column2
WHERE table2.column2 IS NOT NULL;
Materialisierung erzwingen
Werden bei der Ausführung einer Datenbankabfrage Zwischenergebnisse in (temporäre) Blöcke des RAM geschrieben, nennt man das Materialisierung. Wenn möglich, sollten Materialisierungen vermieden werden. Wenn sie klein sind, können sie die Performance manchmal aber auch verbessern.
Bei der Verwendung von Common Table Expressions (CTE) werden nicht automatisch Materialisierungen erzeugt. Der Optimizer von Exasol schreibt grundsätzlich, wenn möglich, alle CTEs in Inline-Joins etc. um.
Eine Materialisierung wird erzeugt, wenn eine Sub-Query, CTE oder View eine der folgenden Operatoren enthält:
- ORDER BY
- GROUP BY
- ROWNUM
- DISTINCT
- Analytische Funktion mit ORDER BY.
Die Best Practice, um eine Materialisierung zu erzwingen, ist an die CTE ein ORDER BY FALSE anzuhängen.
Vermeide IN und EXISTS
Die Operatoren IN und EXISTS werden von Exasols Optimizer gleich behandelt. Sie sollten prinzipiell vermieden werden, weil sie nicht performant sind. (Sie verursachen auch eine Materialisierung.) Solche Statements können fast immer so umgeschrieben werden, dass stattdessen ein Join verwendet wird. Bei NOT IN und NOT EXISTS geht es manchmal nicht.
Häufig wird als IN-Liste in der WHERE-Bedingung das Ergebnis einer Sub-Query oder CTE verwendet. Dann kann die WHERE-IN-Bedingung durch ein Join mit der CTE ersetzt werden.
Codebeispiel
Vorher:
WITH mycte AS (
SELECT column2 FROM table2
WHERE column3 = 'Special Value')
SELECT SUM(table1.column1) FROM table1
WHERE table1.column2 IN (SELECT column2 FROM mycte);
Nachher:
WITH mycte AS (
SELECT column2 FROM table2
WHERE column3 = 'Special Value')
SELECT SUM(table1.column1) FROM table1
JOIN mycte
ON table1.column2 = mycte.column2;
Vorsicht ist geboten, wenn der Join Duplikate erzeugen kann, die es bei der WHERE-IN-Bedingung nicht gegeben hätte. Dann muss im Idealfall die CTE so umgeschrieben werden, dass keine Duplikate auftreten. Ist das nicht gut möglich, kann stattdessen auf DISTINCT oder GROUP BY zur Vermeidung von Duplikaten zurückgegriffen werden. Dies führt zwar auch zu einer Materialisierung, insgesamt ist das aber trotzdem performanter als die Verwendung eines IN.
Vermeide UNION (ohne ALL)
Der UNION-Befehl ist sehr ressourcenintensiv, da jede Zeile mit jeder anderen Zeile verglichen werden muss. Grundsätzlich sollte UNION ALL anstatt UNION verwendet werden. UNION wird von Exasol grundsätzlich so umgesetzt, dass erst ein UNION ALL ausgeführt wird und danach ein GROUP BY auf alle Spalten ausgeführt wird.
Sortierungsoptimierung
Sortierungen sind ressourcenintensiv, da in einem parallelen System die Daten auf verschiedenen Knoten liegen, so dass viel Netzwerkkommunikation erforderlich ist.
Exasol muss bei folgenden Befehlen sortieren:
- ORDER BY
- Analytische Funktionen mit ORDER BY, z. B. RANK, ROW_NUMBER oder SUM oder AVG mit OVER-Klausel
Um die Performance zu verbessern, gibt es entweder die Möglichkeiten die Datenmenge zu reduzieren oder, was noch besser ist, Sortierungen zu vermeiden.
Eine Möglichkeit, Daten zu reduzieren, ist, bis zur Materialisierung weniger Spalten mitzunehmen und sie stattdessen erst im Nachhinein über einen JOIN hinzuzufügen.
Bei Exasol kann eine Verwendung von ROW_NUMBER() zusammen mit Rank = 1 oft stark verbessert werden. Zur Veranschaulichung hier ein passendes Minimalbeispiel:
WITH mycte AS (
SELECT table1.key1,
table1.key2,
table2.key3,
table1.zeitspalte,
ROW_NUMBER() OVER (PARTITION BY table1.key2, table2.key3 ORDER BY table1.zeitspalte DESC) AS rnk
FROM table1
JOIN table2
ON table1.key1 = table2.key1)
SELECT key1,
key2,
key3,
zeitspalte
FROM mycte
WHERE rnk = 1;
Bei dem SQL wird für jede Kombination aus key2
und key3
anhand des Datums ein Rang für key1
erstellt und dann mit „WHERE rnk = 1
“ gefiltert, so dass von jeder Kombination nur der neuste key1
-Datensatz ausgegeben wird.
- Möglichkeit: Verwendung von Preference SQL („PREFERRING HIGH …“)
Syntax:
SELECT [Alle Spaltennamen] FROM [Tabellenname] PREFERRING HIGH [Spaltenname der Spalte, die gerankt würde];
- Möglichkeit: Verwendung von Aggregationsfunktionen, z.B. MAX(), s. u.
Sortierungsoptimierung mit Aggregationsfunktionen
Die Query aus dem vorherigen Abschnitt kann mit Hilfe der MAX-Funktion optimiert werden. Wird im gleichen Zug ein Join anstatt einer IN-Bedingung verwendet, ergibt sich
WITH CTE_letzter AS (
SELECT table1.key2, table2.key3, MAX(table1.zeitspalte) AS max_zeit
FROM table2
JOIN table1
ON table1.key1 = table2.key1
GROUP BY table1.key2, table2.key3)
SELECT FIRST_VALUE(table1.key1),
table1.key2,
table2.key3
table1.zeitspalte
FROM table1
JOIN table2
ON table1.key1 = table2.key1
JOIN CTE_letzter
ON CTE_letzter.key2 = table1.key2
AND CTE_letzter.key3 = table2.key3
AND CTE_letzter.max_zeit = table1.zeitspalte
GROUP BY table1.key2, table2.key3, table1.zeitspalte;
In der CTE wird jetzt der jeweilige maximale Zeitwert mit Hilfe der MAX-Funktion berechnet. Der Filter auf die Zeitspalte wird hier mit einem Join umgesetzt. Das letzte GROUP BY ist nötig, um Duplikate zu vermeiden, wenn MAX(table1.zeitspalte) nicht eindeutig ist. Aufgrund dieser Aggregation ist in der ersten Zeile ein FIRST_VALUE() nötig. Für diese Query muss Exasol keine Sortieroperation ausführen, was die Performance stark verbessert.
Teil2: Best Practices
Damit Exasol optimal genutzt werden kann und effizientes Arbeiten ermöglicht wird, werden in diesem Kapitel einige erfolgsversprechende Methoden und Tipps beschrieben.
Temporäre Tabellen
Temporäre Tabellen können in Exasol nicht manuell erstellt werden. Aber es ist möglich, durch die Deaktivierung des autocommits Tabellen zu droppen bevor ein commit ausgeführt wird. Diese Tabellen werden dann nicht von anderen Transaktionen gesehen und nicht auf die Disk (Festplatte) geschrieben.
Sehr praktisch für ETL-Jobs ist folgendes Vorgehen:
- Schreibe Daten in eine temporäre Tabelle (Materialisierung).
- Arbeite damit.
- Droppe die Tabelle und commite
Bei diesem Vorgehen ist es kein Problem, wenn andere Sessions denselben Tabellennamen verwenden.
Noch besser ist es, die temporäre Tabelle als View zu definieren. Dann kann Exasols Optimizer Filter früher anwenden and Spalten eliminieren. Es kann also besser optimiert werden. (Ein späteres Update auf die Inhalte der View geht dann natürlich nicht mehr.)
Das Erzeugen mehrerer Views mit verrückten zufälligen Namen wird Schemaverschmutzung genannt und sollte vermieden werden. Bei aktiviertem autocommit sollten besser CTEs anstatt Views verwendet werden.
Umschreiben von LIKE-Prädikaten
Das LIKE-Schlüsselwort ist optimiert für den Abgleich mit konstanten Mustern, also z. B. LIKE ‚%-%‘. Konkatenation (Verkettungen) mit Spaltennamen sind nicht so performant. Wenn man z. B. prüfen oder filtern will, ob der Inhalt einer Spalte eines Datensatzes einen zugehörigen Stadtnamen enthält, sollte nicht
LIKE '%' || c.city_name || '%'
verwendet werden. Es ist besser STRING-Funktionen zu benutzen. Dafür nützliche Funktionen sind:
- INSTR: Nimmt zwei Strings als Argument und gibt zurück, an welcher Position der zweite String im Ersten enthalten ist.
- SUBSTR: Gibt abhängig von den Argumenten bestimmte Teile eines Strings zurück (z. B. Zeichen 3 bis 6 oder die letzten 5 Zeichen).
- LENGTH: Gibt die Länge eines Strings zurück.
Vermeide teure Operationen
Für die beste Performance sollte man darauf achten, wenn möglich, teure Operationen zu vermeiden. Im Folgenden werden teure Operationen aufgelistet und es wird z. T. erklärt, wie sie vermieden werden können:
- Indexerstellung
- Index-Neubau (z. B. nach einer Reorganisation einer Tabelle nach einer Clustervergrößerung oder wenn >25% als gelöscht markiert wurden oder es ein Update auf eine distribution-key-Spalte gab)
- Deshalb sollte man Updates auf distribution-key-Spalten vermeiden.
- BETWEEN-Joins (BETWEEN in der Join-Bedingung)
- Am schnellsten sind Joins mit Equal-Bedingungen (
tab1.x = tab2.x
)
- Am schnellsten sind Joins mit Equal-Bedingungen (
- Globale Joins
- Diese können verhindert werden, indem man kleine Tabellengrößen verwendet, distribution keys benutzt oder die Replizierungsgrenze (replication border) vergrößert. In Exasol werden Tabellen, die weniger Zeilen als die Replizierungsgrenze haben, auf allen Dantebankknoten repliziert.
- Sortierungen
- Siehe oben.
- UNION
- Siehe oben.
- REORGANIZE
- Constraint-Checks, z. B. Primärschlüssel und NOT-NULL-Restriktionen für Spalten
- Da es hierzu einiges zu sagen gibt, folgt ein eigener Abschnitt zu Constraint-Checks
Tipps zum Umgang mit Constraints
Die Rechenkosten steigen mit der Menge der importierten Daten und der Menge der vorhandenen Daten. Es ist Best Practice Constraints in Tabellen zu deaktivieren, damit sie nicht bei jedem einzelnen INSERT und UPDATE geprüft werden müssen.
Codebeispiel:
CITY_ID INTEGER PRIMARY KEY DISABLE;
anstatt
CITY_ID INTEGER PRIMARY KEY;
Befehle, um Constraint-Bedingungen zu testen, sind
ALTER TABLE cities MODIFY PRIMARY KEY ENABLE;
und
SELECT * WITH INVALID PRIMARY KEY(city_id) FROM cities;
Das erste Statement wirft einen Fehler, wenn es eine PRIMARY-KEY-Verletzung gibt, (Duplikate oder NULL-Werte) und das zweite Statement gibt direkt die ungültigen/fehlerhaften Zeilen zurück.
Analog zur Prüfung der PRIMARY-KEY-Bedingung gibt es Prüfungen auf FOREIGN KEYs und UNIQUEness:
SELECT * WITH INVALID FOREIGN KEY(city_id) FROM markets REFERENCING cities(city_id);
SELECT * WITH INVALID UNIQUE(city_id) FROM cities;
Am besten können die Checks am Ende des ETL-Jobs gemacht werden. Es ist billiger die Constraint Checks alle gesammelt zu machen anstatt nach jedem einzelnen INSERT und UPDATE.
ETL
Je nach Datenbank und Verbindungsmethode werden ETL-Prozesse seriell oder parallel ausgeführt. Für eine Oracle-Datenbank ist beispielweise ORA die native Schnittstelle. Deshalb wird folgender Code parallel ausgeführt:
IMPORT INTO sales FROM ORA AT oracle_con TABLE myoracledb.sales;
Mit JDBC wird dieser Import nicht mehr parallel ausgeführt:
IMPORT INTO sales FROM jdbc AT mysql_con TABLE mysql.sales;
Stattdessen wird die Query an die Datenbank gesendet und sobald das Ergebnis verfügbar ist, werden die Daten über das Netzwerk transferiert. Um hier eine Parallelisierung zu erreichen, muss die STATEMENT-Klausel verwendet werden:
IMPORT INTO sales FROM jdbc AT mysql_con TABLE mysql.sales
STATEMENT 'SELECT * FROM mysql.sales WHERE year(sales_date)=2018';
Es können mehrere Statements in einer Query benutzt werden:
IMPORT INTO sales FROM jdbc AT mysql_con TABLE mysql.sales
STATEMENT 'SELECT * FROM mysql.sales WHERE year(sales_date)=2018'
STATEMENT 'SELECT * FROM mysql.sales WHERE year(sales_date)=2017'
STATEMENT 'SELECT * FROM mysql.sales WHERE year(sales_date)=2016';
Man sollte nicht versuchen, mehrere Import-Statements in mehreren Sessions, die in die gleiche Tabelle schreiben, gleichzeitig ausführen. Denn dann gibt es Transaktionskonflikte und Schreib-Locks. Stattdessen sollte man wie oben zu sehen mehrere Statement-Klauseln benutzen. Dann werden die Importe parallelisiert.
Paralleles Laden wird auch für Dateien unterstützt.
Beispielcode für eine Linux-Shell:
$ wc -l SALES.csv
100001 SALES.csv
Eine Datei wird nicht parallel importiert. Um eine Parallelisierung zu erreichen, sollte die Datei serverseitig in so viele Teile zerteilt werden, wie der Cluster Knoten hat. Der Code für das parallele Einlesen von vier Dateien könnte dann so aussehen:
IMPORT INTO sales FROM CSV AT ftp_con
FILE '/import/SALES.csv_part_aa'
FILE '/import/SALES.csv_part_ab'
FILE '/import/SALES.csv_part_ac'
FILE '/import/SALES.csv_part_ad';
Ein letzter Punkt zu ETL-Tools:
Wähle die höchstmögliche batch size für bulk inserts und die höchstmögliche fetch size und verwende bulk inserts statt single-row inserts.
Datenbankdesign
Für eine gute Performance ist es wichtig, passende distributions keys zu wählen.
Daten sollten normalisiert gespeichert werden. Nicht-normalisierte Daten verbrauchen mehr Speicherplatz, sowohl auf der der Festplatte als auch im RAM.
Es sollte kein ORDER BY in Views verwendet werden. Das würde dazu führen, dass jedes Mal materialisiert wird und so Optimierungspotential verloren geht. Generell sollten große Materialisierungen vermieden werden.
Datentypen
Ähnliche Daten (z. B. Preise) sollten den gleichen Datentyp haben
- Es sollten keine unnötig großen Datentypen benutzt werden. Zum Beispiel ist der Datentyp DATE besser als TIMESTAMP und kleine VARCHARs sind besser.
- CHARs sind besser als VARCHARs.
- Filter auf INTEGERs sind ca. zehnmal so schnell wie auf CHAR und Aggregationen sind zwei- bis dreimal schneller. Auf Joins trifft dies in ähnlicher Weise zu.
Zahlentypen sind auch auf der Datenbank und in Indizes kleiner. Es ist also immer besser numerische Werte anstatt CHARs zu benutzen.
Bei Joins sollten die Join-Spalten der beiden Join-Partner die gleichen Datentypen haben. Ansonsten muss Exasol die eine Seite der Gleichheits-Join-Bedingung konvertieren und einen expression index erzeugen. Diese Indizes werden auch bei anderen Funktionen wie CAST, einem Funktionsaufruf, UPPERCASE, Konkatenation und SUBSTRING erstellt. Expression indexes werden nach dem Join gelöscht, so dass sie jedes Mal wieder neu erstellt werden müssen.
Künstliche Schlüssel
Künstliche Schlüssel (surrogate keys) sind eine Möglichkeit Joins mit mehreren Spalte zu vermeiden.
Bei einem gewöhnlichen Datenmodell werden Dimensionstabellen an Faktentabellen gejoint. Wenn eine Dimensionstabelle Gültigkeitsspalten besitzt, müssen in der Regel diese Gültigkeiten in der Join-Bedingung geprüft werden, was Performanceeinbußen mit sich bringt. In den meisten Fällen sind die Join-Partner von Fakten- und Dimensionstabelle unveränderlich, so dass es sinnvoll ist, die Zuordnung einmal zu berechnen und abzuspeichern und so die Join-Performance zu verbessern. Hier kommen die künstlichen Schlüssel ins Spiel. Der Dimensionstabelle wird eine ID-Spalte gegeben, die für die Tabelle eindeutig ist und die Rolle des künstlichen Schlüssels einnimmt. Das geht z. B. mit dem Befehl
ALTER TABLE [Dimensionstabellenname] ADD COLUMN [Spaltenname des künstlichen Schlüssels] INT IDENTITY;
Die neue Spalte muss nicht manuell befüllt werden. Die IDENTITY-Option sorgt für eine automatische Befüllung mit aufsteigenden INTEGER-Werten.
Dann muss die neue Schlüsselspalte noch an die Faktentabelle gehängt und mit einem MERGE-Befehl befüllt werden.
Fazit
Mit Hilfe der Optimierungstipps und Best Practices aus diesem Blogbeitrag können Sie Exasol noch performanter benutzen und effektiver damit arbeiten. Viel Spaß bei der Verwendung von Exasol!
Quelle: Exasol Performance Management auf https://exacademy.exasol.com/