Entwicklungsmerkmale des Modells für NoSQL

Einführung

Entwicklungsmerkmale des Modells für NoSQL „Man muss mit aller Kraft rennen, um nur an Ort und Stelle 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 zu halten für die Analysten unseres Unternehmens zum Thema Entwurf von Datenmodellen, denn wenn man lange an Projekten arbeitet (manchmal mehrere Jahre), verliert man das, was in der Welt der IT-Technologien passiert, aus den Augen. In unserem Unternehmen (und so kam es) werden in vielen Projekten keine NoSQL-Datenbanken verwendet (zumindest vorerst), daher habe ich in meinem Vortrag diesen eine besondere Aufmerksamkeit gewidmet, am Beispiel von HBase und versucht, die Präsentation des Materials auf diejenigen auszurichten, die noch nie mit ihnen gearbeitet haben. Insbesondere habe ich einige Besonderheiten des Entwurfs von Datenmodellen anhand eines Beispiels veranschaulicht, das ich vor einigen Jahren gelesen habe. in dem Artikel „Introduction to HBase Schema Design“ von Amandeep Khurana. Während ich die Beispiele durchging, verglich ich verschiedene Lösungsmöglichkeiten für dasselbe Problem, um den Zuhörern die grundlegenden Ideen besser zu vermitteln.

Neulich, „aus Langeweile“, stellte ich mir die Frage (lange Mai-Wochenenden im Quarantänemodus laden dazu besonders ein), inwieweit die theoretischen Ableitungen mit der Praxis übereinstimmen würden? So entstand die Idee zu diesem Artikel. Ein Entwickler, der schon länger mit NoSQL arbeitet, wird möglicherweise nichts Neues daraus schöpfen (und könnte daher gleich die halbe Seite überspringen). Aber für Analysten, die noch nicht intensiv mit NoSQL gearbeitet haben, denke ich, dass sie nützlich sein wird, um grundlegende Vorstellungen über die Besonderheiten des Entwurfs von Datenmodellen für HBase zu erhalten.

Analyse eines Beispiels

Meiner Meinung nach sollte man, bevor man NoSQL-Datenbanken zu verwenden beginnt, gut darüber nachdenken und die Vor- und Nachteile abwägen. Oft lässt sich das Problem wahrscheinlich auch mit herkömmlichen relationalen Datenbanksystemen lösen. Daher sollte man NoSQL nicht ohne triftige Gründe verwenden. Falls dennoch die Entscheidung getroffen wurde, eine NoSQL-Datenbank zu verwenden, muss man berücksichtigen, dass die Ansätze im Design hier etwas anders sind. Besonders einige davon könnten für diejenigen ungewohnt sein, die zuvor nur mit relationalen Datenbanksystemen zu tun hatten (meiner Beobachtung nach). So gehen wir in der „relationalen“ Welt normalerweise von der Modellierung des Anwendungsbereichs aus und führen erst dann bei Bedarf eine Denormalisierung des Modells durch. In NoSQL hingegen müssen wir sofort die voraussichtlichen Nutzungsszenarien für die Daten berücksichtigen und die Daten von Anfang an denormalisieren. Darüber hinaus gibt es eine Reihe weiterer Unterschiede, die weiter unten beschrieben werden.

Betrachten wir die folgende „synthetische“ Aufgabe, mit der wir weiterarbeiten werden:

Es ist notwendig, die Speicherstruktur einer Freundesliste von Benutzern eines bestimmten abstrakten sozialen Netzwerks zu entwerfen. Zur Vereinfachung nehmen wir an, dass alle Beziehungen gerichtet sind (wie bei Instagram und nicht bei LinkedIn). Die Struktur sollte es ermöglichen, effizient:

  • Auf die Frage zu antworten, ob Benutzer A Benutzer B liest (Lesevorlage)
  • Es zu ermöglichen, Beziehungen im Falle einer Anmeldung/Abmeldung von Benutzer A von Benutzer B hinzuzufügen/zu entfernen (Datenänderungsvorlage)

Natürlich gibt es viele Möglichkeiten zur Lösung der Aufgabe. In einer normalen relationalen Datenbank würden wir wahrscheinlich einfach eine Beziehungstabelle erstellen (möglicherweise typisiert, wenn wir beispielsweise eine Benutzergruppe speichern müssen: Familie, Arbeit usw., zu der dieser „Freund“ gehört), und um die Zugriffszeit zu optimieren, würden wir Indizes/Partitionierung hinzufügen. Wahrscheinlich würde die endgültige Tabelle etwa so aussehen:

user_id
friend_id

Vasya
Petja

Vasya
Olya

Hier und im Folgenden werde ich zur Veranschaulichung und besseren Verständlichkeit statt IDs Namen verwenden.

Im Fall von HBase wissen wir, dass:

  • eine effektive Suche, die keinen Full Table Scan verursacht, möglich ist ausschließlich über den Schlüssel
    • Eigentlich ist es deshalb eine schlechte Idee, gewohnte SQL-Abfragen an solche Datenbanken zu schreiben; technisch gesehen können Sie aus Impala SQL-Abfragen mit Joins und weiterer Logik in HBase senden, aber wie effektiv das sein wird ...

Deshalb sind wir gezwungen, die Benutzer-ID als Schlüssel zu verwenden. Der erste Gedanke zum Thema „Wo und wie sollte man die IDs von Freunden speichern?“ könnte die Idee sein, sie in Spalten zu speichern. Diese offensichtlichste und „naive“ Variante würde etwa so aussehen (nennen wir sie Option 1 (Standard), um später darauf zu verweisen):

RowKey
Spalten

Vasya
1: Petja
2: Olya
3: Dasha

Petja
1: Masha
2: Vasja

Hier entspricht jede Zeile einem Benutzer im Netzwerk. Spalten haben die Namen: 1, 2, … — entsprechend der Anzahl der Freunde, und in den Spalten werden die IDs der Freunde gespeichert. Wichtig zu beachten ist, dass jede Zeile eine unterschiedliche Anzahl von Spalten haben wird. Im obigen Beispiel hat eine Zeile drei Spalten (1, 2 und 3), die andere nur zwei (1 und 2) – hier haben wir die beiden Eigenschaften von HBase genutzt, die relationale Datenbanken nicht haben:

  • die Möglichkeit, den Inhalt der Spalten dynamisch zu ändern (Freund hinzufügen -> Spalte hinzufügen, Freund entfernen -> Spalte entfernen)
  • unterschiedliche Zeilen können unterschiedliche Spalteninhalte haben

Überprüfen wir unsere Struktur auf die Anforderungen der Aufgabe:

  • Daten lesen: um zu verstehen, ob Vasja Olya folgt, müssen wir die gesamte Zeile nach dem Schlüssel RowKey = „Vasja“ abfragen und die Werte der Spalten durchgehen, bis wir Olya in ihnen „treffen“. Oder alle Werte aller Spalten durchgehen, Olya nicht „treffen“ und die Antwort False zurückgeben;
  • Datenänderung: Freund hinzufügen: für eine solche Aufgabe müssen wir ebenfalls abfragen die gesamte Zeile nach dem Schlüssel RowKey = „Vasja“, um die Gesamtzahl seiner Freunde zu berechnen. Diese Gesamtzahl an Freunden benötigen wir, um die Nummer der Spalte zu bestimmen, in die die ID des neuen Freundes geschrieben werden soll.
  • Datenänderung: Freund entfernen:
    • Wir müssen abfragen die gesamte Zeile nach dem Schlüssel RowKey = „Vasja“ und die Spalten durchgehen, um diejenige zu finden, in der der zu entfernende Freund gespeichert ist;
    • Danach müssen wir, nach dem Entfernen des Freundes, alle Daten um eine Spalte „verschieben“, um keine „Lücken“ in der Nummerierung zu erhalten.

Lassen Sie uns nun bewerten, wie leistungsfähig die Algorithmen sind, die wir auf der Seite der „vermeintlichen Anwendung“ implementieren müssen, indem wir O-Symbolik verwendenBezeichnen wir die Größe unseres hypothetischen sozialen Netzwerks mit n. Dann kann die maximale Anzahl an Freunden eines Nutzers (n-1) betragen. Auf dieses (-1) können wir für unsere Zwecke später verzichten, da es im Rahmen der O-Notation unerheblich ist.

  • Daten lesen: Es ist notwendig, die gesamte Zeile auszulesen und in der Grenze alle ihre Spalten zu durchsuchen. Daher wird die obere Kostenschätzung ungefähr O(n) betragen.
  • 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:
    • Analog zum Hinzufügen – es ist erforderlich, im Extremfall alle Spalten zu durchlaufen => O(n).
    • Nach dem Löschen der Spalten müssen wir diese "verschieben". Wenn wir dies "direkt" umsetzen, sind im Extremfall bis zu (n-1) Operationen erforderlich. Wir werden jedoch in diesem und dem folgenden praktischen Teil einen anderen Ansatz anwenden, der einen "Pseudo-Verschiebung" mit einer festen Anzahl von Operationen umsetzt - das heißt, es werden konstant Zeit aufgewendet unabhängig von n. Diese konstante Zeit (genauer gesagt, O(2)) kann im Vergleich zu O(n) vernachlässigt werden. Der Ansatz wird im untenstehenden Bild veranschaulicht: Wir kopieren einfach die Daten aus der "letzten" Spalte in die Spalte, aus der die Daten gelöscht werden sollen, und löschen danach die letzte Spalte:
      Entwicklungsmerkmale des Modells für NoSQL

Insgesamt erhalten wir in allen Szenarien eine asymptotische Berechnungskomplexität von O(n).
Sie haben wahrscheinlich bereits bemerkt, dass wir fast immer die gesamte Zeile aus der Datenbank auslesen müssen, und zwar in zwei von drei Fällen nur, um alle Spalten zu durchlaufen und die Gesamtzahl der Freunde zu zählen. Daher können wir als Optimierungsversuch eine Spalte "count" hinzufügen, in der die Gesamtzahl der Freunde jedes Nutzers im Netzwerk gespeichert wird. In diesem Fall müssen wir die gesamte Zeile nicht mehr auslesen, um die Gesamtzahl der Freunde zu zählen, sondern nur eine Spalte "count" lesen. Wichtig ist, dass wir "count" bei Manipulationen mit den Daten aktualisieren. So erhalten wir eine verbesserte Variante 2 (count):

RowKey
Spalten

Vasya
1: Petja
2: Olya
3: Dasha
count: 3

Petja
1: Masha
2: Vasja

count: 2

Im Vergleich zur ersten Variante:

  • Daten lesen: Um die Frage zu beantworten "Liest Vasya Olya?" hat sich nichts geändert => O(n)
  • Datenänderung: Freund hinzufügen: Wir haben das Einfügen eines neuen Freundes vereinfacht, da wir jetzt nicht mehr die gesamte Zeile durchlesen und ihre Spalten durchgehen müssen, sondern nur den Wert der Spalte „count“ abrufen können, um somit sofort die Spaltennummer für das Einfügen des neuen Freundes zu bestimmen. Dies reduziert die Rechenkomplexität auf O(1)
  • Datenänderung: Freund entfernen: Beim Löschen eines Freundes können wir ebenfalls auf diese Spalte zurückgreifen, um die Anzahl der Ein- und Ausgabeoperationen beim „Verschieben“ der Daten um eine Zelle nach links zu reduzieren. Die Notwendigkeit, die Spalten zu durchlaufen, um die zu löschende zu finden, bleibt jedoch bestehen, daher => O(n)
  • Andererseits müssen wir nun bei jedem Update der Daten auch die Spalte „count“ aktualisieren, was jedoch konstante Zeit in Anspruch nimmt, die im Rahmen der O-Notation vernachlässigt werden kann.

Insgesamt erscheint Variante 2 etwas optimierter, es ist jedoch eher eine „Evolution statt Revolution“. Um eine „Revolution“ durchzuführen, benötigen wir Variante 3 (col).
Lassen Sie uns alles „auf den Kopf stellen“: wir benennen die Spalte nach der Benutzer-ID! Was letztendlich in der Spalte gespeichert wird, ist für uns nicht mehr wichtig, es könnte die Zahl 1 sein (allerdings könnte man dort z. B. auch eine Gruppe wie „Familie/Freunde/usw.“ speichern). Dieser Ansatz könnte einen unvorbereiteten „Durchschnittsverbraucher“ überraschen, der bisher keine Erfahrung mit NoSQL-Datenbanken hatte, aber gerade dieser Ansatz ermöglicht es, das Potenzial von HBase in dieser Aufgabe wesentlich effektiver zu nutzen:

RowKey
Spalten

Vasya
Petja: 1
Olja: 1
Dascha: 1

Petja
Mascha: 1
Wassja: 1

Hier erhalten wir gleich mehrere Vorteile. Um diese zu verstehen, analysieren wir die neue Struktur und bewerten die Rechenkomplexität:

  • Daten lesen: Um die Frage zu beantworten, ob Wassja Olja abonniert hat, reicht es aus, eine Spalte „Olja“ zu lesen: Wenn sie vorhanden ist, ist die Antwort Wahr, wenn nicht – Falsch => 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 reicht aus, einfach die Spalte „Freund-ID“ zu löschen => O(1)

Wie wir sehen, ist ein wesentlicher Vorteil dieses Speichermodells, dass wir in allen notwendigen Szenarien nur mit einer einzigen Spalte arbeiten, wodurch wir das Auslesen der gesamten Zeile aus der Datenbank und erst recht das Durchlaufen aller Spalten dieser Zeile vermeiden. An dieser Stelle könnte man aufhören, aber...

Man kann sich auch mit der Frage auseinandersetzen und noch einen Schritt weiter gehen, um die Leistung zu optimieren und die Eingabe-/Ausgabeoperationen beim Zugriff auf die Datenbank zu reduzieren. Was wäre, wenn man die vollständigen Informationen über die Beziehung direkt im Schlüssel der Zeile speichern würde? Das heißt, einen zusammengesetzten Schlüssel wie userID.friendID zu erstellen? In diesem Fall könnte man die Spalten der Zeile sogar ganz ignorieren.Variante 4 (Zeile)):

RowKey
Spalten

Wanja.Petja
Petja: 1

Wanja.Olja
Olja: 1

Wanja.Dascha
Dascha: 1

Petja.Mascha
Mascha: 1

Petja.Wanja
Wassja: 1

Offensichtlich wird die Bewertung aller Szenarien zur Datenmanipulation in dieser Struktur, genauso wie in der vorherigen Variante, O(1) sein. Der Unterschied zu Variante 3 wird sich ausschließlich in der Effizienz der Ein- und Ausgabeoperationen in der Datenbank zeigen.

Und das letzte ‘Schleifchen’. Es ist leicht zu erkennen, dass der Schlüssel der Zeile in Variante 4 variable Länge haben wird, was möglicherweise die Leistung beeinflussen kann (hier denken wir daran, dass HBase Daten als Byte-Set speichert und die Zeilen in den Tabellen nach dem Schlüssel sortiert sind). Außerdem haben wir einen Trenner, der in einigen Szenarien möglicherweise bearbeitet werden muss. Um diesen Einfluss auszuschließen, kann man Hashes von userID und friendID verwenden, und da beide Hashes eine konstante Länge haben, kann man sie einfach ohne Trenner verketten. Dann würden die Daten in der Tabelle so aussehen:Variante 5 (Hash)):

RowKey
Spalten

dc084ef00e94aef49be885f9b01f51c01918fa783851db0dc1f72f83d33a5994
Petja: 1

dc084ef00e94aef49be885f9b01f51c0f06b7714b5ba522c3cf51328b66fe28a
Olja: 1

dc084ef00e94aef49be885f9b01f51c00d2c2e5d69df6b238754f650d56c896a
Dascha: 1

1918fa783851db0dc1f72f83d33a59949ee3309645bd2c0775899fca14f311e1
Mascha: 1

1918fa783851db0dc1f72f83d33a5994dc084ef00e94aef49be885f9b01f51c0
Wassja: 1

Offensichtlich wird die algorithmische Komplexität des Umgangs mit dieser Struktur in den von uns betrachteten Szenarien dieselbe sein wie bei Variante 4 – also O(1).
Zusammenfassend fassen wir alle unsere Bewertungen der Rechenkomplexität in einer Tabelle zusammen:

Freund hinzufügen
Freund überprüfen
Freund entfernen

Option 1 (Standard)
O(n)
O(n)
O(n)

Variante 2 (Zahl)
O(1)
O(n)
O(n)

Variante 3 (Spalte)
O(1)
O(1)
O(1)

Variante 4 (Zeile)
O(1)
O(1)
O(1)

Variante 5 (Hash)
O(1)
O(1)
O(1)

Wie zu sehen ist, scheinen die Varianten 3-5 am vorteilhaftesten und gewährleisten theoretisch die Ausführung aller erforderlichen Datentransaktionsszenarien in konstanter Zeit. In der Aufgabenstellung gibt es kein explizites Erfordernis für die Erstellung einer Liste aller Freunde des Nutzers, aber in der tatsächlichen Projektarbeit wäre es für uns als gute Analysten ratsam, "vorwegzusehen", dass eine solche Aufgabe auftreten könnte und entsprechend vorzusorgen. Daher neige ich zur Variante 3. Es ist jedoch durchaus möglich, dass diese Anfrage im realen Projekt bereits auf andere Weise gelöst wurde, sodass ohne ein allgemeines Verständnis der gesamten Aufgabe keine endgültigen Schlussfolgerungen gezogen werden sollten.

Vorbereitung des Experiments

Die obigen theoretischen Überlegungen möchten wir in der Praxis überprüfen – das war das Ziel des während des langen Wochenendes entstandenen Plans. Dafür müssen wir die Geschwindigkeit unseres "hypothetischen Anwendung" in allen beschriebenen Nutzungsszenarien der Datenbank bewerten und das Wachstum dieser Zeit mit der Größe des sozialen Netzwerks (n) in Beziehung setzen. Der Zielparameter, der uns interessiert und den wir während des Experiments messen werden, ist die Zeit, die unser "hypothetisches Anwendung" für die Durchführung einer "Business-Operation" aufwendet. Unter "Business-Operation" verstehen wir eine der folgenden:

  • Hinzufügen eines neuen Freundes
  • Überprüfen, ob Benutzer A ein Freund von Benutzer B ist
  • Löschen eines Freundes

Somit ergibt sich aus den in der ursprünglichen Aufgabenstellung festgelegten Anforderungen folgendes Szenario für die Überprüfung:

  • Datenaufzeichnung. Eine zufällige Ausgangsnetzwerkgröße von n generieren. Um näher an der "realen Welt" zu sein, ist die Anzahl der Freunde jedes Benutzers ebenfalls eine zufällige Variable. Messen Sie die Zeit, die unser "hypothetisches Anwendung" benötigt, um alle generierten Daten in HBase zu speichern. Teilen Sie dann die gemessene Zeit durch die Gesamtzahl der hinzugefügten Freunde – so erhalten wir die durchschnittliche Zeit für eine "Business-Operation".
  • Daten lesen. Erstelle für jeden Benutzer eine Liste von „Identitäten“, für die überprüft werden muss, ob der Benutzer ihnen folgt oder nicht. Die Länge der Liste entspricht ungefähr der Anzahl der Freunde des Benutzers, wobei für die Hälfte der überprüften Freunde die Antwort „Ja“ und für die andere Hälfte die Antwort „Nein“ lauten sollte. Die Überprüfung erfolgt in einer Reihenfolge, sodass die Antworten „Ja“ und „Nein“ abwechseln (das bedeutet, dass wir in jedem zweiten Fall alle Spalten der Zeile für die Varianten 1 und 2 durchgehen müssen). Die Gesamtprüfungszeit sollte dann durch die Anzahl der überprüften Freunde geteilt werden, um die durchschnittliche Zeit für die Überprüfung eines Subjekts zu erhalten.
  • Daten löschen. Lösche alle Freunde des Benutzers. Die Reihenfolge der Löschung sollte zufällig sein (das heißt, wir „mischen“ die ursprüngliche Liste, die für die Datenaufzeichnung verwendet wurde). Die Gesamtprüfungszeit sollte dann durch die Anzahl der zu löschenden Freunde geteilt werden, um die durchschnittliche Zeit für eine Überprüfung zu erhalten.

Die Szenarien müssen für jedes der 5 Datenmodellvarianten und für unterschiedliche Größen von sozialen Netzwerken durchlaufen werden, um zu sehen, wie die Zeit mit deren Wachstum variiert. Innerhalb einer n-Verbindung im Netzwerk sollten die Liste der Benutzer zur Überprüfung und die Anzahl der Verbindungen natürlich für alle 5 Varianten gleich sein.
Um das 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 – für die Einspeisung
  • das zweite – zur Überprüfung
  • das dritte – zur Löschung

{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 sehen kann, sind alle IDs, die größer als 10.000 im Wörterbuch zur Überprüfung – genau die, die bekanntenmaßen die Antwort False liefern werden. Die Einspeisung, Überprüfung und Löschung von „Freunden“ erfolgt genau in der im Wörterbuch angegebenen Reihenfolge.

Das Experiment wurde auf einem Laptop mit Windows 10 durchgeführt, auf dem in einem Docker-Container eine HBase-Datenbank und in einem anderen Python mit Jupyter Notebook ausgeführt wurde. Dem Docker wurden 2 CPU-Kerne und 2 GB RAM zugewiesen. Alle logischen Abläufe sowie die Emulation der Funktionsweise einer „hypothetischen Anwendung“ und das „Gerüst“ zur Generierung von Testdaten und zur Zeitmessung wurden in Python geschrieben. Für die Arbeit mit HBase wurde die Bibliothek happybase, zur Berechnung von Hashes (MD5) für Variante 5 — hashlib

Unter Berücksichtigung der Rechenleistung des jeweiligen Laptops wurde experimentell der Start für n = 10, 30, …. 170 ausgewählt – als die Gesamtdauer des vollständigen Testzyklus (alle Szenarien für alle Varianten für alle n) noch mehr oder weniger angemessen war und in die Zeit eines Teepausen passte (im Durchschnitt 15 Minuten).

Hier ist zu bemerken, dass wir in diesem Experiment in erster Linie nicht die absoluten Leistungszahlen bewerten. Selbst der relativ Vergleich von zwei verschiedenen Varianten kann nicht ganz korrekt sein. Momentan interessiert uns insbesondere die Art der Zeitänderung in Abhängigkeit von n, da es unter Berücksichtigung der oben angegebenen Konfiguration des „Teststandes“ sehr schwierig ist, zeitliche Schätzungen zu erhalten, die „von Zufalls- und anderen Faktoren befreit“ sind (und eine solche Aufgabe wurde nicht gestellt).

Ergebnis des Experiments

Der erste Test – wie sich die Zeit verändert, die für das Ausfüllen der Freundesliste aufgebracht wird. Ergebnis – im folgenden Diagramm.
Entwicklungsmerkmale des Modells für NoSQL
Die Varianten 3-5 zeigen erwartungsgemäß nahezu konstante Zeiten für die „Geschäftsoperation“, die nicht vom Wachstum der Netzwerkgröße abhängen, sowie keinen bemerkbaren Leistungsunterschied.
Variante 2 zeigt ebenfalls eine konstante, aber etwas schlechtere Leistung, und zwar ungefähr genau das Doppelte im Vergleich zu den Varianten 3-5. Das kann erfreuen, da es mit der Theorie übereinstimmt – bei dieser Variante ist die Anzahl der Ein- und Ausgabeoperationen in/aus HBase gerade doppelt so hoch. Das kann als indirekter Hinweis dienen, dass unser Teststand im Prinzip eine recht gute Genauigkeit bietet.
Variante 1 erweist sich ebenfalls erwartungsgemäß als die langsamste und zeigt ein lineares Wachstum der benötigten Zeit in Abhängigkeit von der Netzwerkgröße beim Hinzufügen von Freunden.
Sehen wir uns nun die Ergebnisse des zweiten Tests an.
Entwicklungsmerkmale des Modells für NoSQL
Die Varianten 3-5 verhalten sich wieder erwartungsgemäß – konstante Zeit, unabhängig von der Größe des Netzwerks. Die Varianten 1 und 2 zeigen ein lineares Wachstum der Zeit mit zunehmender Netzwerkgröße und eine ähnliche Leistung. Variante 2 ist jedoch etwas langsamer – offenbar aufgrund der Notwendigkeit, eine zusätzliche Spalte "count" zu lesen und zu verarbeiten, was bei wachsendem n deutlicher wird. Ich werde mich jedoch mit Schlussfolgerungen zurückhalten, da die Genauigkeit dieses Vergleichs relativ gering ist. Darüber hinaus variieren diese Verhältnisse (welche Variante, 1 oder 2, schneller ist) von Durchlauf zu Durchlauf (wobei der Charakter der Abhängigkeit und "Nase an Nase" erhalten bleibt).

Und das letzte Diagramm – das Ergebnis des Löschtests.

Entwicklungsmerkmale des Modells für NoSQL

Auch hier gibt es wieder keine Überraschungen. Die Varianten 3-5 führen die Löschung in konstanter Zeit durch.
Interessanterweise zeigen die Varianten 4 und 5, anders als in den vorherigen Szenarien, eine merklich geringere Leistung als Variante 3. Offensichtlich ist die Zeilenlöschoperation aufwendiger 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 E/A-Operation zur "Betreuung" der Spalte count.

Allgemeine Schlussfolgerungen des Experiments:

  • Die Varianten 3-5 zeigen eine höhere Effizienz, da sie die Vorteile von HBase nutzen; ihre Leistung unterscheidet sich dabei nur um eine Konstante und hängt nicht von der Größe des Netzwerks ab.
  • Ein Unterschied zwischen den Varianten 4 und 5 wurde nicht festgestellt. Aber das bedeutet nicht, dass Variante 5 nicht verwendet werden sollte. Es ist durchaus möglich, dass das im Experiment verwendete Szenario in Anbetracht der technischen Daten des Teststandes es nicht ermöglicht hat, diesen zu erkennen.
  • Der Charakter des Wachstums der Zeit, die für die Ausführung von "Business-Operationen" mit den Daten erforderlich ist, hat insgesamt die zuvor gewonnenen theoretischen Überlegungen für alle Varianten bestätigt.

Epilog

Die durchgeführten groben Experimente sollten nicht als absolute Wahrheit betrachtet werden. Es gibt viele Faktoren, die nicht berücksichtigt wurden und die Ergebnisse verzerrt haben (insbesondere sind diese Fluktuationen in Grafiken bei kleiner Netzgröße gut sichtbar). Zum Beispiel die Arbeitsgeschwindigkeit von Thrift, das für HappyBase verwendet wird, das Volumen und die Art der Implementierung der Logik, die ich in Python geschrieben habe (ich kann nicht behaupten, dass der Code optimal geschrieben wurde und die Möglichkeiten aller Komponenten effektiv genutzt hat), möglicherweise Besonderheiten der HBase-Caching und die Hintergrundaktivität von Windows 10 auf meinem Laptop usw. Insgesamt kann man sagen, dass alle theoretischen Ableitungen experimentell ihre Gültigkeit gezeigt haben. Oder zumindest ist es nicht gelungen, sie mit einem solchen „Überrumpeln frontal“ zu widerlegen.

Abschließend — Empfehlungen für alle, die gerade erst mit der Modellierung von Daten in HBase beginnen: abstrahieren Sie vom vorherigen Erfahrung mit relationalen Datenbanken und beachten Sie die „Gebote“:

  • Bei der Planung gehen wir von der Aufgabe und den Mustern der Datenmanipulation aus und nicht von einem Fachgebietmodell.
  • Effizienter Zugang (ohne Full Table Scan) – nur über den Schlüssel.
  • Denormalisierung.
  • Verschiedene Zeilen können unterschiedliche Spalten enthalten.
  • Dynamische Zusammensetzung der Spalten.

Quelle: habr.com

60GB SSD 8Gb DDR4