EAV durch JSONB in PostgreSQL ersetzen

TL; DR: JSONB kann die Entwicklung von DB-Schemata erheblich vereinfachen, ohne die Abfrageleistung zu beeintrÀchtigen.

EinfĂŒhrung

Ein klassisches Beispiel, wahrscheinlich eine der Ă€ltesten Anwendungen in der Welt der relationalen Datenbanken (Datenbank): Wir haben eine EntitĂ€t und mĂŒssen bestimmte Eigenschaften (Attribute) dieser EntitĂ€t speichern. Aber nicht alle Exemplare können denselben Satz von Eigenschaften haben; zudem können in Zukunft weitere Eigenschaften hinzugefĂŒgt werden.

Der einfachste Weg, dieses Problem zu lösen, besteht darin, eine Spalte in der DB-Tabelle fĂŒr jeden Wert des Attributs zu erstellen und einfach die auszufĂŒllen, die fĂŒr ein bestimmtes Exemplar der EntitĂ€t benötigt werden. Großartig! Problem gelöst... bis deine Tabelle Millionen von DatensĂ€tzen enthĂ€lt und du die Notwendigkeit hast, einen neuen Datensatz hinzuzufĂŒgen.

Betrachten wir das EAV-Muster (Entity-Attribute-Value), das ziemlich hĂ€ufig vorkommt. Eine Tabelle enthĂ€lt EntitĂ€ten (DatensĂ€tze), eine andere Tabelle enthĂ€lt Eigenschaftsnamen (Attribute), und eine dritte Tabelle verbindet EntitĂ€ten mit ihren Attributen und enthĂ€lt die Werte dieser Attribute fĂŒr die aktuelle EntitĂ€t. Dies ermöglicht es dir, verschiedene EigenschaftssĂ€tze fĂŒr unterschiedliche Objekte zu haben und Eigenschaften „on the fly“ hinzuzufĂŒgen, ohne die DB-Strukturen zu Ă€ndern.

Dennoch wĂŒrde ich diese Notiz nicht schreiben, wenn es nicht auch Nachteile bei der Verwendung von EAV gĂ€be. So benötigt man zum Abrufen einer oder mehrerer EntitĂ€ten, die jeweils 1 Attribut haben, 2 Joins in der Abfrage: der erste – der Join mit der Tabelle der Attribute, der zweite – der Join mit der Tabelle der Werte. Haben die EntitĂ€ten 2 Attribute, sind es bereits 4 Joins! DarĂŒber hinaus werden alle Attribute normalerweise als Strings gespeichert, was zu Typumwandlungen sowohl fĂŒr das Ergebnis als auch fĂŒr die WHERE-Bedingung fĂŒhrt. Wenn du viele Abfragen schreibst, ist das hinsichtlich des Ressourcenverbrauchs recht verschwenderisch.

Trotz dieser offensichtlichen Nachteile wird EAV schon lange zur Lösung solcher Probleme eingesetzt. Es waren unvermeidbare Nachteile, und es gab einfach keine bessere Alternative.
Doch dann kam eine neue „Technologie“ in PostgreSQL


Seit PostgreSQL 9.4 wurde der Datentyp JSONB hinzugefĂŒgt, um binĂ€re JSON-Daten zu speichern. Obwohl die Speicherung von JSON in diesem Format in der Regel etwas mehr Platz und Zeit benötigt als einfacher Text-JSON, sind die Operationen damit viel schneller. Außerdem unterstĂŒtzt JSONB das Indizieren, was Abfragen noch schneller macht.

Der Datentyp JSONB ermöglicht es uns, das umstĂ€ndliche EAV-Muster zu ersetzen, indem wir lediglich eine JSONB-Spalte zu unserer EntitĂ€tstabelle hinzufĂŒgen, was das Datenbankdesign erheblich vereinfacht. Viele behaupten jedoch, dass dies mit einem RĂŒckgang der Leistung einhergehen sollte
 Aus diesem Grund ist dieser Artikel entstanden.

Einrichten einer Testdatenbank

FĂŒr diesen Vergleich habe ich eine Datenbank auf einer neuen Installation von PostgreSQL 9.5 auf einem 80-Dollar-Bauteil erstellt DigitalOcean Ubuntu 14.04. Nach der Anpassung einiger Parameter in postgresql.conf habe ich diesen das Skript mit psql ausgefĂŒhrt. FĂŒr die Darstellung der Daten im EAV wurden die folgenden Tabellen erstellt:

CREATE TABLE entity ( 
  id           SERIAL PRIMARY KEY, 
  name         TEXT, 
  description  TEXT
);
CREATE TABLE entity_attribute (
  id          SERIAL PRIMARY KEY, 
  name        TEXT
);
CREATE TABLE entity_attribute_value (
  id                  SERIAL PRIMARY KEY, 
  entity_id           INT    REFERENCES entity(id), 
  entity_attribute_id INT    REFERENCES entity_attribute(id), 
  value               TEXT
);

Unten ist die Tabelle dargestellt, wo die gleichen Daten gespeichert werden, aber mit Attributen in der JSONB-Spalte – properties.

CREATE TABLE entity_jsonb (
  id          SERIAL PRIMARY KEY, 
  name        TEXT, 
  description TEXT,
  properties  JSONB
);

Sieht viel einfacher aus, oder? Dann wurden 10 Millionen DatensĂ€tze zu den EntitĂ€tstabellen (entity & entity_jsonb) hinzugefĂŒgt, und entsprechend wurden die Tabellen, in denen das EAV-Muster und der Ansatz mit der JSONB-Spalte verwendet wird, mit denselben Daten gefĂŒllt – entity_jsonb.properties. Damit erhielten wir mehrere verschiedene Datentypen unter all den Eigenschaften. Beispiel fĂŒr Daten:

{
  id:          1
  name:        "Entity1"
  description: "Test-Entity Nr. 1"
  properties:  {
    color:        "rot"
    length:       120
    width:        3.1882420
    hasSomething: true
    country:      "Belgien"
  } 
}

Jetzt haben wir also die gleichen Daten fĂŒr zwei Varianten. Lassen Sie uns beginnen, die Implementierungen in der Praxis zu vergleichen!

Vereinfachung des Designs

Es wurde bereits erwĂ€hnt, dass das Datenbankdesign erheblich vereinfacht wurde: eine Tabelle, durch die Verwendung der JSONB-Spalte fĂŒr Eigenschaften, anstelle von drei Tabellen fĂŒr EAV. Aber wie wirkt sich das auf die Abfragen aus? Die Aktualisierung einer Eigenschaft einer EntitĂ€t sieht folgendermaßen aus:

-- EAV
UPDATE entity_attribute_value 
SET value = 'blau' 
WHERE entity_attribute_id = 1 
  AND entity_id = 120;

-- JSONB
UPDATE entity_jsonb 
SET properties = jsonb_set(properties, '{"color"}', '"blau"') 
WHERE id = 120;

Wie wir sehen, sieht die letzte Abfrage nicht einfacher aus. Um den Wert einer Eigenschaft im JSONB-Objekt zu aktualisieren, mĂŒssen wir die Funktion jsonb_set(), und wir mĂŒssen unseren neuen Wert als JSONB-Objekt ĂŒbergeben. Allerdings mĂŒssen wir keinen Identifier im Voraus kennen. Schaut man sich das Beispiel mit EAV an, mĂŒssen wir sowohl entity_id als auch entity_attribute_id kennen, um das Update durchzufĂŒhren. Wenn Sie eine Eigenschaft in einer JSONB-Spalte basierend auf dem Objektname aktualisieren möchten, geschieht das alles in einer einfachen Zeile.

Jetzt lassen Sie uns die EntitÀt auswÀhlen, die wir gerade basierend auf ihrer neuen Farbe aktualisiert haben:

-- EAV
SELECT e.name 
FROM entity e 
  INNER JOIN entity_attribute_value eav ON e.id = eav.entity_id
  INNER JOIN entity_attribute ea ON eav.entity_attribute_id = ea.id
WHERE ea.name = 'farbe' AND eav.value = 'blau';

-- JSONB
SELECT name 
FROM entity_jsonb 
WHERE properties ->> 'color' = 'blau';

Ich denke, wir können uns einigen, dass die zweite Abfrage kĂŒrzer ist (ohne Join!) und somit lesbarer. Hier gewinnt JSONB! Wir verwenden den JSON-Operator ->>, um die Farbe als Textwert aus dem JSONB-Objekt zu erhalten. Es gibt auch einen zweiten Weg, um dasselbe Ergebnis im JSONB-Modell mit dem Operator @> zu erreichen:

-- JSONB 
SELECT name 
FROM entity_jsonb 
WHERE properties @> '{"color": "blau"}';

Das ist etwas komplizierter: Wir prĂŒfen, ob das JSON-Objekt in der Spalte Eigenschaften das Objekt enthĂ€lt, das sich rechts vom Operator @> befindet. Weniger lesbar, aber leistungsstĂ€rker (siehe weiter unten).

Lassen Sie uns die Verwendung von JSONB noch weiter vereinfachen, wenn Sie mehrere Eigenschaften gleichzeitig auswĂ€hlen mĂŒssen. Hier passt der JSONB-Ansatz wirklich gut: Wir wĂ€hlen einfach die Eigenschaften als zusĂ€tzliche Spalten in unserem Ergebnisset aus, ohne Joins vornehmen zu mĂŒssen:

-- JSONB 
SELECT name
  , properties ->> 'color'
  , properties ->> 'country'
FROM entity_jsonb 
WHERE id = 120;

Mit EAV benötigen Sie 2 Joins fĂŒr jede Eigenschaft, die Sie abfragen möchten. Meiner Meinung nach zeigen die oben genannten Abfragen eine erhebliche Vereinfachung im Datenbankdesign. Mehr Beispiele, wie man Abfragen an JSONB schreibt, finden Sie möglicherweise auch in das dem Beitrag.
Jetzt ist es an der Zeit, ĂŒber die Leistung zu sprechen.

Leistung

Um die Leistung zu vergleichen, habe ich EXPLAIN ANALYZE In den Abfragen zur Berechnung der AusfĂŒhrungszeit. Jede Abfrage wurde mindestens dreimal ausgefĂŒhrt, da der Abfrageplaner beim ersten Mal mehr Zeit benötigt. Zuerst habe ich die Abfragen ohne jegliche Indizes ausgefĂŒhrt. Offensichtlich hatte dies einen Vorteil fĂŒr JSONB, da die Joins, die fĂŒr EAV erforderlich sind, keine Indizes verwenden konnten (die FremdschlĂŒssel-Felder waren nicht indiziert). Danach habe ich einen Index fĂŒr die 2 externen SchlĂŒsselspalten der EAV-Wertetabelle sowie einen Index erstellt. GIN fĂŒr die JSONB-Spalte.

Die Datenaktualisierungen zeigten die folgenden Ergebnisse hinsichtlich der Zeit (in ms). Bitte beachten Sie, dass die Skala logarithmisch ist:

EAV durch JSONB in PostgreSQL ersetzen

Wir sehen, dass JSONB viel schneller (> 50000-x) als EAV ist, wenn keine Indizes verwendet werden, aus den oben genannten GrĂŒnden. Wenn wir die Spalten mit PrimĂ€rschlĂŒsseln indizieren, verschwindet der Unterschied fast, aber JSONB ist immer noch 1,3-mal schneller als EAV. Beachten Sie, dass der Index in der JSONB-Spalte hier keinen Einfluss hat, da wir die Eigenschaften-Spalte in den Bewertungskriterien nicht verwenden.

FĂŒr die Auswahl von Daten basierend auf dem Wert von Eigenschaften erhalten wir die folgenden Ergebnisse (normale Skala):

EAV durch JSONB in PostgreSQL ersetzen

Es ist zu erkennen, dass JSONB erneut schneller arbeitet als EAV ohne Indizes, aber wenn EAV mit Indizes arbeitet, ist es dennoch schneller als JSONB. Dann habe ich gesehen, dass die Zeit fĂŒr JSONB-Abfragen gleich war, was mich auf die Tatsache aufmerksam machte, dass der GIN-Index nicht greift. Offenbar funktioniert der GIN-Index fĂŒr Spalten mit ausgefĂŒllten Eigenschaften nur bei der Verwendung des Einschlussoperators @>. Ich nutzte dies in einem neuen Test, was einen enormen Einfluss auf die Zeit hatte: nur 0,153 ms! Das ist 15000-mal schneller als EAV und 25000-mal schneller als der Operator ->>.

Ich denke, das war schnell genug!

GrĂ¶ĂŸe der Datenbanktabellen

Lassen Sie uns die GrĂ¶ĂŸen der Tabellen bei beiden AnsĂ€tzen vergleichen. In psql können wir die GrĂ¶ĂŸe aller Tabellen und Indizes mit dem Befehl dti+

EAV durch JSONB in PostgreSQL ersetzen

FĂŒr den EAV-Ansatz betragen die TabellengrĂ¶ĂŸen etwa 3068 MB, und die Indizes bis zu 3427 MB, was insgesamt 6,43 GB ergibt. Bei Verwendung des JSONB-Ansatzes werden 1817 MB fĂŒr die Tabelle und 318 MB fĂŒr die Indizes verwendet, was 2,08 GB ergibt. Das ist dreimal weniger! Diese Tatsache hat mich etwas ĂŒberrascht, da wir die Eigenschaften in jedem JSONB-Objekt speichern.

Aber die Zahlen sprechen fĂŒr sich selbst: In EAV speichern wir 2 Ganzzahl-FremdschlĂŒssel auf Attributwerte, was zu 8 zusĂ€tzlichen Bytes an Daten fĂŒhrt. DarĂŒber hinaus werden in EAV alle Eigenschaftswerte als Text gespeichert, wĂ€hrend JSONB numerische und boolesche Werte verwendet, wo dies möglich ist, was zu einem geringeren Volumen fĂŒhrt.

Ergebnisse

Insgesamt denke ich, dass die Speicherung von EntitĂ€tseigenschaften im JSONB-Format das Design und die Wartung Ihrer Datenbank erheblich vereinfachen kann. Wenn Sie viele Abfragen durchfĂŒhren, wird alles, was in einer Tabelle mit der EntitĂ€t gespeichert ist, tatsĂ€chlich effizienter funktionieren. Und die Tatsache, dass es die Interaktion zwischen den Daten vereinfacht, ist bereits ein Vorteil, aber auch die resultierende Datenbank hat ein Volumen, das dreimal kleiner ist.

Auch aus den durchgefĂŒhrten Tests lĂ€sst sich zusammenfassen, dass die Leistungseinbußen sehr geringfĂŒgig sind. In einigen FĂ€llen funktioniert JSONB sogar schneller als EAV, was es noch besser macht. Allerdings deckt dieser Benchmark-Test natĂŒrlich nicht alle Aspekte ab (zum Beispiel EntitĂ€ten mit einer sehr großen Anzahl von Eigenschaften, signifikantem Anstieg der Eigenschaften vorhandener Daten,
), daher, wenn Sie VorschlĂ€ge haben, wie man sie verbessern kann, zögern Sie bitte nicht, sie in den Kommentaren zu hinterlassen!

Quelle: habr.com

60GB SSD 8Gb DDR4