Wie speichern Datenbanken Indexschlüsselwerte (auf der Festplatte) für Felder variabler Länge?

16

Kontext

Diese Frage bezieht sich auf die Details der Implementierung von Indizes auf niedriger Ebene in SQL- und NoSQL-Datenbanksystemen. Die tatsächliche Struktur des Index (B + Baum, Hash, SSTable usw.) ist irrelevant, da die Frage speziell die Schlüssel betrifft, die in einem einzelnen Knoten einer dieser Implementierungen gespeichert sind.

Hintergrund

Wenn Sie in SQL-Datenbanken (z. B. MySQL) und NoSQL-Datenbanken (CouchDB, MongoDB usw.) einen Index für ein Spalten- oder JSON-Dokumentdatenfeld erstellen, wird von der Datenbank im Wesentlichen eine sortierte Liste aller Daten erstellt Diese Werte werden zusammen mit einem Dateioffset in die Hauptdatendatei eingefügt, in der sich der Datensatz zu diesem Wert befindet.

(Der Einfachheit halber kann ich andere esoterische Details bestimmter Werkzeuge von Hand wegwedeln.)

Einfaches klassisches SQL-Beispiel

Stellen Sie sich eine Standard-SQL-Tabelle mit einem einfachen 32-Bit-Primärschlüssel vor, für den wir einen Index erstellen. Am Ende erhalten wir einen Index auf der Festplatte der ganzzahligen Schlüssel, die sortiert und mit einem 64-Bit-Versatz in der Datendatei verknüpft sind Die Aufzeichnung lebt, zB:

id   | offset
--------------
1    | 1375
2    | 1413
3    | 1786

Die Darstellung der Schlüssel im Index auf der Festplatte sieht ungefähr so ​​aus:

[4-bytes][8-bytes] --> 12 bytes for each indexed value

Halten Sie sich an die Faustregeln zur Optimierung der Festplatten-E / A mit Dateisystemen und Datenbanksystemen. Nehmen wir an, Sie speichern Schlüssel in 4-KB-Blöcken auf der Festplatte. Das bedeutet:

4096 bytes / 12 bytes per key = 341 keys per block

Wenn wir die Gesamtstruktur des Index (B + -Baum, Hash, sortierte Liste usw.) ignorieren, lesen und schreiben wir Blöcke mit jeweils 341 Schlüsseln in den Speicher und kehren nach Bedarf auf die Festplatte zurück.

Beispielabfrage

Angenommen, anhand der Informationen aus dem vorherigen Abschnitt wird "id = 2" abgefragt. Die klassische DB-Indexsuche sieht folgendermaßen aus:

  1. Lesen Sie die Wurzel des Index (in diesem Fall 1 Block)
  2. Durchsuchen Sie den sortierten Block binär, um den Schlüssel zu finden
  3. Ermittelt den Versatz der Datendatei aus dem Wert
  4. Suchen Sie den Datensatz in der Datendatei mit dem Offset
  5. Senden Sie die Daten an den Anrufer zurück

Frageneinrichtung ...

Ok, hier kommt die Frage zusammen ...

Schritt 2 ist der wichtigste Teil, mit dem diese Abfragen in O (logn) Zeit ausgeführt werden können ... die Informationen müssen sortiert werden, ABER Sie müssen in der Lage sein, die Liste schnell zu sortieren ... mehr Insbesondere müssen Sie in der Lage sein, zu genau definierten Offsets zu springen, um den Indexschlüsselwert an dieser Position einzulesen.

Nachdem Sie den Block eingelesen haben, müssen Sie in der Lage sein, sofort zur 170. Position zu springen, den Schlüsselwert zu lesen und zu sehen, ob GT oder LT diese Position ist (und so weiter und so fort ...).

Die einzige Möglichkeit, wie Sie in diesem Block in den Daten springen können, besteht darin, dass die Schlüsselwertgrößen genau definiert sind, wie in unserem obigen Beispiel (4 Byte, dann 8 Byte pro Schlüssel).

FRAGE

Ok, hier bin ich also mit dem effizienten Indexdesign beschäftigt ... für varchar-Spalten in SQL-Datenbanken oder genauer gesagt für Felder in Dokumentdatenbanken wie CouchDB oder NoSQL, bei denen jedes zu indizierende Feld ein beliebiges sein kann Länge Wie implementieren Sie die Schlüsselwerte, die sich in den Blöcken der Indexstruktur befinden, aus der Sie Ihre Indizes erstellen?

Angenommen, Sie verwenden einen sequentiellen Zähler für eine ID in CouchDB und indizieren Tweets. Nach einigen Monaten werden Werte zwischen "1" und "100.000.000.000" angezeigt.

Angenommen, Sie erstellen den Index für die Datenbank am ersten Tag. Wenn die Datenbank nur 4 Tweets enthält, ist CouchDB möglicherweise versucht, das folgende Konstrukt für die Schlüsselwerte in den Indexblöcken zu verwenden:

[1-byte][8-bytes] <-- 9 bytes
4096 / 9 = 455 keys per block

Irgendwann bricht dies ab und Sie benötigen eine variable Anzahl von Bytes, um Ihren Schlüsselwert in den Indizes zu speichern.

Der Punkt ist noch eklatanter, wenn Sie sich entscheiden, ein Feld mit wirklich variabler Länge wie eine "tweet_message" oder so etwas zu indizieren.

Da die Länge des Schlüssels selbst völlig variabel ist und die Datenbank keine Möglichkeit hat, auf intelligente Weise eine "maximale Schlüsselgröße" zu erraten, wenn der Index erstellt und aktualisiert wird, wie werden diese Schlüssel tatsächlich in den Blöcken gespeichert, die Segmente der Indizes in diesen Datenbanken darstellen ?

Wenn Ihre Schlüssel eine variable Größe haben und Sie einen Schlüsselblock einlesen, wissen Sie nicht nur nicht, wie viele Schlüssel sich tatsächlich im Block befinden, sondern Sie wissen auch nicht, wie Sie zur Mitte der Liste springen sollen, um eine Binärdatei zu erstellen suche nach ihnen.

Hier stolpere ich über alles.

Bei statisch typisierten Feldern in klassischen SQL-Datenbanken (wie bool, int, char usw.) kann der Index meines Erachtens nur die Schlüssellänge vordefinieren und sich daran halten ... aber in dieser Welt der Dokumentendatenspeicher bin ich das auch verblüfft darüber, wie effizient diese Daten auf der Festplatte modelliert werden, sodass sie weiterhin in Echtzeit gescannt werden können.

Bitte lassen Sie mich wissen, wenn Klarstellungen erforderlich sind!

Update (Gregs Antwort)

Bitte beachten Sie meine Kommentare zu Gregs Antwort. Nach einer Woche mehr Recherche, denke ich, ist er wirklich auf einen wunderbar einfachen und performanten Vorschlag gestoßen, der in der Praxis kinderleicht zu implementieren und zu verwenden ist und gleichzeitig große Leistungsgewinne bei der Vermeidung der Deserialisierung von Schlüsselwerten bietet, die Ihnen egal sind.

Ich habe 3 separate DBMS-Implementierungen (CouchDB, kivaloo und InnoDB) untersucht und alle haben dieses Problem behoben, indem sie den gesamten Block in die interne Datenstruktur deserialisiert haben, bevor sie die Werte in ihrer Ausführungsumgebung (erlang / C) durchsucht haben.

Das ist es, was ich an Gregs Vorschlag für so brillant halte. Eine normale Blockgröße von 2048 würde normalerweise 50 oder weniger Offsets haben, was zu einem sehr kleinen Zahlenblock führen würde, der eingelesen werden müsste.

Update (Mögliche Nachteile von Gregs Vorschlag)

Um diesen Dialog mit mir bestmöglich fortzusetzen, habe ich folgende Nachteile erkannt ...

  1. Wenn jeder "Block" mit Versatzdaten überschrieben ist, können Sie die Blockgröße später in der Konfiguration nicht anpassen, da Sie möglicherweise Daten einlesen, die nicht mit einem korrekten Header oder einem Block beginnen enthielt mehrere Header.

  2. Wenn Sie große Schlüsselwerte indizieren (z. B. versucht jemand, eine Spalte von char (8192) oder blob (8192) zu indizieren), passen die Schlüssel möglicherweise nicht in einen einzelnen Block und müssen nebeneinander über zwei Blöcke verteilt werden . Dies bedeutet, dass Ihr erster Block einen Offset-Header haben würde und der zweite Block sofort mit den Schlüsseldaten beginnen würde.

Die Lösung für all dies ist eine feste Datenbankblockgröße, die nicht anpassbar ist, und die Entwicklung von Header-Blockdatenstrukturen. Beispielsweise fixieren Sie alle Blockgrößen auf 4 KB (normalerweise sowieso die optimalste) und schreiben eine sehr kleine Blockheader, der am Anfang den "Blocktyp" enthält. Wenn es sich um einen normalen Block handelt, sollte unmittelbar nach dem Blockheader der Versatzheader stehen. Wenn es sich um einen "Überlauf" -Typ handelt, werden die Rohschlüsseldaten unmittelbar nach dem Blockheader angezeigt.

Update (potenziell großartig)

Nachdem der Block als eine Reihe von Bytes eingelesen und die Offsets decodiert wurden; Technisch gesehen können Sie den gesuchten Schlüssel einfach in unformatierte Bytes kodieren und dann den Bytestrom direkt vergleichen.

Sobald der gesuchte Schlüssel gefunden ist, kann der Zeiger dekodiert und verfolgt werden.

Ein weiterer großartiger Nebeneffekt von Gregs Idee! Das Potenzial für die Optimierung der CPU-Zeit ist hier groß genug, dass es sich lohnen könnte, eine feste Blockgröße festzulegen, um all dies zu erreichen.

Riad Kalla
quelle
Für alle anderen, die sich für dieses Thema interessieren, ist Redis 'leitender Entwickler genau auf dieses Problem gestoßen, als er versucht hat, die nicht mehr vorhandene "Plattenspeicher" -Komponente für Redis zu implementieren. Ursprünglich entschied er sich für eine "ausreichend große" statische Schlüsselgröße von 32 Byte, erkannte jedoch das Potenzial für Probleme und entschied sich stattdessen dafür, den Hash der Schlüssel (sha1 oder md5) zu speichern, nur um eine einheitliche Größe zu erhalten. Dies beendet die Möglichkeit, Fernabfragen durchzuführen, bringt den Baum jedoch in einem guten FWIW-Gleichgewicht. Details hier redis.hackyhack.net/2011-01-12.html
Riyad Kalla
Einige weitere Infos habe ich gefunden. Es sieht so aus, als hätte SQLite eine Obergrenze dafür, wie groß die Schlüssel werden können, oder es schneidet den Schlüsselwert an einer Obergrenze tatsächlich ab und legt den Rest auf einer "Überlaufseite" auf der Festplatte ab. Dies kann Abfragen nach großen Schlüsseln fürchterlich machen, da sich das zufällige E / A verdoppelt. Scrolle
Riyad Kalla

Antworten:

7

Sie können Ihren Index als Liste von Offsets fester Größe in dem Block speichern, der Ihre Schlüsseldaten enthält. Beispielsweise:

+--------------+
| 3            | number of entries
+--------------+
| 16           | offset of first key data
+--------------+
| 24           | offset of second key data
+--------------+
| 39           | offset of third key data
+--------------+
| key one |
+----------------+
| key number two |
+-----------------------+
| this is the third key |
+-----------------------+

(Nun, die Schlüsseldaten würden in einem realen Beispiel sortiert, aber Sie bekommen die Idee).

Beachten Sie, dass dies nicht unbedingt widerspiegelt, wie Indexblöcke tatsächlich in einer Datenbank erstellt werden. Dies ist nur ein Beispiel für die Organisation eines Indexdatenblocks, bei dem die Schlüsseldaten eine variable Länge haben.

Greg Hewgill
quelle
Greg, ich habe deine Antwort noch nicht als defacto-Antwort ausgewählt, weil ich auf mehr Feedback sowie weitere Nachforschungen zu anderen DBMS hoffe (ich füge meine Kommentare zum ursprünglichen Q hinzu). Bisher scheint der gängigste Ansatz eine Obergrenze und dann der Rest des Schlüssels in einer Überlauftabelle zu sein, die nur dann überprüft wird, wenn der vollständige Schlüssel benötigt wird. Nicht so elegant. Ihre Lösung hat eine gewisse Eleganz, die ich mag, aber in dem Fall, in dem die Tasten Ihre Seitengröße sprengen, würde Ihr Weg immer noch eine Überlauftabelle benötigen oder es einfach nicht zulassen.
Riad Kalla
Ich hatte keinen Platz mehr ... Kurz gesagt, wenn der DB-Designer mit einigen harten Einschränkungen der Schlüsselgröße leben könnte, denke ich, dass Ihr Ansatz der effizienteste und flexibelste ist. Schöne Kombination aus Platz und CPU-Effizienz. Überlauftabellen sind flexibler, können jedoch problematisch sein, wenn Sie bei der Suche nach Schlüsseln, die ständig überlaufen, zufällige E / A-Vorgänge hinzufügen. Danke für den Input dazu!
Riad Kalla
Greg, ich habe mehr und mehr darüber nachgedacht und nach alternativen Lösungen gesucht, und ich denke, Sie haben es mit der Offset-Header-Idee geschafft. Wenn Sie Ihre Blöcke klein halten, könnten Sie mit 8-Bit-Offsets (1-Byte-Offsets) davonkommen. Bei größeren Blöcken wären 16-Bit-Offsets am sichersten, selbst bei 128-KB- oder 256-KB-Blöcken, die vernünftig sein sollten (4-Byte- oder 8-Byte-Schlüssel vorausgesetzt). Der große Gewinn ist, wie billig und schnell Sie die Offset-Daten einlesen können und wie viel Deserialisierung Sie dadurch einsparen. Ausgezeichneter Vorschlag, nochmals vielen Dank.
Riad Kalla
Dies ist auch der Ansatz in UpscaleDB: upscaledb.com/about.html#varlength
Mathieu Rodic