Zëvendësimi i EAV me JSONB në PostgreSQL

TL; DR: JSONB mund të thjeshtojë shumë zhvillimin e skemës së bazës së të dhënave pa sakrifikuar performancën e pyetjeve.

Paraqitje

Le të japim një shembull klasik të ndoshta një prej rasteve më të vjetra të përdorimit në botë të një baze të dhënash relacionale (bazë të dhënash): ne kemi një entitet dhe duhet të ruajmë disa veti (atribute) të këtij entiteti. Por jo të gjitha rastet mund të kenë të njëjtin grup vetish dhe mund të shtohen më shumë prona në të ardhmen.

Mënyra më e lehtë për të zgjidhur këtë problem është të krijoni një kolonë në tabelën e bazës së të dhënave për secilën vlerë të pronës dhe thjesht të plotësoni ato që nevojiten për një shembull specifik të njësisë. E shkëlqyeshme! Problemi u zgjidh... derisa tabela juaj të përmbajë miliona regjistrime dhe ju duhet të shtoni një rekord të ri.

Merrni parasysh modelin EAV (Entitet-Atribut-Vlera), ndodh mjaft shpesh. Një tabelë përmban entitete (rekorde), një tabelë tjetër përmban emra pronash (atribute) dhe një tabelë e tretë lidh entitetet me atributet e tyre dhe përmban vlerën e atyre atributeve për entitetin aktual. Kjo ju jep mundësinë që të keni grupe të ndryshme të vetive për objekte të ndryshme, dhe gjithashtu të shtoni vetitë menjëherë pa ndryshuar strukturën e bazës së të dhënave.

Sidoqoftë, nuk do ta shkruaja këtë postim nëse nuk do të kishte disa dobësi në qasjen EVA. Kështu, për shembull, për të marrë një ose më shumë entitete që kanë nga 1 atribut secila, kërkohen 2 bashkime në pyetje: e para është një bashkim me tabelën e atributeve, e dyta është një bashkim me tabelën e vlerave. Nëse një entitet ka 2 atribute, atëherë nevojiten 4 bashkime! Për më tepër, të gjitha atributet zakonisht ruhen si vargje, gjë që rezulton në hedhjen e tipit si për rezultatin ashtu edhe për klauzolën WHERE. Nëse shkruani shumë pyetje, atëherë kjo është mjaft e kotë për sa i përket përdorimit të burimeve.

Pavarësisht nga këto mangësi të dukshme, EAV është përdorur prej kohësh për të zgjidhur këto lloj problemesh. Këto ishin mangësi të pashmangshme dhe thjesht nuk kishte alternativë më të mirë.
Por më pas një "teknologji" e re u shfaq në PostgreSQL...

Duke filluar me PostgreSQL 9.4, lloji i të dhënave JSONB u shtua për të ruajtur të dhënat binare JSON. Megjithëse ruajtja e JSON në këtë format zakonisht kërkon pak më shumë hapësirë ​​dhe kohë sesa JSON me tekst të thjeshtë, kryerja e operacioneve në të është shumë më e shpejtë. JSONB gjithashtu mbështet indeksimin, gjë që i bën pyetjet edhe më të shpejta.

Lloji i të dhënave JSONB na lejon të zëvendësojmë modelin e rëndë EAV duke shtuar vetëm një kolonë JSONB në tabelën tonë të entitetit, duke thjeshtuar shumë dizajnin e bazës së të dhënave. Por shumë argumentojnë se kjo duhet të shoqërohet me ulje të produktivitetit... Prandaj e shkrova këtë artikull.

Krijimi i një baze të dhënash testimi

Për këtë krahasim, unë krijova bazën e të dhënave në një instalim të ri të PostgreSQL 9.5 në ndërtimin prej $80 DigitalOcean Ubuntu 14.04. Pasi vendosa disa parametra në postgresql.conf vrapova этот skript duke përdorur psql. Tabelat e mëposhtme janë krijuar për të paraqitur të dhënat në formën EAV:

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
);

Më poshtë është një tabelë ku do të ruhen të njëjtat të dhëna, por me atribute në një kolonë të tipit JSONB - Pronat.

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

Duket shumë më e thjeshtë, apo jo? Më pas u shtua në tabelat e entiteteve (entitet & entiteti_jsonb) 10 milion regjistrime, dhe në përputhje me rrethanat, tabela u mbush me të njëjtat të dhëna duke përdorur modelin EAV dhe qasjen me një kolonë JSONB - entiteti_jsonb.vetitë. Kështu, ne morëm disa lloje të ndryshme të dhënash midis të gjithë grupit të vetive. Shembull i të dhënave:

{
  id:          1
  name:        "Entity1"
  description: "Test entity no. 1"
  properties:  {
    color:        "red"
    lenght:       120
    width:        3.1882420
    hassomething: true
    country:      "Belgium"
  } 
}

Pra, tani kemi të njëjtat të dhëna për të dy opsionet. Le të fillojmë të krahasojmë implementimet në punë!

Thjeshtoni dizajnin tuaj

U tha më parë se dizajni i bazës së të dhënave ishte thjeshtuar shumë: një tabelë, duke përdorur një kolonë JSONB për pronat, në vend të përdorimit të tre tabelave për EAV. Por si reflektohet kjo në kërkesat? Përditësimi i një vetie të një entiteti duket kështu:

-- 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;

Siç mund ta shihni, kërkesa e fundit nuk duket më e thjeshtë. Për të përditësuar vlerën e një vetie në një objekt JSONB, duhet të përdorim funksionin jsonb_set(), dhe duhet të kalojë vlerën tonë të re si një objekt JSONB. Megjithatë, ne nuk kemi nevojë të dimë ndonjë identifikues paraprakisht. Duke parë shembullin EAV, duhet të dimë si entity_id ashtu edhe entity_attribute_id për të kryer përditësimin. Nëse dëshironi të përditësoni një pronë në një kolonë JSONB bazuar në emrin e objektit, atëherë gjithçka bëhet në një rresht të thjeshtë.

Tani le të zgjedhim entitetin që sapo përditësuam bazuar në ngjyrën e tij të re:

-- 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';

Mendoj se mund të pajtohemi që i dyti është më i shkurtër (pa bashkim!), dhe për këtë arsye më i lexueshëm. JSONB fiton këtu! Ne përdorim operatorin JSON ->> për të marrë ngjyrën si vlerë teksti nga një objekt JSONB. Ekziston gjithashtu një mënyrë e dytë për të arritur të njëjtin rezultat në modelin JSONB duke përdorur operatorin @>:

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

Kjo është pak më e komplikuar: ne kontrollojmë nëse objekti JSON në kolonën e vetive të tij përmban një objekt që është në të djathtë të operatorit @>. Më pak i lexueshëm, më produktiv (shih më poshtë).

Le ta bëjmë përdorimin e JSONB edhe më të lehtë kur ju duhet të zgjidhni disa prona njëherësh. Këtu hyn me të vërtetë qasja JSONB: ne thjesht zgjedhim vetitë si kolona shtesë në grupin tonë të rezultateve pa pasur nevojë për bashkime:

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

Me EAV do t'ju duhen 2 bashkime për çdo pronë që dëshironi të kërkoni. Sipas mendimit tim, pyetjet e mësipërme tregojnë një thjeshtësim të madh në hartimin e bazës së të dhënave. Shihni më shumë shembuj se si të shkruani pyetje JSONB, gjithashtu në kjo postim.
Tani është koha për të folur për performancën.

prodhimtari

Për të krahasuar performancën kam përdorur SHPJEGON ANALIZO në pyetje, për të llogaritur kohën e ekzekutimit. Çdo pyetje u ekzekutua të paktën tre herë, sepse planifikuesi i pyetjes merr më shumë kohë herën e parë. Së pari i drejtova pyetjet pa asnjë indeks. Natyrisht, ky ishte një avantazh i JSONB, pasi bashkimet e kërkuara për EAV nuk mund të përdornin indekse (fushat kryesore të huaja nuk u indeksuan). Pas kësaj krijova një indeks në 2 kolonat e çelësit të huaj të tabelës së vlerës EAV, si dhe një indeks GIN për një kolonë JSONB.

Përditësimi i të dhënave tregoi rezultatet e mëposhtme për sa i përket kohës (në ms). Vini re se shkalla është logaritmike:

Zëvendësimi i EAV me JSONB në PostgreSQL

Ne shohim që JSONB është shumë (> 50000-x) më i shpejtë se EAV nëse nuk përdorni indekse, për arsyen e përmendur më sipër. Kur indeksojmë kolonat me çelësat kryesorë, ndryshimi pothuajse zhduket, por JSONB është ende 1,3 herë më i shpejtë se EAV. Vini re se indeksi në kolonën JSONB nuk ka asnjë efekt këtu pasi ne nuk po përdorim kolonën e vetive në kriteret e vlerësimit.

Për zgjedhjen e të dhënave bazuar në vlerën e pronës, marrim rezultatet e mëposhtme (shkallë normale):

Zëvendësimi i EAV me JSONB në PostgreSQL

Ju mund të vini re se JSONB përsëri funksionon më shpejt se EAV pa indekse, por kur EAV me indekse, ai përsëri funksionon më shpejt se JSONB. Por më pas pashë që kohët për pyetjet JSONB ishin të njëjta, kjo më shtyu në faktin se indekset GIN nuk funksionojnë. Me sa duket, kur përdorni një indeks GIN në një kolonë me veti të mbushura, ai hyn në fuqi vetëm kur përdorni operatorin e përfshirjes @>. E përdora këtë në një test të ri dhe pati një ndikim të madh në kohë: vetëm 0,153 ms! Kjo është 15000 herë më e shpejtë se EAV dhe 25000 herë më e shpejtë se operatori ->>.

Unë mendoj se ishte mjaft i shpejtë!

Madhësia e tabelës së bazës së të dhënave

Le të krahasojmë madhësitë e tabelës për të dyja qasjet. Në psql ne mund të tregojmë madhësinë e të gjitha tabelave dhe indekseve duke përdorur komandën dti+

Zëvendësimi i EAV me JSONB në PostgreSQL

Për qasjen EAV, madhësitë e tabelave janë rreth 3068 MB dhe indeksohen deri në 3427 MB për një total prej 6,43 GB. Qasja JSONB përdor 1817 MB për tabelën dhe 318 MB për indekset, që është 2,08 GB. Rezulton 3 herë më pak! Ky fakt më befasoi pak sepse ne ruajmë emrat e pronave në çdo objekt JSONB.

Por megjithatë, numrat flasin vetë: në EAV ne ruajmë 2 çelësa të huaj me numra të plotë për vlerë atributi, duke rezultuar në 8 bajt të dhëna shtesë. Për më tepër, EAV ruan të gjitha vlerat e pronave si tekst, ndërsa JSONB do të përdorë vlerat numerike dhe boolean brenda vendit ku është e mundur, duke rezultuar në një gjurmë më të vogël.

Rezultatet e

Në përgjithësi, mendoj se ruajtja e vetive të entitetit në formatin JSONB mund ta bëjë dizajnimin dhe mirëmbajtjen e bazës së të dhënave tuaja shumë më të lehtë. Nëse po kryeni shumë pyetje, atëherë mbajtja e gjithçkaje në të njëjtën tabelë me entitetin në të vërtetë do të funksionojë në mënyrë më efikase. Dhe fakti që kjo thjeshton ndërveprimin midis të dhënave është tashmë një plus, por baza e të dhënave që rezulton është 3 herë më e vogël në vëllim.

Gjithashtu, në bazë të testeve të kryera, mund të konstatojmë se humbjet e performancës janë shumë të parëndësishme. Në disa raste, JSONB është edhe më i shpejtë se EAV, duke e bërë atë edhe më të mirë. Megjithatë, ky pikë referimi sigurisht që nuk mbulon të gjitha aspektet (p.sh. subjektet me një numër shumë të madh pronash, një rritje të konsiderueshme në numrin e pronave të të dhënave ekzistuese, ...), kështu që nëse keni ndonjë sugjerim se si t'i përmirësoni ato , ju lutem mos ngurroni të lini në komente!

Burimi: www.habr.com

Shto një koment