EAV durch JSONB in PostgreSQL ersetzen

TL; DR: JSONB kann die Entwicklung von Datenbankschemata erheblich erleichtern, ohne die Abfrageleistung zu beeintrÀchtigen.

EinfĂŒhrung

Betrachten wir ein klassisches Beispiel, wahrscheinlich eine der Ă€ltesten Anwendungen in der Welt der relationalen Datenbanken: Wir haben eine EntitĂ€t und mĂŒssen bestimmte Eigenschaften (Attribute) dieser EntitĂ€t speichern. Doch nicht alle Instanzen können ĂŒber denselben Satz von Eigenschaften verfĂŒgen, und zukĂŒnftig könnte es notwendig sein, weitere Eigenschaften hinzuzufĂŒgen.

Der einfachste Weg, dieses Problem zu lösen, besteht darin, fĂŒr jede Eigenschaft einen Spalten in der Datenbanktabelle zu erstellen und nur die fĂŒr eine bestimmte Instanz der EntitĂ€t benötigten Werte auszufĂŒllen. Das klingt gut! Problem gelöst
 bis Ihre Tabelle Millionen von DatensĂ€tzen enthĂ€lt und Sie eine neue Datensatz hinzufĂŒgen mĂŒssen.

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

Dennoch wĂŒrde ich diese Anmerkung nicht schreiben, wenn es keine Nachteile beim Ansatz mit EVA gĂ€be. Um beispielsweise eine oder mehrere EntitĂ€ten zu erhalten, die jeweils 1 Attribut haben, sind 2 Joins im Abfragesyntax erforderlich: der erste – der Join mit der Attributtabelle, der zweite – der Join mit der Wertetabelle. Wenn die EntitĂ€t 2 Attribute hat, sind bereits 4 Joins nötig! Außerdem 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 Sie viele Abfragen schreiben, ist das aus Sicht der Ressourcennutzung ziemlich verschwenderisch.

Trotz dieser offensichtlichen Nachteile wird EAV bereits seit langem zur Lösung solcher Probleme eingesetzt. Diese Nachteile waren unvermeidlich, und es gab einfach keine bessere Alternative.
Doch dann tauchte in PostgreSQL eine neue 'Technologie' auf...

Mit PostgreSQL 9.4 wurde der Datentyp JSONB zum Speichern von binĂ€ren JSON-Daten eingefĂŒhrt. Obwohl das Speichern von JSON in diesem Format normalerweise etwas mehr Platz und Zeit benötigt als einfaches Text-JSON, erfolgen die Operationen damit deutlich schneller. Außerdem unterstĂŒtzt JSONB die Indizierung, was die Abfragen noch schneller macht.

Der Datentyp JSONB ermöglicht es uns, das umfangreiche 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 Leistungseinbruch einhergehen sollte... Aus diesem Grund entstand dieser Artikel.

Einrichtung der Testdatenbank

FĂŒr diesen Vergleich habe ich eine Datenbank auf einer frischen PostgreSQL 9.5 Installation auf einem 80-Dollar-Setup erstellt. DigitalOcean Ubuntu 14.04. Nach der Anpassung einiger Parameter in der postgresql.conf habe ich sie gestartet. diese Ein Skript mit psql. Um die Daten im EAV-Format darzustellen, wurden folgende 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 eine Tabelle dargestellt, in der dieselben Daten gespeichert werden, jedoch mit Attributen in einer JSONB-Spalte – properties.

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

Sieht viel einfacher aus, oder? Dann wurden in die EntitĂ€tstabellen (entity & entity_jsonb) 10 Millionen DatensĂ€tze eingefĂŒgt, und entsprechend wurden dieselben Daten in den Tabellen, die das EAV-Muster und den JSONB-Spaltenansatz verwenden, vervollstĂ€ndigt – entity_jsonb.properties. Somit haben wir verschiedene Datentypen unter der gesamten Eigenschaftensammlung erhalten. Beispielhafte Daten:

{
  id:          1
  name:        "Entity1"
  description: "TestentitÀt Nr. 1"
  properties:  {
    color:        "rot"
    length:       120
    width:        3.1882420
    hasSomething: true
    country:      "Belgien"
  } 
}

So, jetzt haben wir dieselben Daten fĂŒr zwei Varianten. Lassen Sie uns die Implementierungen im Betrieb vergleichen!

Vereinfachung des Designs

Bereits erwĂ€hnt wurde, dass das Design der Datenbank erheblich vereinfacht wurde: eine Tabelle, die durch die Verwendung einer JSONB-Spalte fĂŒr Eigenschaften ersetzt wird, anstatt drei Tabellen fĂŒr EAV zu nutzen. Aber wie spiegelt sich das in den Abfragen wider?

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

-- JSONB
UPDATE entity_jsonb 
SET properties = jsonb_set(properties, '{"color"}', '"blue"') 
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(), verwenden und unser neues Wert als JSONB-Objekt ĂŒbergeben. Dennoch mĂŒssen wir keinen Identifikator im Voraus kennen. Im Beispiel mit EAV mussten wir sowohl entity_id als auch entity_attribute_id wissen, um das Update durchzufĂŒhren. Wenn Sie eine Eigenschaft in der JSONB-Spalte basierend auf dem Objektname aktualisieren möchten, geschieht das alles mit einer einfachen Zeile.

Lassen Sie uns nun die EntitÀt auswÀhlen, die wir gerade mit der Bedingung 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 = 'color' AND eav.value = 'blue';

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

Ich denke, wir können uns darauf einigen, dass die zweite Option 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 abzurufen. Es gibt auch eine zweite Methode, um dasselbe Ergebnis im JSONB-Modell mit dem Operator @> zu erreichen:

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

Es ist etwas komplizierter: Wir ĂŒberprĂŒfen, ob das JSON-Objekt in der Spalte properties das Objekt rechts vom @> Operator enthĂ€lt. Weniger lesbar, aber leistungsfĂ€higer (siehe weiter unten).

Lassen Sie uns die Verwendung von JSONB weiter vereinfachen, wenn Sie mehrere Eigenschaften gleichzeitig auswĂ€hlen mĂŒssen. Hier spielt der JSONB-Ansatz wirklich seine StĂ€rken aus: Wir wĂ€hlen einfach die Eigenschaften als zusĂ€tzliche Spalten in unserem Ergebnis-Set aus, ohne dass Joins erforderlich sind:

-- 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 aufgefĂŒhrten Abfragen eine erhebliche Vereinfachung im Datenbankdesign. Weitere Beispiele, wie man Abfragen fĂŒr JSONB schreibt, finden Sie auch in diesem dem Beitrag.
Nun ist es an der Zeit, ĂŒber die Leistung zu sprechen.

Leistung

Um die Leistung zu vergleichen, habe ich EXPLAIN ANALYZE in den Abfragen verwendet, um die AusfĂŒhrungszeit zu messen. Jede Abfrage wurde mindestens drei Mal ausgefĂŒhrt, da der Abfrageplaner beim ersten Mal mehr Zeit benötigt. ZunĂ€chst fĂŒhrte ich die Abfragen ohne Indizes aus. Offensichtlich war dies ein Vorteil fĂŒr JSONB, da die Joins, die fĂŒr EAV erforderlich sind, keine Indizes nutzen konnten (die Felder der FremdschlĂŒssel waren nicht indiziert). Danach erstellte ich einen Index fĂŒr die 2 Spalten der FremdschlĂŒssel in der EAV-Wertetabelle sowie einen Index GIN fĂŒr die JSONB-Spalte.

Datenaktualisierungen zeigten folgende Ergebnisse in Bezug auf die Zeit (in ms). Beachten Sie, dass die Skala logarithmisch ist:

EAV durch JSONB in PostgreSQL ersetzen

Wir sehen, dass JSONB deutlich schneller ist (> 50.000 Mal) als EAV, wenn keine Indizes verwendet werden, aus dem oben genannten Grund. Wenn wir die Spalten mit PrimĂ€rschlĂŒsseln indizieren, verschwindet der Unterschied nahezu, 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 Eigenschaftsspalte nicht in den Bewertungsbedingungen verwenden.

FĂŒr die Auswahl von Daten basierend auf dem Wert einer Eigenschaft erhalten wir folgende Ergebnisse (ĂŒblicher Maßstab):

EAV durch JSONB in PostgreSQL ersetzen

Es ist zu bemerken, dass JSONB erneut schneller funktioniert als EAV ohne Indizes. Wenn EAV jedoch Indizes hat, arbeitet es immer noch schneller als JSONB. SpĂ€ter bemerkte ich, dass die Zeit fĂŒr JSONB-Abfragen gleich blieb, was mich zu dem Schluss fĂŒhrte, dass der GIN-Index nicht ausgelöst wurde. Anscheinend wirkt der GIN-Index bei einer Spalte mit ausgefĂŒllten Eigenschaften nur, wenn der Einschlussoperator @> verwendet wird. Ich habe dies in einem neuen Test verwendet, der enorme Auswirkungen auf die Zeit hatte: nur 0,153 ms! Das ist 15.000 Mal schneller als EAV und 25.000 Mal schneller als der Operator ->>.

Ich denke, das war schnell genug!

GrĂ¶ĂŸe der DB-Tabellen

Lassen Sie uns die TabellengrĂ¶ĂŸen bei beiden AnsĂ€tzen vergleichen. In psql können wir die GrĂ¶ĂŸe aller Tabellen und Indizes mit dem Befehl anzeigen 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 entspricht. Das ist dreimal weniger! Diese Tatsache hat mich etwas ĂŒberrascht, da wir die Eigenschaftsnamen in jedem JSONB-Objekt speichern.

Aber die Zahlen sprechen fĂŒr sich: Im EAV speichern wir 2 ganzzahlige FremdschlĂŒssel fĂŒr den Attributwert, was zu 8 Byte zusĂ€tzlichen Daten fĂŒhrt. Außerdem werden im EAV alle Werte als Text gespeichert, wĂ€hrend JSONB wo möglich numerische und boolesche Werte verwendet, was zu einem geringeren Volumen fĂŒhrt.

Ergebnisse

Insgesamt denke ich, dass die Beibehaltung der Eigenschaften von EntitĂ€ten im JSONB-Format das Design und die Wartung Ihrer Datenbank erheblich vereinfachen kann. Wenn Sie viele Abfragen durchfĂŒhren, funktioniert alles, was in einer Tabelle mit der EntitĂ€t gespeichert ist, tatsĂ€chlich effizienter. DarĂŒber hinaus vereinfacht es die Interaktion zwischen den Daten, was bereits einen Vorteil darstellt, und die resultierende Datenbank ist um das Drei-fache kleiner.

Auch aus den durchgefĂŒhrten Tests lĂ€sst sich zusammenfassen, dass die Leistungseinbußen Ă€ußerst gering sind. In einigen FĂ€llen arbeitet JSONB sogar schneller als EAV, was es noch vorteilhafter macht. Dieser Benchmark-Test deckt jedoch natĂŒrlich nicht alle Aspekte ab (z.B. EntitĂ€ten mit einer sehr großen Anzahl von Eigenschaften, signifikante Zunahme der Anzahl der Eigenschaften bestehender Daten,...), daher, wenn Sie VorschlĂ€ge zur Verbesserung haben, zögern Sie bitte nicht, diese in den Kommentaren zu hinterlassen!

Quelle: habr.com

Erwerben Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster