Kapitel 6
BIN
Assets/DBimpl-erweiterbares-hashing-2.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
Assets/DBimpl-erweiterbares-hashing-3.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
Assets/DBimpl-erweiterbares-hashing.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
Assets/DBimpl-grid-beispiel-1.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
Assets/DBimpl-grid-beispiel-5.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
Assets/DBimpl-grid-beispiel-6.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
Assets/DBimpl-grid-files.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
Assets/DBimpl-grid-start.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
Assets/DBimpl-hashing-split.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
Assets/DBimpl-lineares-hashen.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
Assets/DBimpl-lineares-hashing-problem.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
Assets/DBimpl-spiral-hashing-2.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Assets/DBimpl-spiral-hashing.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
Assets/DBimpl-trie.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
Assets/Dbimpl-grid-beispiel-2.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
Assets/Dbimpl-grid-beispiel-3.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
Assets/Dbimpl-grid-beispiel-4.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
Assets/Dbimpl-hashverfahren.png
Normal file
After Width: | Height: | Size: 36 KiB |
@ -1255,3 +1255,292 @@ LSM-Baum: Lesezugriffe
|
||||
- noch nicht betrachtet: Nebenläufigkeitskontrolle und Wiederherstellung im Fehlerfall
|
||||
- diverse Varianten und Optimierungen
|
||||
- LSM-Baum für schreibintensive Workloads
|
||||
|
||||
# Hashing
|
||||
## Hashing
|
||||
- Zugriff über Adressberechnung aus Schlüssel
|
||||
- linearer Adressraum der Grösse $n$
|
||||
- Adressierung in einem Array
|
||||
- Ziel: direkter Zugriff in $O(1)$ statt logarithmisch wie bei Bäumen
|
||||
|
||||
Hashverfahren
|
||||
- Schlüsseltransformation und Überlaufbehandlung
|
||||
- DB-Technik:
|
||||
- Disk-basiert: Bildbereich entspricht Seiten-Adressraum
|
||||
- Hauptspeicher: Adresse in einem Array (Hauptspeicheradresse plus Offset)
|
||||
- Dynamik: dynamische Hashfunktionen oder Re-Hashen
|
||||
|
||||
Grundprinzipien
|
||||
- Basis-Hashfunktion: $h(k)= k mod m$
|
||||
- $m$ oft Primzahl da besseres Verhalten bei Kollisionen
|
||||
- oder $m=2^k$ aufgrund einfacher Berechnungen
|
||||
- Überlauf-Behandlung
|
||||
- Überlaufseiten als verkettete Liste
|
||||
- lineares Sondieren
|
||||
- quadratisches Sondieren
|
||||
- doppeltes Hashen
|
||||
- ...
|
||||
|
||||
Hashverfahren für blockorientierte Datenhaltung
|
||||

|
||||
|
||||
Operationen und Zeitkomplexität
|
||||
- lookup, modify, insert, delete
|
||||
- lookup benötigt maximal $1+ #B(h(w))$ Seitenzugriffe
|
||||
- $#B(h(w))$ Anzahl der Seiten (inklusive der Überlaufseiten) des Buckets für Hash-Wert $h(w)$
|
||||
- Untere Schranke 2 (Zugriff auf Hashverzeichnis plus Zugriff auf erste Seite)
|
||||
|
||||
Statisches Hashen: Probleme
|
||||
- mangelnde Dynamik
|
||||
- Vergrößerung des Bildbereichs erfordert komplettes Neu-Hashen
|
||||
- Wahl der Hashfunktion entscheidend;
|
||||
- Bsp.: Hash-Index aus 100 Buckets, Studenten über 6-stellige MATRNR (wird fortlaufend vergeben) hashen
|
||||
- ersten beiden Stellen: Datensätze auf wenigen Seiten quasi sequenziell abgespeichert
|
||||
- letzten beiden Stellen: verteilen die Datensätze gleichmäßig auf alle Seiten
|
||||
- Sortiertes Ausgeben einer Relation nicht unterstützt
|
||||
|
||||
## Hash-Funktionen
|
||||
- klassisch, etwa Divisions-Rest-Methode
|
||||
$h() = x mod m$
|
||||
- zusammengesetzt, etwa $h(k)= h_2 (h_1 (k))$ (siehe später Spriralhashen)
|
||||
- ordnungserhaltend
|
||||
$k_1 < k_2 => ( h(k_1) = h(k_2) \vee h(k_1) < h(k_2))$
|
||||
- dynamisch (siehe später)
|
||||
- mehrdimensional (siehe später)
|
||||
- materialisiert (etwa Dictionary Encoding, siehe später)
|
||||
|
||||
Ordnungserhaltenes Hashen
|
||||
- Schlüsselwerte werden als 8-Bit-Integer-Werte ohne Vorzeichen kodiert und sind gleichmässig im Bereich $0...2^8-1$ verteilt.
|
||||
- Die Extraktion der ersten drei Bits ergibt eine ordnungserhaltende Hashfunktion für den Bereich $0...2^3-1$.
|
||||
- Sind die Schlüsselwerte nicht gleichverteilt, etwa weil es sich um fortlaufend vergebene Nummern handelt, ist das Ergebnis zwar weiterhin ordnungserhaltend, aber die Hash-Tabelle ist sehr ungleichmäßig gefüllt.
|
||||
|
||||
## Hardware-sensitives Hashen
|
||||
Neue Hardware und Hash-Funktionen
|
||||
- Beobachtung: Hashen mit klassischem Sondieren ungünstig für neue Hardware
|
||||
- schwer parallelisierbar
|
||||
- Clustern von Werten verletzt Nähe der Werte (bei Cache Lines)
|
||||
- Varianten versuchen beide Punkte anzugehen
|
||||
- Cuckoo-Hashing
|
||||
- optimiertes lineares Sondieren
|
||||
- Hopscotch-Hashing
|
||||
- Robin-Hood-Hashing
|
||||
|
||||
Cuckoo-Hashen
|
||||
- Kuckucks-Hashen
|
||||
- soll Parallelität erhöhen im Vergleich zu linearem Sondieren
|
||||
- Idee: Zwei Tabellen mit zwei Hash-Funktionen
|
||||
- im Fall einer Kollision in einer Tabelle wird in der zweiten Tabelle gesucht
|
||||
- ist dort der Platz belegt, wird der dortige Eintrag verdrängt in die jeweils andere Tabelle
|
||||
- _Kuckuck wirft Ei aus dem Nest_
|
||||
- dies wird solange gemacht bis ein freier Platz gefunden wird
|
||||
- Beispiel
|
||||
- zwei einfache Hash-Funktionen, die jeweils die letzte beziehungsweise vorletzte Dezimalstelle einer Zahl extrahieren
|
||||
$h_1(k) = k mod 10$
|
||||
$h_2(k) = (k/10) mod 10$
|
||||
- Bei einer Suche muss immer in beiden Tabellen nachgeschaut werden, also $T_1[h_1(k)] = k\vee T_2[h_2(k)] = k$.
|
||||
- Wir fügen die Zahlen $433, 129$ und $555$ in die Tabelle $T_1$ ein. Beim Einfügen von $783$ ist der Platz in Tabelle $T_1$ belegt, so dass diese Zahl in $T_2$ gespeichert werden muss. Wird nun mit $103$ eine weitere Zahl eingefügt, die mit $433$ unter $h_1$ kollidiert, ist dies mit $h_2$ weiterhin möglich.
|
||||
|
||||
Cuckoo Beispiel
|
||||
- Ergebnis des Einfügens von 433, 129 , 555 , 783 , 103
|
||||
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|
||||
| ----- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| $T_1$ | | | | 433 | | 555 | | | | 129 |
|
||||
| $T_2$ | 103 | | | | | | | | 783 |
|
||||
- Wird nun die Zahl $889$ eingefügt, so sind beide möglichen Positionen belegt. $889$ kann in $T_1$ die dort stehende Zahl $129$ verdrängen, die in $T_2$ an der Position $T_2[2]$ gespeichert werden kann.
|
||||
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|
||||
| ----- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| $T_1$ | | | | 433 | | 555 | | | | 129 |
|
||||
| $T_2$ | 103 | | 129 | | | | | | 783 |
|
||||
- Wird nun $789$ eingefügt, sind wiederum beide Positionen belegt. Das Verdrängen von $889$ aus $T_1$ würde zu einem kaskadierenden Verdrängen führen: $889$ würde in $T_2$ dann $783$ verdrängen, das wiederum $433$ in $T_1$ verdrängen würde. Dies würde gehen da $433$ an der Stelle $T_2[3]$ Platz hätte
|
||||
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|
||||
| ----- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| $T_1$ | | | | 783 | | 555 | | | | 129 |
|
||||
| $T_2$ | 103 | | 129 | 433 | | | | | 783 |
|
||||
|
||||
Weitere Prinzipien der Optimierung
|
||||
- Lokalität von Datenzugriffen verringert die Wahrscheinlichkeit von Cache Misses
|
||||
- Blockung von Daten kann an die Grösse von Cache-Lines (64 Bytes) angepasst werden, und erhöht den Durchsatz
|
||||
- Parallelisierung für SIMD basierend auf einer Vektorisierung der Daten kann insbesondere SIMD-basierte Co-Prozessoren gut ausnutzen, aber greift auch bei MICs
|
||||
|
||||
Optimiertes lineares Sondieren
|
||||
- Lineares Sondieren ist gut geeignet, um den Sondierungsvorgang auf Vektoren zu parallelisieren
|
||||
- Suchschlüssel kann in einen Vektor der Länge $n$ an alle Positionen kopiert werden
|
||||
- beginnend ab derem initialen Sondierungspunkt $h(k)$ können dann Vektoren jeweils mit Vektoren aus der Hash-Tabelle verglichen werden, also zuerst mit
|
||||
$H[h)k),...,h(k)+n - 1]$, dann mit $H[h(k) + n,...,h(k) + 2n-1 ]$, etc.
|
||||
- Vergleich kann parallel erfolgen; muss sowohl auf Vorhandensein von $k$ als auch auf Existenz einer leeren Position prüfen
|
||||
|
||||
Hopscotch-Hashen
|
||||
- Hopscotch: _Himmel und Hölle_ beziehungsweise _wild herumhopsen_
|
||||
- Begrenzung des Sondierungsraum auf eine (konstante) Länge
|
||||
- Idee:
|
||||
- beim Einfügen erfolgt die Suche (parallel) in der festen Nachbarschaft
|
||||
- wird Schlüssel $k$ nicht gefunden und existiert kein freier Slot in der festen Nachbarschaft, dann wird versucht, $k$ mit einem anderen Schlüssel aus der festen Nachbarschaft zu tauschen
|
||||
- dafür wird die nächste freie Stelle gesucht; von dieser wird rückwärts in Richtung $h(k)$ gesucht und jeder Eintrag $k′$ untersucht
|
||||
- wenn die aktuelle freie Stelle noch in der festen Nachbarschaft von $k′$ liegt, wird getauscht: $k′$ springt auf die freie Stelle
|
||||
|
||||
Robin-Hood-Hashen
|
||||
- Robin-Hood: _Nimm von den Reichen gib es den Armen_
|
||||
- Basisidee: in der Situation, dass beim Sondieren für $k$ ein Platz bereits mit einem Element $k′$ besetzt ist, wird der nächste Sondierungsschritt mit demjenigen Element weitergeführt, das die kleinere Distanz zum eigentlichen Hash-Wert $h(k)$ bzw. $h(k′)$ hat
|
||||
|
||||
## Dynamische Hash-Verfahren
|
||||
Lineares Hashen
|
||||
- Folge von Hash-Funktionen, die wie folgt charakterisiert sind:
|
||||
- $h_i$: dom(Primärschlüssel) ->$\{0,..., 2^i \times N\}$ ist eine Folge von Hash-Funktionen mit $i\in\{0,1,2,...\}$ und $N$ als Anfangsgröße des Hash-Verzeichnisses
|
||||
- Wert von $i$ wird auch als Level der Hash-Funktion bezeichnet
|
||||
- $dom(Primärschlüssel)$ wird im folgenden als $dom(Prim)$ abgekürzt
|
||||
- Für diese Hash-Funktionen gelten die folgenden Bedingungen:
|
||||
- $h_{i+1}(w) = h_i(w)$ für etwa die Hälfte aller $w\in dom(Prim)$
|
||||
- $h_{i+1}(w) = h_i(w) + 2^i\times N$ für die andere Hälfte
|
||||
- Bedingungen sind zum Beispiel erfüllt, wenn $h_i(w)$ als $w mod(2^i\times N)$ gewählt wird
|
||||
- Darstellung durch Bit-Strings, Hinzunahme eines Bits verdoppelt Bildbereich
|
||||
|
||||
Prinzip lineares Hashen
|
||||
- für ein $w$ höchstens zwei Hash-Funktionen zuständig, deren Level nur um 1 differiert, Entscheidung zwischen diesen beiden durch Split-Zeiger
|
||||
- $sp$ Split-Zeiger (gibt an, welche Seite als nächstes geteilt wird)
|
||||
- $lv$ Level (gibt an, welche Hash-Funktionen benutzt werden)
|
||||
- Aus Split-Zeiger und Level läßt sich die Gesamtanzahl $Anz$ der belegten Seiten wie folgt berechnen:
|
||||
- $Anz = 2^{lv} + sp$
|
||||
- Beide Werte werden am Anfang mit 0 initialisiert.
|
||||

|
||||
|
||||
Lookup
|
||||
- $$s := h_{lv}(w)$;
|
||||
- if $s < sp$
|
||||
- then $s := h_{lv + 1}(w)$;
|
||||
- zuerst Hash-Wert mit der "kleineren" Hash-Funktion bestimmen
|
||||
- liegt dieser unter dem Wert des Split-Zeigers => größere Hash-Funktion verwenden
|
||||
|
||||
Splitten einer Seite
|
||||
1. Die Sätze der Seite (Bucket), auf die $sp$ zeigt, werden mittels $h_{lv+1}$ neu verteilt (ca. die Hälfte der Sätze wird auf Seite (Bucket) unter Hash-Nummer $2^{lv}*N +sp$ verschoben)
|
||||
2. Der Split-Zeiger wird weitergesetzt: $sp:=sp +1;$
|
||||
3. Nach Abarbeiten eines Levels wird wieder bei Seite 0 begonnen; der Level wird um 1 erhöht:
|
||||
|
||||
```
|
||||
if sp = 2^{lv} * N then
|
||||
begin
|
||||
lv := lv + 1 ;
|
||||
sp := 0
|
||||
end;
|
||||
```
|
||||

|
||||
|
||||
|
||||
Problem lineares Hashen
|
||||

|
||||
|
||||
Erweiterbares Hashen
|
||||
- Problem: Split erfolgt an fester Position, nicht dort wo Seiten überlaufen
|
||||
- Idee: binärer Trie zum Zugriff auf Indexseiten
|
||||
- Blätter unterschiedlicher Tiefe
|
||||
- Indexseiten haben Tiefenwert
|
||||
- Split erfolgt bei Überlauf
|
||||
- aber: Speicherung nicht als Trie, sondern als Array
|
||||
- entspricht vollständigem Trie mit maximaler Tiefe
|
||||
- "shared" Seiten als Blätter
|
||||
- Array der Grösse 2 _d_ für maximale Tiefe _d_
|
||||
- erfordert nun nur einen Speicherzugriff!
|
||||
- bei Überlauf: Indexgrösse muss möglicherweise verdoppelt werden!
|
||||
- Ausgangslage:
|
||||
- Einfügen von 00111111 würde Überlauf bei erreichter maximaler Tiefe erzeugen
|
||||

|
||||
- Verdopplung der Indexgrösse
|
||||

|
||||
- nun möglich: Split der Seite
|
||||

|
||||
|
||||
Variante: Array als Trie gespeichert
|
||||

|
||||
|
||||
Spiral-Hashen
|
||||
- Problem: zyklisch erhöhte Wahrscheinlichkeit des Splittens
|
||||
- Lösung: unterschiedliche Dichte der Hashwerte
|
||||
- Interpretation der Bit-Strings als binäre Nachkommadarstellung einer Zahl zwischen $0.0$ und $1.0$
|
||||
- Funktion von $[0.0,1.0] -> [0.0,1.0]$ so dass Dichte gleichmässig verteilter Werte nahe $1.0$ doppelt so gross ist wie nahe $0.0$
|
||||
- Umverteilung mittels Exponentialfunktion
|
||||
- Funktion $exp(n)$ $exp(n) = 2^n - 1$ erfüllt die Bedingungen
|
||||
- insbesondere gilt $2^0 - 1 = 0$ und $2^1 - 1 = 1$
|
||||
- Hashfunktion exhash
|
||||
$exhash(k) = exp(h(k)) = 2^{h(k)} - 1$
|
||||
- Wirkung der verwendeten Hashfunktion im Intervall $0.0$ bis $1.0$
|
||||
| $n$ | $2^n-1$ |
|
||||
| ----- | ----------- |
|
||||
| $0.0$ | $0.0$ |
|
||||
| $0.1$ | $0.0717735$ |
|
||||
| $0.2$ | $0.1486984$ |
|
||||
| $0.3$ | $0.2311444$ |
|
||||
| $0.4$ | $0.3195079$ |
|
||||
| $0.5$ | $0.4142136$ |
|
||||
| $0.6$ | $0.5157166$ |
|
||||
| $0.7$ | $0.6245048$ |
|
||||
| $0.8$ | $0.7411011$ |
|
||||
| $0.9$ | $0.866066$ |
|
||||
| $1.0$ | $1.0$ |
|
||||
- Spiralförmiges Ausbreiten
|
||||
- Ausgangslage: 4 Seiten der Tiefe 2
|
||||

|
||||
- Spiralförmiges Ausbreiten
|
||||
- Split der Seite mit der höchsten Dichte
|
||||
- Ergebnis: 5 Seiten, davon 3 der Tiefe 2 und 2 der Tiefe 3
|
||||

|
||||
|
||||
## Grid-File
|
||||
Grid-Files
|
||||
- bekannteste und von der Technik her attraktive mehrdimensionale Dateiorganisationsform
|
||||
- eigene Kategorie: Elemente der Schlüsseltransformation wie bei Hashverfahren und Indexdateien wie bei Baumverfahren kombiniert
|
||||
- deshalb hier bei Hash-Verfahren betrachtet
|
||||
|
||||
Grid-File: Zielsetzungen
|
||||
- Prinzip der 2 Plattenzugriffe: Jeder Datensatz soll bei einer _exact-match_ -Anfrage in 2 Zugriffen erreichbar sein
|
||||
- Zerlegung des Datenraums in Quader: _n_ -dimensionale Quader bilden die Suchregionen im Grid-File
|
||||
- Prinzip der Nachbarschaftserhaltung: Ähnliche Objekte sollten auf der gleichen Seite gespeichert werden
|
||||
- Symmetrische Behandlung aller Raum-Dimensionen: _partial-match_ -Anfragen ermöglicht
|
||||
- Dynamische Anpassung der Grid-Struktur beim Löschen und Einfügen
|
||||
|
||||
Prinzip der zwei Plattenzugriffe
|
||||
- bei exact-match
|
||||
1. gesuchtes _k_ -Tupel auf Intervalle der Skalen abbilden; als Kombination der ermittelten Intervalle werden Indexwerte errechnet; Skalen im Hauptspeicher => noch kein Plattenzugriff
|
||||
2. über errechnete Indexwerte Zugriff auf das _Grid-Directory_ ; dort Adressen der Datensatz-Seiten gespeichert; erster _Plattenzugriff_.
|
||||
3. Der Datensatz-Zugriff: zweiter _Plattenzugriff_.
|
||||
|
||||
Aufbau eines Grid-Files
|
||||

|
||||
- Grid: $k$ eindimensionale Felder (Skalen), jede Skala repräsentiert Attribut
|
||||
- _Skalen_ bestehen aus Partition der zugeordneten Wertebereiche in Intervalle
|
||||
- Grid-Directory besteht aus Grid-Zellen, die den Datenraum in Quader zerlegen
|
||||
- Grid-Zellen bilden eine Grid-Region, der genau eine Datensatz-Seite zugeordnet wird
|
||||
- Grid-Region: $k$-dimensionales, konvexes (Regionen sind paarweise disjunkt)
|
||||
|
||||
Operationen
|
||||
- Zu Anfang: Zelle = Region = eine Datensatz-Seite
|
||||
- Seitenüberlauf:
|
||||
- Seite wird geteilt
|
||||
- falls zugehörige Gridregion aus nur einer Gridzelle besteht, muss ein Intervall auf einer Skala in zwei Intervalle unterteilt werden
|
||||
- besteht Region aus mehreren Zellen, so werden diese Zellen in einzelne Regionen zerlegt
|
||||
- Seitenunterlauf:
|
||||
- Zwei Regionen zu einer zusammenfassen, falls das Ergebnis eine neue, konvexe Region ergibt
|
||||
|
||||
Beispiel
|
||||
- Start-Grid-File
|
||||

|
||||
- Datensätze einfügen: $(45,D),(2,B),(87,S),(75,M),(55,K),(3,Y),(15,D),(25,K),(48,F)$
|
||||
- jede Seite des Grid-Files fasst bis zu drei Datensätze
|
||||
- Eingefügt: $(45, D), (2, B), (87, S)$
|
||||

|
||||
- Einfügen von $(75, M)$ erzwingt Split
|
||||

|
||||
- Eingefügt: $(55, K)$
|
||||

|
||||
- Einfügen von $(3, Y)$ erzwingt wiederum einen Split
|
||||

|
||||
- Eingefügt: (15, D), (25, K),
|
||||

|
||||
- Einfügen von (48, F) erzwingt wiederum einen Split
|
||||

|
||||
|
||||
|
||||
Buddy-System
|
||||
- Beschriebenes Verfahren: Buddy-System (Zwillings-System)
|
||||
- Die im gleichen Schritt entstandenen Zellen können zu Regionen zusammengefasst werden; Keine andere Zusammenfassung von Zellen ist im Buddy-System erlaubt
|
||||
- Unflexibel beim Löschen: nur Zusammenfassungen von Regionen erlaubt, die vorher als Zwillinge entstanden waren
|
||||
- Beispiel: $(15,D)$ löschen: Seiten 1 und 4 zusammenfassen; $(87,S)$ löschen, Seite 2 zwar unterbelegt, kann aber mit keiner anderen Seite zusammengefasst werden
|
||||
|
||||
|