Der In-Memory Base Level: Bessere und kostenlose Performance für (fast) alle Oracle-Datenbanken
"Erst machen wir es richtig, dann machen wir es schnell" vs. "Wenn Ihr es richtig macht, dann ist es schnell!" – Das waren früher die gegensätzlichen Meinungen zwischen Entwicklern und Betreibern einer Applikation.
Aber gehören diese Aussagen wirklich in die Vergangenheit? Überall findet dieser Spagat zwischen dem richtigen Workflow und Aussehen einer Anwendung auf der einen und Performance auf der anderen Seite statt. Vor 15, 20 oder 25 Jahren war es dennoch einfacher, richtige und schnelle Applikationen als Entwickler auszuliefern. Die Systeme waren monolithisch aufgebaut. Meistens in Client-Server-Technologie mit einer dedizierten Programmiersprache und mit einem Frontend, das Workflow und Benutzeroberfläche vereinte. Im Hintergrund werkelte eine Datenbank vor sich hin, die teilweise den Workflow unterstützte und die Daten ansonsten bereitwillig an das Frontend weiterreichte oder von diesem entgegennahm.
Diese Zeiten sind lange vorbei. Heutige Applikationen bestehen aus "Kits" – aus Werkzeugen, die mal mehr, mal weniger dynamisch Anwendungen erzeugen. Workflows sind nicht mehr in Quellcode gemeißelt, sondern werden deskriptiv angelegt. Kurzfristig erneuerte und geänderte Businessprozesse ziehen sofortige Anpassungen innerhalb der Applikationen nach sich. SQL-Befehle und teilweise auch Codesegmente werden dynamisch erzeugt, Zugriffe erfolgen mal durch Web-Aufrufe, JavaScript, Java, PL/SQL, manchmal über APIs, (Micro-)Services oder direkt durch Reports. Es wird kumuliert, gruppiert und Daten werden analysiert – zugeschnitten auf jeden Kunden, auf jeden Workflow, auf jedes Business, mit neuen Datentypen (z. B. JSON) oder gleich als unstrukturierte Daten. Um diesem Trend noch einen weiteren Schub zu geben, wurden Entwicklungsteams in Rapid-Application-Development-Methoden geschult und heute liefern diese eben nicht mehr ein- oder zweimal pro Jahr Software aus. Sprints finden quasi wöchentlich statt, neuer Code kommt ständig auf Produktivsysteme.
Selbst, wenn Entwickler bei der Entwicklung des "Applikations-Kits" also "wenn Ihr es richtig macht, dann ist es schnell!" bestmöglich berücksichtigen: Niemand kann diese Fülle der Anwendungskomponenten und -möglichkeiten zentral testen, weder zeitlich noch inhaltlich. Geschweige denn, wie sich solche Anwendungen mit exponentiell steigendem Datenvolumen verhalten. Und nicht zuletzt gewöhnt sich ein Benutzer auch an eine bestimmte Geschwindigkeit und will diese mindestens beibehalten oder am liebsten noch verbessert bekommen.
So ist es kein Wunder, dass eine Suche im Internet nach "Performance-Problem" oder "Application-Performance-Problem" Milliarden an Suchergebnissen liefert.
Performance ist für Oracle-Datenbanken immer schon ein zentrales Thema. Historisch gesehen spielen da die beiden Optimizer-Varianten mit den Statistiken und generierten Ausführungsplänen eine große Rolle. Ergänzt werden diese durch diverse Varianten von Caches für Daten und Code, Indizierung und ein ganzes Sammelsurium von Tools, wie z. B. Statspack, Diagnostic- und Tuning-Pack mit SQL Tuning Advisor oder Automatic Database Diagnostic Advisor, SQL Profile und SQL Patches. Für die tiefer eintauchenden Administratoren stehen Traces aller Art zur Verfügung.
Mit der Datenbankversion 12.1 wurde von Oracle eine weitere, neue Technologie als kostenpflichtige Option zur Enterprise-Edition eingeführt, das sogenannte Database In-Memory. Seit dem Datenbank-Release 19.8 gibt es nun eine kostenlose Variante – den sogenannten Base Level. Im Folgenden wird geklärt, wie das funktioniert, wie man es benutzt und überwacht.
Hintergrundwissen zur In-Memory-Funktion
Neben dem klassischen Buffer-Cache, in dem innerhalb von Blöcken die Daten im Zeilenformat abgelegt werden, existiert nun ein weiterer Cache, in dem Daten im Spaltenformat abgelegt sind, der sogenannte In-Memory Column Store (Abb. 1). Beide Caches können, müssen aber nicht, die gleichen Tabellen bzw. "qualifizierten Objekte" beinhalten, beide Caches sind gleichzeitig aktiv und für Transaktionen konsistent. Abfragen haben die Möglichkeit, aus beiden Caches Daten zu beziehen.
Fehlen Daten im In-Memory-Cache oder wird im Execution Plan auf den In-Memory-Cache verzichtet, wird transparent für die Applikationen auf die Daten im Buffer-Cache zugegriffen. Damit ist der In-Memory-Cache (von möglichen Bugs einmal abgesehen) zu 100 Prozent kompatibel mit allen Applikationen und kann ohne Eingriff in den Applikationscode verwendet werden. Im Gegensatz zu den Blöcken im Buffer-Cache werden die Daten im In-Memory-Cache komprimiert abgelegt, sodass bereits kleine Cachegrößen einen großen Unterschied in der Applikationsperformance nach sich ziehen können.
Der größte Teil des In-Memory Column Stores besteht aus sogenannten IMCUs (In-Memory Compression Unit). Hier werden die Nutzdaten komprimiert abgelegt und die Datenbank kann, zum Teil ohne Dekomprimierung, auf diese Daten direkt zugreifen. Ein in den In-Memory Column Store geladenes Objekt, z. B. ein Segment, belegt ein oder mehrere IMCUs. Diese IMCUs sind jeweils ein MB groß. Sind Objekte (komprimiert) kleiner als ein MB, verschenkt man also Platz, sind Objekte größer als ein MB werden mehrere IMCUs verwendet (Abb. 2).
Die Daten werden innerhalb einer Column in einer IMCU (der sogenannten Column Compression Unit=Column CU) nicht mit ihren originalen Werten abgelegt, sondern "kodiert" in einer lokalen Bibliothek (Abb. 3).
Diese lokale Bibliothek beinhaltet Minimal- und Maximalwerte der jeweiligen Spalte pro IMCU sowie die Werte in numerischer Form. Greift die Datenbank nun auf diese Column CU zu und benötigt z. B. alle Zeilen, die den Wert "Cadillac" beinhalten, wird nach dem Wert "2" innerhalb der Column CU gesucht. Da in dieser Column CU der Wert "Fiat" erst gar nicht vorkommt, kann die Datenbank, wie das vom Partition Pruning bekannt ist, ganze IMCUs skippen und muss diese gar nicht erst durchsuchen.
Neben den IMCUs gibt es innerhalb des In-Memory-Caches (Abb. 4) noch IMEU (In-Memory Expression Unit, hier werden Ausdrücke, z. B. Joins oder virtuelle Spalten analog den Columns gecacht) und die SMU (Snapshot Metadata Unit). SMUs sind 64 KB große, zusätzliche "Bibliotheken". Diese beinhalten u. a. ein Transaktionslog und zeichnen auf, welche der Zeilen aus den IMCUs nicht mehr aktuell sind. Diese dürfen aus Lesekonsistenzgründen natürlich nicht mehr verwendet werden. Aber wieso gibt es überhaupt "Stale Rows" innerhalb von IMCUs?
Dazu muss nochmal ein kleines bisschen (versprochen, letzter Teil der trockenen Theorie) in die Architektur eingetaucht werden.
Im Gegensatz zum normalen Buffer-Cache, in dem ein Block immer die "aktuelle" Repräsentation von Daten beinhaltet (Datenblöcke, die zu Lesekonsistenzzwecken behalten werden müssen, verschwinden in die Undo Tablespaces), ist die Repräsentation eines Objektes im In-Memory-Cache ein Snapshot des Objektes zu einem speziellen Zeitpunkt.
Finden nun Datenmanipulationen statt, also Inserts, Deletes oder Updates, so werden die Daten in den IMCUs nicht sofort ausgetauscht, sondern in den Snapshot Metadata Units vorerst nur als "Stale" markiert, das Transaktionsjournal wird mit der Row-ID des betroffenen Datensatzes befüllt. Somit können diese detektiert, aber nicht mehr verwendet werden. Stattdessen greift die Datenbank bei einem Scan durch die IMCUs für diese Rows dann auf den Buffer-Cache zurück.
Der In-Memory Coordinator Process (IMCO) läuft zyklisch im Hintergrund und ist zuständig für das Anschieben des Füllens oder Erneuerns des In-Memory-Caches für ein Objekt (oder eine Expression). Dieser Prozess entscheidet also, wann ein IMCU erstmalig befüllt oder entsprechend erneuert wird.
Die tatsächliche Arbeit – also das Füllen und Erneuern von IMCUs oder IMEUs, das Löschen der Stale Row-IDs aus den SMUs oder das Komprimieren der Daten – übernehmen dabei Space-Worker-Prozesse. Die Standardeinstellung ist fünfzig Prozent des Parameters CPU_COUNT, mit INMEMORY_MAX_POPULATE_SERVERS kann dieser Wert nach eigenem Gusto angepasst werden.
Der kostenlose In-Memory Base Level
Wie bereits erwähnt steht der In-Memory Base Level seit der Datenbankversion 19.8 und neuer zur Verfügung. Bis zu 16 GB RAM kann pro Instanz zugeordnet werden. Die entsprechenden Parameter werden auf Instanz-Ebene definiert (also für die CDB oder Non-CDB), vorhandene PDBs können sich aus der CDB entsprechend bedienen. Ist der Base Level erst einmal gesetzt, schränkt die Datenbank automatisch nicht erlaubte Funktionalitäten ein. Damit werden Lizenzverstöße quasi unmöglich gemacht. Trotzdem sollte man wissen, welche Einschränkungen mit dem Base Level einhergehen. Technische Einschränkungen sind z. B. kein automatisches In-Memory von Objekten, Tabellen können nur komplett geladen werden (ein exclude column ist nicht möglich) und die Komprimierung ist festgelegt auf "Memcompress for query low". Letzteres ist die Komprimierung für leseoptimierte Zugriffe und auch in der kostenpflichtigen Option Default, benötigt aber von allen Komprimierungsvarianten am meisten RAM. Lizenzrechtlich gibt es eine wichtige Einschränkung: In einer 3rd-Party-Cloud darf der Base Level nicht verwendet werden! Hier muss die kostenpflichtige Option zum Einsatz kommen, soll ein In-Memory-Cache zum Einsatz kommen.
Durch Setzen des Datenbankparameters INMEMORY_FORCE von "default" auf "BASE_LEVEL" und der INMEMORY_SIZE auf einen Wert zwischen 100 MB und 16 GB steht nach einem Restart der Datenbank der In-Memory-Cache zur Verfügung. Die Größe ist bedingt änderbar, eine Verkleinerung benötigt einen Restart der Datenbank, Vergrößern geht dynamisch.
Ist die Datenbank vorbereitet, kann mit dem Laden von Objekten begonnen werden. Hier als Beispiel eine fiktive Join Group zweier Spalten, die in diesem Falle nicht indiziert sind:
create inmemory join group emp_join_empid3_empid4 (employees1(employee_id_3), employees2(employee_id_4));
Bei einer Tabelle kann das entsprechend so aussehen:
Alter table employees1 inmemory;
Wer nicht auf den In-Memory-Coordinator-Prozess warten möchte, kann das Populate auch direkt starten:
exec DBMS_INMEMORY.POPULATE(schema_name=>'HR', table_name => 'EMPLOYEES1‘);
Dieser Befehl triggert nun das Populate an und die Worker-Prozesse beginnen im Hintergrund zu arbeiten. Wie ein Blick in die View v$im_segments im Status "COMPLETED" zeigt, ist der Populationsprozess beendet und die Daten können verwendet werden.
Join Groups werden nicht in v$im_segments aufgeführt, sondern in dba_joingroups.
Objekte für den In-Memory Column Store finden
Eingerichtet ist der In-Memory Column Store schnell. Etwas aufwändiger ist es, die richtigen Objekte (z. B. Tabellen, Partitionen, Join Groups, Virtual Columns) zu finden. Da im Base Level nur die Standard-Komprimierung erlaubt ist, hilft der "Database In-Memory Advisor", den man mit dem Diagnostics-/Tuning-Pack nutzen darf und der in der MOS Note 1965343.1 zum Download zur Verfügung steht, nur bedingt. Als DBA macht es daher Sinn, Ausführungsstatistiken und -pläne manuell zu analysieren. Hier sind die Standard-Views (v$sql, v$sql_plan, etc.) die richtige Anlaufstelle. Interessant sind dabei Statements mit hoher Applikationsdauer, wiederkehrenden Joins (auch auf indizierten Spalten!), viel I/O (inklusive vieler Index-Zugriffe) oder hohen Block gets aus dem Buffer-Cache. Auch die Verwendung von Materialized Views oder einzelnen, hochfrequentierten Tabellenpartitionen sollte geprüft werden. Sind Tabellen bereits geladen, kann nach einiger Zeit In-Memory Expression Capture verwendet werden. Hier werden für häufig vorkommende Ausdrücke dann versteckte virtuelle Columns angelegt.
Hilfreich ist der In-Memory Column Store bei sehr verschiedenen Statements, z. B. bei Filteroperationen (kleiner, größer, gleich, IN), Suchen in Text, XML- oder JSON-Dokumenten (contains/JSON_textcontains) oder bei Joins (kleine Tabellen "Lookup Tables" gegen große Tabellen, Primary Key – Foreign Key Scans), um nur einige, wenige Möglichkeiten genannt zu haben. Mit diesem Wissen im Hinterkopf lassen sich schnell erste Kandidaten finden. Da Datenbanken sich in ihrem Workload ständig verändern können, macht es Sinn, solche Analysen immer mal wieder zyklisch durchzuführen. Spätestens ein paar Wochen nach dem Upgrade einer Applikation oder der Datenbankversion sollte eine erneute Analyserunde durchgeführt werden.
Performance-Gewinne durch den In-Memory Column Store
Wer nicht sofort an seine Produktionsdatenbank zum Test möchte, im Blogeintrag "Setup einer Testumgebung für Oracle In-Memory (Base Level)" ist beschrieben, wie mit den Beispielschemata von Oracle eine entsprechende Testumgebung aufgebaut werden kann [1]. Diese basiert dann z. B. auf dem allgemein bekannten HR-Schema, das für die eigenen Tests entsprechend erweitert wurde. Neben den Tests mit eigenen Applikationen kann somit herausgefunden werden, welche Typen von Statements, Joins etc. sich allgemein am besten als Kandidaten für den In-Memory Column Store eignen.
Basierend auf der erstellten Testumgebung aus dem Blogeintrag, im Folgenden ein Beispiel, welches logisch (also aus Datensicht) absolut keinen Sinn macht, aber sehr gut darstellen kann, was passiert und wie dieses Wissen für die eigene Umsetzung nutzbar ist: Um Geschwindigkeitsunterschiede sehen zu können, wurde die Employees-Tabelle in den Anzahl-Columns erweitert und die Anzahl der Zeilen erhöht, am Ende waren in Employees (> 700.000), Departments (> 500.000), Regions (4) und Locations (> 180.000) also deutlich mehr Zeilen enthalten.
Abb. 5 zeigt einen der verwendeten Test-Selects. Hier wurde versucht, möglichst viele verschiedene Varianten zu verwenden, also "Like", "größer" und "kleiner", "In" mit feststehenden Werten oder Subselects, um nur einige zu nennen. Die Ausführungszeit ohne Verwendung des In-Memory Column Stores lag auf der verwendeten Hardware bei fast einer halben Stunde.
Im Ausführungsplan wurde erwartungsgemäß ein Sammelsurium von Index (Fast Full/Unique) Scans mit Tabellenzugriffen und Full Table Scans (aufgrund nicht indexierter Spalten) aufgeführt (Abb. 6). Die Predicate Information (s. Abb. 7) zeigt, wie der Optimizer den Select zerlegt hat und über welche Filter und Zugriffsvarianten die entsprechenden Daten zusammengesammelt werden.
Interessant ist nun, welche Verhaltensänderungen stattfinden, wenn alle vier verwendeten Tabellen in den In-Column Store geladen werden. Um Join Groups zu testen, wurde auch noch auf den nicht-indexierten Spalten employee_id_3 und employee_id_4 eine entsprechende inmemory join group angelegt:
create inmemory join group emp_join_empid3_empid4 (employees(employee_id_3), employees(employee_id_4));
Nach dem erfolgreichen Laden aller Objekte in den In-Memory Column Store fiel die Elapsed Time auf etwa dreizehneinhalb Minuten. Die Laufzeit wurde ohne Änderungen von Code oder des Selects in etwa halbiert. Dass es "nur" 50 Prozent Laufzeitverbesserung waren, lag vor allem an der verwendeten Hardware. Während des Selects war die verwendete CPU zu 100 Prozent ausgelastet. Es hätte also durchaus noch schneller verarbeitet werden können, hätten mehr Threads zur Verfügung gestanden. Die In-Memory-Funktionalität verwendet automatisch freie Kapazitäten der CPUs, ist also auch ohne Verwendung von expliziter, paralleler Ausführung von Statements in der Lage, Aufgaben schneller abzuarbeiten. Dies macht sich natürlich auch im Ausführungsplan bemerkbar.
Abb. 8 beinhaltet den geänderten Ausführungsplan. Aus diesem wird ersichtlich, dass der Optimizer keine Full Table Scans mehr durchführt, sondern Table Access InMemory Full durchführt. Auch einige Index Unique Scans fallen ersatzlos weg.
Interessant sind aber auch die Predicate Informationen. In Abb. 9 fällt sofort auf, dass ein großer Teil der Zugriffe über "inmemory"-Funktionalitäten erfolgt. Die Verwendung von Bloom-Filtern ermöglicht es, für den Zugriff nicht benötigte Daten vorab zu eliminieren. Nimmt man dann noch IMCU Pruning und weitere Möglichkeiten wie Vector Processing hinzu, die als Technologien im Hintergrund werkeln, erklärt sich der Laufzeitunterschied schnell.
Neben diesen Tests stellt sich die Frage, was die In-Memory-Column-Store-Technologie im Base Level in freier Wildbahn an Verbesserungen bringt. Wir haben in einem Proof of Concept mit einer eigenen Applikation bei einem Kunden sehr gute Erfahrungen gemacht. Bei nur zwei GB verwendetem In-Memory Column Store auf einer Oracle Database Appliance konnten parallel ablaufende, komplexe Berechnungen, die auf sehr viele, kleine Look-Up-Tabellen zugreifen, von einer Durchlaufzeit von etwa einer Stunde und fünfundvierzig Minuten auf konstant zehn bis elf Minuten verkürzt werden. Auf der Oracle Database Appliance waren acht Cores freigeschaltet, die während des Tests auch nahezu vollumfänglich von der Datenbank verwendet wurden. Die Anzahl der Zugriffe auf Indizes und Tabelleninhalte über den Database-Buffer-Cache reduzierte sich dramatisch. Inzwischen findet die In-Memory-Base-Level-Technologie regelmäßig seinen Einsatz bei Kunden.
Überwachung
Wie alle anderen performance-relevanten Aspekte ändern sich auch die Anforderungen an den Objekten im In-Memory Column Store von Zeit zu Zeit. Deshalb ist es unabdingbar, sowohl die Performance als auch weitere Parameter zyklisch im Blick zu behalten. Hilfreich sind dabei neben den schon erwähnten SQL-Views spezielle In-Memory Views.
Die v$inmemory_area ermöglicht die Abschätzung, was noch an Daten in den In-Memory Column Store geladen werden kann. Hier ist erkennbar, was aus dem gesamten Memory für den 1-MB-Pool, also IMCU und IMEU, und was für die SMU, also den 64-KB-Pool, verwendet wird. Die Differenz zwischen den Used_Bytes und den allokierten Bytes des 1-MB-Pools steht also für weitere Objekte zur Verfügung. Interessant ist auch die View v$im_segments.
Wer genau hinschaut, kann unter Umständen erkennen, dass die Inmemory_Size größer ist als die Anzahl Bytes (also die Größe auf Disk). Dies lässt sich unter anderem durch eine schlechte Komprimierung der Daten, zusätzliche Expressions, die zu einer Tabelle gehören, und durch die zusätzlichen SMUs für Tabellen, erklären. Das ist keinesfalls unüblich und deutet nicht darauf hin, dass dieses Objekt ineffizient und nicht geeignet für die Verwendung im In-Memory Column Store ist. Für die Eignung zählt allein der Performance-Gewinn! Da der Base Level auf maximal 16 GB RAM beschränkt bleibt, kann es natürlich Fälle geben, in denen lieber ein großes Objekt gegen viele kleine Objekte ausgetauscht wird. Die Entscheidung hierüber kann allerdings nicht pauschaliert werden, sondern ist eine Einzelfallentscheidung. Sollte ein Objekt nicht ganz in den In-Memory Column Store passen, kann dieses anhand eines Wertes größer Null der BYTES_NOT_POPULATED-Spalte von v$im_segments erkannt werden. Auch hier stellt sich dann die Frage, ob ein solches Objekt teilweise im In-Memory Column Store behalten werden soll oder ob dieses (oder ggfs. ein anderes Objekt, damit dieses passt) dann nicht wieder entfernt wird. Da fehlende Werte einfach aus dem Database- Buffer-Cache gezogen werden, kann es weiterhin Sinn machen, nicht vollständig geladene Objekte zu behalten. Wie der Database- Buffer-Cache wird auch der In-Memory Column Store "wärmer" – Daten, die weniger häufig verwendet werden, werden bei Memory-Druck automatisch aus dem Store entfernt und gegen (hoffentlich) häufiger benötigte Daten ausgetauscht (Least-Recently-Used-Mechanismus).
Noch ein Hinweis: Segmente, die zwar für den In-Memory Column Store markiert, aber nicht populated sind, werden auch nicht in v$im_segments aufgeführt. Auch diese Segmente können aber geladen und dafür andere Segmente wieder entfernt werden. Es ist also möglich, mehr als 16 GB Objekte zu markieren und die Datenbank entscheidet dann in gewissem Rahmen selbständig (siehe auch INMEMORY_PRIORITY) was populated wird.
Fazit
"Was nix kostet, taugt auch nix" trifft im Falle des In-Memory Base Levels überhaupt nicht zu. Natürlich gibt es einige Einschränkungen im Vergleich zur kostenpflichtigen In-Memory-Option der Oracle-Enterprise-Edition-Datenbank. Für kleine und mittelgroße Umgebungen und Applikationen ist der Base Level aber eine einfach und schnell umgesetzte Variante, um realistische Performance-Gewinne von bis zu 90 Prozent realisieren zu können – zumindest auf Datenbankseite. Häufig ist die Gesamt-Performance auch von Netzwerken und Clients einer Applikation abhängig. Dieser Teil lässt sich natürlich nicht beeinflussen. Die Basiskonfiguration besteht aus dem Setzen der Parameter und dem Restart einer Datenbank und kann innerhalb weniger Minuten erledigt werden. Etwas aufwändiger ist das Suchen und Finden geeigneter Objekte, da selbst bei lizenziertem Diagnostics- und Tuning-Pack das vorhandene Skript aus MOS den Base Level (noch) nicht unterstützt. Dieser Aufwand (und eine zyklische Überprüfung) lohnt sich aber zweifelsohne, denn die Performancegewinne sind auch bei gut indizierten Applikationen hervorragend. Einer der wenigen Wermutstropfen: Leider ist der In-Memory Base Level in der Standard Edition 2 nicht verfügbar, die Nutzung bleibt Enterprise-Edition-Datenbanken vorbehalten.