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:
- Lesen Sie die Wurzel des Index (in diesem Fall 1 Block)
- Durchsuchen Sie den sortierten Block binär, um den Schlüssel zu finden
- Ermittelt den Versatz der Datendatei aus dem Wert
- Suchen Sie den Datensatz in der Datendatei mit dem Offset
- 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 ...
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.
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.