Einführung
„Man muss mit aller Kraft rennen, um nur am selben Ort zu bleiben,
und um irgendwohin zu gelangen, muss man mindestens doppelt so schnell rennen!“
(c) Alice im Wunderland
Vor einiger Zeit wurde ich gebeten, einen Vortrag für die Analysten unseres Unternehmens zum Thema Datenmodellierung zu halten. Da wir oft lange an Projekten arbeiten (manchmal mehrere Jahre), verlieren wir den Blick auf die Entwicklungen in der Welt der IT-Technologien. In unserem Unternehmen, so wie es scheint, werden in vielen Projekten keine NoSQL-Datenbanken verwendet (zumindest bisher nicht), deshalb habe ich in meinem Vortrag einige Aufmerksamkeit darauf gerichtet, insbesondere am Beispiel von HBase, und versucht, das Material für diejenigen aufzubereiten, die noch nie mit ihnen gearbeitet haben. Insbesondere habe ich einige Besonderheiten der Datenmodellierung anhand eines Beispiels illustriert, das ich vor einigen Jahren gelesen habe. . Während ich die Beispiele durchging, habe ich verschiedene Lösungsmöglichkeiten für dieselbe Aufgabe verglichen, um den Zuhörern die Hauptideen besser zu vermitteln.
Kürzlich, aus Langeweile, habe ich mich gefragt (die langen Maifeiertage im Quarantänemodus laden dazu besonders ein), inwieweit theoretische Überlegungen der Praxis entsprechen. So entstand die Idee zu diesem Artikel. Ein Entwickler, der bereits länger mit NoSQL arbeitet, könnte möglicherweise nichts Neues daraus lernen (und könnte daher gleich zur nächsten Hälfte des Artikels übergehen). Doch für Analytiker, die bisher noch nicht intensiv mit NoSQL gearbeitet haben, denke ich, dass es hilfreich sein wird, um grundlegende Vorstellungen über die Besonderheiten der Datenmodellierung für HBase zu vermitteln.
Analyse eines Beispiels
Meiner Meinung nach ist es wichtig, bevor man mit NoSQL-Datenbanken beginnt, sorgfältig die Vor- und Nachteile abzuwägen. Oft kann die Aufgabe auch mit traditionellen relationalen Datenbanksystemen gelöst werden. Daher sollte NoSQL nur bei triftigen Gründen eingesetzt werden. Wenn dennoch die Entscheidung getroffen wurde, eine NoSQL-Datenbank zu verwenden, ist zu beachten, dass sich die Ansätze im Design deutlich unterscheiden können. Besonders einige davon können ungewohnt sein für diejenigen, die zuvor nur mit relationalen Datenbanken gearbeitet haben. In der relationalen Welt beginnen wir normalerweise mit dem Modellieren des Anwendungsbereichs und führen gegebenenfalls eine Denormalisierung des Modells durch. Bei NoSQL hingegen müssen wir von Anfang an die geplanten Nutzungsszenarien für die Daten berücksichtigen und die Daten von vornherein denormalisieren. Darüber hinaus gibt es eine Reihe anderer Unterschiede, die im Folgenden erläutert werden.
Betrachten wir die folgende „synthetische“ Aufgabe, mit der wir weiterarbeiten werden:
Es ist notwendig, die Speicherstruktur einer Freundesliste für ein abstraktes soziales Netzwerk zu entwerfen. Zur Vereinfachung nehmen wir an, dass alle Verbindungen gerichtet sind (wie bei Instagram, nicht bei LinkedIn). Die Struktur sollte es ermöglichen, effizient:
- Die Frage zu beantworten, ob Benutzer A Benutzer B liest (Lesevorlage)
- Verbindungen hinzuzufügen oder zu entfernen, wenn Benutzer A Benutzer B folgt oder ihm entfolgt (Datenänderungsvorlage)
Natürlich gibt es viele Ansätze zur Lösung dieser Aufgabe. In einer herkömmlichen relationalen Datenbank würden wir wahrscheinlich einfach eine Beziehungstabelle erstellen (möglicherweise typisiert, wenn es erforderlich ist, eine Benutzergruppe wie Familie, Arbeit etc. zu speichern, zu der dieser „Freund“ gehört), und um die Zugriffsgeschwindigkeit zu optimieren, würden wir Indizes oder Partitionierung hinzufügen. Wahrscheinlich würde die endgültige Tabelle ungefähr so aussehen:
user_id
friend_id
Vasya
Petya
Vasya
Olya
Hier und im Folgenden werde ich zur besseren Veranschaulichung und zum besseren Verständnis anstelle von IDs die Namen angeben.
Im Falle von HBase wissen wir, dass:
- eine effektive Suche, die keinen Full Table Scan erfordert, möglich ist ausschließlich über den Schlüssel
- Genau aus diesem Grund ist es eine schlechte Idee, gewohnheitsmäßig SQL-Abfragen an solche Datenbanken zu schreiben; technisch gesehen können Sie natürlich aus Impala SQL-Abfragen mit Joins und anderer Logik an HBase senden, aber wie effektiv wird das sein ...
Deshalb sind wir gezwungen, die Benutzer-ID als Schlüssel zu verwenden. Der erste Gedanke zur Frage „Wo und wie speichern wir die IDs der Freunde?“ könnte die Idee sein, sie in Spalten zu speichern. Diese naheliegende und „naive“ Variante würde ungefähr so aussehen (nennen wir sie Option 1 (default), um später darauf zu verweisen):
RowKey
Spalten
Vasya
1: Petja
2: Olja
3: Dascha
Petya
1: Mascha
2: Wassja
Hier entspricht jede Zeile einem Netzwerkbenutzer. Die Spalten haben Namen: 1, 2, … — je nach Anzahl der Freunde, und in den Spalten werden die IDs der Freunde gespeichert. Es ist wichtig zu beachten, dass jede Zeile eine unterschiedliche Anzahl an Spalten haben wird. Im obigen Beispiel hat eine Zeile drei Spalten (1, 2 und 3), während die andere nur zwei hat (1 und 2) – hier haben wir beide Eigenschaften von HBase genutzt, die relationale Datenbanken nicht besitzen:
- die Möglichkeit, die Zusammensetzung der Spalten dynamisch zu ändern (Freund hinzufügen -> Spalte hinzufügen, Freund löschen -> Spalte löschen)
- Bei verschiedenen Zeilen kann die Zusammensetzung der Spalten unterschiedlich sein.
Lassen Sie uns unsere Struktur auf die Anforderungen der Aufgabe überprüfen:
- Daten lesen: Um zu verstehen, ob Vasya Olya folgt, müssen wir die gesamte Zeile mit dem Schlüssel RowKey = „Vasya“ auslesen und die Spaltenwerte durchgehen, bis wir Olya darin „treffen“. Alternativ können wir alle Spaltenwerte durchgehen, Olya nicht finden und die Antwort False zurückgeben;
- Datenänderung: Freund hinzufügen: Für eine ähnliche Aufgabe müssen wir auch die gesamte Zeile mit dem Schlüssel RowKey = „Vasya“ auslesen, um die Gesamtzahl seiner Freunde zu zählen. Diese Gesamtzahl benötigen wir, um die Spaltennummer zu bestimmen, in die wir die ID des neuen Freundes schreiben müssen.
- Datenänderung: Freund entfernen:
- Wir müssen auslesen die gesamte Zeile mit dem Schlüssel RowKey = „Vasya“ und die Spalten durchgehen, um die zu finden, in der der zu entfernende Freund gespeichert ist;
- Anschließend müssen wir, nach dem Entfernen des Freundes, alle Daten um eine Spalte „verschieben“, um keine „Lücken“ in ihrer Nummerierung zu erhalten.
Lassen Sie uns nun beurteilen, wie leistungsfähig die Algorithmen sein werden, die wir auf der Seite der „hypothetischen Anwendung“ implementieren müssen. . Lassen Sie uns die Größe unseres hypothetischen sozialen Netzwerks als n bezeichnen. Dann kann die maximale Anzahl von Freunden eines Benutzers (n-1) betragen. Dieses (-1) können wir für unsere Zwecke vernachlässigen, da es im Rahmen der Verwendung von O-Symbolik unerheblich ist.
- Daten lesen: Es ist notwendig, die gesamte Zeile zu lesen und theoretisch alle ihre Spalten zu durchlaufen. Daher wird die oberste Kostenschätzung ungefähr O(n) sein.
- Datenänderung: Freund hinzufügen: Um die Anzahl der Freunde zu bestimmen, müssen alle Spalten der Zeile durchlaufen werden, bevor eine neue Spalte eingefügt wird => O(n)
- Datenänderung: Freund entfernen:
- Ähnlich wie beim Hinzufügen – es ist erforderlich, theoretisch alle Spalten zu durchlaufen => O(n)
- Nach dem Löschen der Spalten müssen wir sie 'verschieben'. Wenn wir dies direkt angehen, werden theoretisch bis zu (n-1) Operationen benötigt. In der praktischen Umsetzung werden wir jedoch einen anderen Ansatz verwenden, der eine 'pseudo-Verschiebung' in einer festen Anzahl von Operationen umsetzt – das heißt, es wird eine konstante Zeit in Anspruch nehmen, unabhängig von n. Diese konstante Zeit (genauer gesagt O(2)) ist im Vergleich zu O(n) vernachlässigbar. Der Ansatz ist im folgenden Diagramm veranschaulicht: Wir kopieren einfach die Daten aus der 'letzten' Spalte in die, aus der wir die Daten entfernen möchten, und löschen dann die letzte Spalte:

Insgesamt haben wir in allen Szenarien eine asymptotische rechnerische Komplexität von O(n) erreicht.
Vielleicht haben Sie bereits bemerkt, dass wir fast immer die gesamte Zeile aus der Datenbank lesen müssen, wobei in zwei von drei Fällen dies nur geschieht, um alle Spalten zu durchlaufen und die Gesamtanzahl der Freunde zu zählen. Daher könnte man als Optimierungsansatz eine Spalte "count" hinzufügen, in der die Gesamtanzahl der Freunde jedes Nutzers gespeichert wird. In diesem Fall müssten wir die gesamte Zeile nicht mehr lesen, um die Gesamtanzahl der Freunde zu zählen, sondern könnten einfach die Spalte "count" abfragen. Wichtig ist, dass wir "count" bei Datenmanipulationen aktualisieren. So erhalten wir eine Verbesserung. Variante 2 (count):
RowKey
Spalten
Vasya
1: Petja
2: Olja
3: Dascha
count: 3
Petya
1: Mascha
2: Wassja
count: 2
Im Vergleich zur ersten Variante:
- Daten lesen: Um die Frage "Liest Wanja Olya?" zu beantworten, hat sich nichts geändert => O(n)
- Datenänderung: Freund hinzufügen: Wir haben das Hinzufügen eines neuen Freundes vereinfacht, da wir jetzt nicht mehr die gesamte Zeile lesen und ihre Spalten durchgehen müssen, sondern nur den Wert der Spalte "count" abfragen können, um sofort die Spaltennummer für die Hinzufügung des neuen Freundes zu bestimmen. Dies führt zu einer Reduzierung der Berechnungskomplexität auf O(1).
- Datenänderung: Freund entfernen: Beim Entfernen eines Freundes können wir auch diese Spalte nutzen, um die Anzahl der Eingabe-/Ausgabeoperationen beim „Verschieben“ von Daten um eine Zelle nach links zu reduzieren. Allerdings bleibt die Notwendigkeit, durch die Spalten zu iterieren, um die zu löschende zu finden, bestehen, daher => O(n)
- Andererseits müssen wir bei der Aktualisierung der Daten jetzt jedes Mal auch die Spalte „count“ aktualisieren, aber dafür benötigen wir eine konstante Zeit, die im Rahmen der O-Notation vernachlässigt werden kann.
Insgesamt erscheint Option 2 etwas optimierter, es ist jedoch eher eine „Evolution statt Revolution“. Um eine „Revolution“ zu vollziehen, benötigen wir Option 3 (col).
Lass uns alles „auf den Kopf stellen“: Wir weisen der Spalte die Benutzer-ID zu! Was in der Spalte selbst gespeichert wird, ist für uns nicht mehr von Bedeutung, lassen wir einfach die Zahl 1 stehen (tatsächlich könnte man dort zum Beispiel eine Gruppe „Familie/Freunde/usw.“ speichern). Dieser Ansatz mag den unvorbereiteten „Durchschnittsbürger“ überraschen, der zuvor keine Erfahrung mit NoSQL-Datenbanken hatte, aber genau das ermöglicht es, das Potenzial von HBase in dieser Aufgabe sehr viel effektiver zu nutzen:
RowKey
Spalten
Vasya
Petja: 1
Olya: 1
Dasha: 1
Petya
Mascha: 1
Vasya: 1
Hier profitieren wir sofort von mehreren Vorteilen. Um diese zu verstehen, analysieren wir die neue Struktur und bewerten die rechnerische Komplexität:
- Daten lesen: Um die Frage zu beantworten, ob Vasja Olya folgt, genügt es, eine Spalte "Olya" zu lesen: Wenn sie existiert, ist die Antwort True, andernfalls False => O(1)
- Datenänderung: Freund hinzufügen: Freund hinzufügen: Es reicht aus, einfach eine neue Spalte "Freund-ID" hinzuzufügen => O(1)
- Datenänderung: Freund entfernen: Es genügt, einfach die Spalte "Freund-ID" zu löschen => O(1)
Wie wir sehen, besteht der wesentliche Vorteil dieses Speichermodells darin, dass wir in all unseren erforderlichen Szenarien nur mit einer einzigen Spalte arbeiten, wodurch wir vermeiden, die gesamte Zeile aus der Datenbank auszulesen, geschweige denn alle Spalten dieser Zeile zu durchsuchen. An dieser Stelle könnte man aufhören, aber...
Man könnte sich auch darüber Gedanken machen und den Weg zur Leistungsoptimierung und zur Verringerung der E/A-Operationen beim Zugriff auf die Datenbank weiter beschreiten. Was wäre, wenn wir die vollständigen Informationen über die Beziehung direkt im Schlüssel der Zeile speichern? Das heißt, den Schlüssel in der Form userID.friendID erstellen? In diesem Fall müssten wir nicht einmal die Spalten der Zeile auslesen (Option 4(row)):
RowKey
Spalten
Vasja.Petja
Petja: 1
Vasja.Olja
Olya: 1
Vasja.Dasha
Dasha: 1
Petja.Masha
Mascha: 1
Petja.Vasja
Vasya: 1
Es ist offensichtlich, dass die Bewertung aller Datenmanipulationsszenarien in dieser Struktur, wie auch im vorherigen Fall, O(1) sein wird. Der Unterschied zu Variante 3 wird sich ausschließlich in der Effizienz der Datenbank-Eingabe- und -Ausgabeoperationen zeigen.
Und hier ist das letzte „Schmankerl“. Es ist leicht zu erkennen, dass in Variante 4 der Schlüssel des Strings eine variable Länge haben wird, was möglicherweise die Leistung beeinflussen kann (wir erinnern uns daran, dass HBase Daten als Byte-Arrays speichert und die Strings in den Tabellen nach dem Schlüssel sortiert sind). Außerdem haben wir einen Trennzeichen, das in einigen Szenarien möglicherweise verarbeitet werden muss. Um diesen Einfluss auszuschließen, könnten Hashes von userID und friendID verwendet werden. Da beide Hashes eine feste Länge haben werden, können sie einfach ohne Trennzeichen zusammengefügt werden. Dann würden die Daten in der Tabelle folgendermaßen aussehen:Variante 5(Hash)):
RowKey
Spalten
dc084ef00e94aef49be885f9b01f51c01918fa783851db0dc1f72f83d33a5994
Petja: 1
dc084ef00e94aef49be885f9b01f51c0f06b7714b5ba522c3cf51328b66fe28a
Olya: 1
dc084ef00e94aef49be885f9b01f51c00d2c2e5d69df6b238754f650d56c896a
Dasha: 1
1918fa783851db0dc1f72f83d33a59949ee3309645bd2c0775899fca14f311e1
Mascha: 1
1918fa783851db0dc1f72f83d33a5994dc084ef00e94aef49be885f9b01f51c0
Vasya: 1
Offensichtlich wird die algorithmische Komplexität, die mit einer solchen Struktur in den von uns betrachteten Szenarien verbunden ist, die gleiche wie bei Variante 4 sein – also O(1).
Zusammenfassend fassen wir alle unsere Einschätzungen zur Berechnungskomplexität in einer Tabelle zusammen:
Freund hinzufügen
Freund überprüfen
Freund entfernen
Option 1 (default)
O(n)
O(n)
O(n)
Variante 2 (count)
O(1)
O(n)
O(n)
Variante 3 (column)
O(1)
O(1)
O(1)
Variante 4 (row)
O(1)
O(1)
O(1)
Variante 5 (hash)
O(1)
O(1)
O(1)
Wie zu sehen ist, erscheinen die Varianten 3-5 am vielversprechendsten und gewährleisten theoretisch die Ausführung aller erforderlichen Datentransformationen in konstanter Zeit. In unserer Aufgabenstellung gibt es keine ausdrückliche Anforderung, eine Liste aller Freunde des Benutzers zu erhalten, aber in der realen Projektarbeit wäre es für uns als gute Analysten ratsam, diese Möglichkeit vorherzusehen und vorzubeugen. Daher neige ich zu Variante 3. Es ist jedoch durchaus möglich, dass diese Anfrage in einem realen Projekt bereits mit anderen Mitteln gelöst wurde, daher sollten wir ohne einen umfassenden Blick auf die gesamte Aufgabe keine endgültigen Schlüsse ziehen.
Vorbereitung des Experiments
Die oben genannten theoretischen Überlegungen möchten wir in der Praxis überprüfen – das war das Ziel, das während der langen Wochenenden entstanden ist. Dazu müssen wir die Geschwindigkeit unserer "hypothetischen Anwendung" in allen beschriebenen Nutzungsszenarien der Datenbank bewerten und auch das Wachstum dieser Zeit in Abhängigkeit von der Größe des sozialen Netzwerks (n) messen. Der Zielparameter, der uns interessiert und den wir während des Experiments messen werden, ist die Zeit, die die "hypothetische Anwendung" für die Durchführung einer "Geschäftsoperation" benötigt. Unter "Geschäftsoperation" verstehen wir eines der Folgenden:
- Hinzufügen eines neuen Freundes
- Überprüfen, ob Benutzer A ein Freund von Benutzer B ist
- Löschen eines Freundes
So ergibt sich unter Berücksichtigung der in der ursprünglichen Aufgabenstellung definierten Anforderungen folgendes Szenario zur Überprüfung:
- Datenaufzeichnung. Generieren Sie zufällig ein Netzwerk der Größe n. Um eine größere Nähe zur "realen Welt" zu erreichen, ist die Anzahl der Freunde jedes Benutzers ebenfalls eine Zufallsgröße. Messen Sie die Zeit, die unsere „hypothetische Anwendung“ benötigt, um alle generierten Daten in HBase zu speichern. Teilen Sie dann die erhaltene Zeit durch die Gesamtanzahl der hinzugefügten Freunde, um die durchschnittliche Zeit für eine „Geschäftstransaktion“ zu berechnen.
- Daten lesen. Erstellen Sie für jeden Benutzer eine Liste von „Identitäten“, für die er prüfen muss, ob er ihnen folgt oder nicht. Die Länge der Liste entspricht ungefähr der Anzahl der Freunde des Benutzers, wobei in der Hälfte der geprüften Fälle die Antwort „Ja“ und in der anderen Hälfte „Nein“ sein sollte. Die Prüfung erfolgt in einer Reihenfolge, sodass sich die Antworten „Ja“ und „Nein“ abwechseln (d.h. in jedem zweiten Fall müssen wir alle Spalten der Zeile für die Varianten 1 und 2 durchgehen). Teilen Sie die Gesamtzeit der Überprüfung anschließend durch die Anzahl der geprüften Freunde, um die durchschnittliche Zeit für die Überprüfung eines Subjekts zu erhalten.
- Daten löschen. Entfernen Sie alle Freunde des Nutzers. Der Löschvorgang erfolgt zufällig (das heißt, die ursprüngliche Liste wird „durchmischt“, die zur Datenspeicherung verwendet wurde). Die Gesamtzeit für die Überprüfung wird dann durch die Anzahl der zu löschenden Freunde geteilt, um die durchschnittliche Zeit pro Überprüfung zu erhalten.
Die Szenarien müssen für jede der 5 Varianten der Datenmodelle und für unterschiedliche Größen des sozialen Netzwerks durchlaufen werden, um zu sehen, wie sich die Zeit mit dem Wachstum ändert. Innerhalb einer n Verbindung im Netzwerk müssen die Liste der Nutzer zur Überprüfung und die Verbindungen natürlich für alle 5 Varianten identisch sein.
Um es besser zu verstehen, gebe ich unten ein Beispiel für generierte Daten für n= 5. Der geschriebene „Generator“ gibt drei Wörterbücher von IDs aus:
- das erste – zum Einfügen
- das zweite – zur Überprüfung
- das dritte – zum Löschen
{0: [1], 1: [4, 5, 3, 2, 1], 2: [1, 2], 3: [2, 4, 1, 5, 3], 4: [2, 1]} # insgesamt 15 Freunde
{0: [1, 10800], 1: [5, 10800, 2, 10801, 4, 10802], 2: [1, 10800], 3: [3, 10800, 1, 10801, 5, 10802], 4: [2, 10800]} # insgesamt 18 überprüfte Subjekte
{0: [1], 1: [1, 3, 2, 5, 4], 2: [1, 2], 3: [4, 1, 2, 3, 5], 4: [1, 2]} # insgesamt 15 Freunde
Wie man erkennen kann, sind alle IDs über 10.000 im Prüfwörterbuch genau die, die mit großer Sicherheit ein False zurückgeben. Das Einfügen, Überprüfen und Löschen von "Freunden" erfolgt genau in der im Wörterbuch angegebenen Reihenfolge.
Das Experiment wurde auf einem Laptop mit Windows 10 durchgeführt, wobei in einem Docker-Container eine HBase-Datenbank und in einem anderen Python mit Jupyter Notebook ausgeführt wurde. Docker wurden 2 CPU-Kerne und 2 GB RAM zugewiesen. Die gesamte Logik, sowohl die Emulation der Funktionsweise einer "hypothetischen Anwendung" als auch die "Wrapper" für die Generierung von Testdaten und zur Zeitmessung, wurde in Python geschrieben. Für die Arbeit mit HBase wurde die Bibliothek , zur Berechnung der Hashes (MD5) für Variante 5 – hashlib
In Anbetracht der Rechenleistung des spezifischen Laptops wurde experimentell die Ausführung für n = 10, 30, … 170 gewählt – als die Gesamtlaufzeit des vollständigen Testzyklus (alle Szenarien für alle Varianten für alle n) noch mehr oder weniger im Rahmen blieb und in die Zeit einer Teepause (im Durchschnitt 15 Minuten) passte.
Hier ist anzumerken, dass wir in diesem Experiment nicht unbedingt absolute Leistungszahlen bewerten. Selbst der relative Vergleich zweier Varianten kann ungenau sein. Derzeit interessiert uns vor allem die Art der Veränderung der Zeit in Abhängigkeit von n, da es aufgrund der oben genannten Konfiguration des „Teststandes“ sehr schwierig ist, zeitliche Schätzungen zu erhalten, die von zufälligen und anderen Faktoren „bereinigt“ sind (und diese Aufgabe war auch nicht gesetzt).
Ergebnis des Experiments
Der erste Test – wie sich die Zeit ändert, die benötigt wird, um die Freundesliste zu füllen. Das Ergebnis – im Diagramm unten.

Die Varianten 3-5 zeigen erwartungsgemäß nahezu konstante Zeiten für die „Geschäftsoperation“, die unabhängig von der Netzwerkausdehnung sind und keinen merklichen Leistungsunterschied aufweisen.
Option 2 also shows a constant performance, but slightly worse, being practically twice as slow compared to options 3-5. This is encouraging as it aligns with theory—this option's I/O operations to/from HBase are indeed twice as high. This may serve as indirect evidence that our test setup generally provides decent accuracy.
As expected, Option 1 turns out to be the slowest, demonstrating linear growth in time taken to add one another, proportional to the size of the network.
Now, let's take a look at the results of the second test.

Die Varianten 3-5 verhalten sich erwartungsgemäß – konstante Zeiten, unabhängig von der Netzgröße. Die Varianten 1 und 2 zeigen ein lineares Wachstum der Zeit mit zunehmender Netzgröße und ähnliche Leistungen. Dabei ist Variante 2 etwas langsamer – offenbar wegen der Notwendigkeit, die zusätzliche Spalte „count“ zu lesen und zu verarbeiten, was bei wachsendem n deutlicher wird. Ich möchte jedoch keine Schlussfolgerungen ziehen, da die Genauigkeit dieses Vergleichs relativ gering ist. Außerdem wechselten diese Verhältnisse (welche Variante, 1 oder 2, schneller ist) von Ausführung zu Ausführung, behielten jedoch den Charakter der Abhängigkeit und „liefen Kopf an Kopf“.
Der letzte Graph zeigt die Ergebnisse des Löschtests.

Auch hier gibt es keine Überraschungen. Die Varianten 3-5 führen die Löschung in konstanter Zeit durch.
Interessanterweise zeigen die Varianten 4 und 5, im Gegensatz zu den vorherigen Szenarien, eine merklich schlechtere Leistung als Variante 3. Offensichtlich ist die Zeilenlöschoperation kostenintensiver als die Spaltenlöschoperation, was insgesamt logisch ist.
Die Varianten 1 und 2 zeigen erwartungsgemäß ein lineares Wachstum der Zeit. Dabei ist Variante 2 konstant langsamer als Variante 1 – aufgrund der zusätzlichen Ein- und Ausgabeoperation für die Verwaltung der Spalte count.
Gesamtergebnisse des Experiments:
- Die Varianten 3-5 zeigen eine höhere Effizienz, da sie die Vorteile von HBase nutzen; dabei unterscheidet sich ihre Leistung nur um eine Konstante und ist unabhängig von der Netzgröße.
- Der Unterschied zwischen den Varianten 4 und 5 wurde nicht festgestellt. Das bedeutet jedoch nicht, dass Variante 5 nicht verwendet werden sollte. Es ist durchaus möglich, dass das gewählte Experimentierszenario unter Berücksichtigung der technischen Parameter des Teststands dies nicht zuließ.
- Die Art des Wachstums der benötigten Zeit für die Durchführung von 'Business-Operationen' mit den Daten hat insgesamt die zuvor erhaltenen theoretischen Ergebnisse für alle Varianten bestätigt.
Epilog
Durchgeführte grobe Experimente sollten nicht als absolute Wahrheit angesehen werden. Es gibt viele Faktoren, die nicht berücksichtigt wurden und die Ergebnisse verzerrt haben (insbesondere sind diese Schwankungen bei kleinem Netzwerk sehr gut auf den Grafiken sichtbar). Zum Beispiel die Leistungsfähigkeit von Thrift, das von HappyBase verwendet wird, das Volumen und die Art und Weise, wie die in Python geschriebene Logik implementiert wurde (ich erhebe keinen Anspruch darauf, dass der Code optimal geschrieben wurde und die Möglichkeiten aller Komponenten effektiv genutzt hat), möglicherweise Besonderheiten des HBase-Cachings, die Hintergrundaktivitäten von Windows 10 auf meinem Laptop usw. Insgesamt kann man sagen, dass alle theoretischen Überlegungen experimentell ihre Gültigkeit bewiesen haben. Oder zumindest war es nicht möglich, sie mit einem „Kopf-an-Kopf“-Ansatz zu widerlegen.
Zusammenfassend – Empfehlungen für alle, die gerade erst anfangen, Datenmodelle in HBase zu entwerfen: abstrahieren Sie sich von vorherigen Erfahrungen mit relationalen Datenbanken und beachten Sie die „Gebote“:
- Beim Entwerfen gehen wir von der Aufgabe und den Mustern der Datenmanipulation aus, nicht vom Modell der Fachdomäne.
- Effizienter Zugriff (ohne Full Table Scan) – nur über den Schlüssel.
- Denormalisierung
- Verschiedene Zeilen können unterschiedliche Spalten enthalten
- Dynamische Spaltenzusammensetzung
Quelle: habr.com

