PostgreSQL JSONB: NoSQL galimybės SQL bazėje

Kai SQL susitinka su NoSQL pasauliu

Prisimenu, kaip prieš keletą metų diskutavome su kolegomis, ar verta migruoti iš PostgreSQL į MongoDB. Tuomet atrodė, kad dokumentinės bazės – tai ateitis, o tradicinis SQL – tai praeitis. Bet štai PostgreSQL komanda padarė kažką genialaus: vietoj to, kad kovotų su NoSQL banga, jie tiesiog įtraukė JSONB duomenų tipą. Ir tai pakeitė žaidimo taisykles.

JSONB – tai ne tik JSON saugojimas duomenų bazėje. Tai pilnavertė NoSQL funkcionalumo integracija į patikimą SQL aplinką. Galite turėti ir griežtą reliacinę struktūrą, ir lankstų dokumentų saugojimą toje pačioje bazėje. Nereikia mokytis naujos užklausų kalbos, nereikia palaikyti dviejų skirtingų sistemų. Viskas vienoje vietoje.

Kuo JSONB skiriasi nuo paprasto JSON

Pirmą kartą dirbdamas su PostgreSQL JSON palaikymu, padariau klaidą – naudojau paprastą JSON tipą. Atrodė, kad viskas veikia gerai, kol nepradėjau daryti sudėtingesnių užklausų. Tada supratau, kad JSON ir JSONB – tai du visiškai skirtingi dalykai.

Paprastas JSON tipas PostgreSQL saugo duomenis kaip tekstą. Kiekvieną kartą, kai norite ką nors ištraukti ar filtruoti, duomenų bazė turi iš naujo parsinti visą JSON struktūrą. JSONB (B reiškia „binary”) saugo duomenis dekompozuotu, binariu formatu. Tai reiškia, kad duomenys jau yra apdoroti ir optimizuoti užklausoms.

Praktiškai tai reiškia, kad JSONB užklausos gali būti 10-100 kartų greitesnės. Be to, JSONB palaiko indeksavimą – galite sukurti GIN ar GiST indeksus, kurie pagreitina paiešką tūkstančius kartų. Taip, JSONB užima šiek tiek daugiau vietos diske ir įrašymas gali būti truputį lėtesnis, bet praktikoje tai niekada nebuvo problema.

Kada JSONB tikrai praverčia

Yra keletas scenarijų, kur JSONB tiesiog spindi. Pirmiausia – tai dinaminiai atributai. Turite e-komercijos sistemą su produktais? Kiekviena produkto kategorija turi skirtingus parametrus. Telefonui reikia ekrano dydžio, kameros megapikselių, baterijos talpos. Knygai – puslapių skaičiaus, leidyklos, ISBN. Batams – dydžio, spalvos, medžiagos.

Tradiciškai turėtumėte dvi galimybes: sukurti atskirą lentelę atributams (EAV modelis, kuris yra tikras košmaras užklausoms) arba sukurti dešimtis stulpelių, iš kurių dauguma bus NULL. Su JSONB tiesiog turite vieną stulpelį attributes, kuriame saugote bet kokius duomenis:


CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price DECIMAL(10,2),
attributes JSONB
);

INSERT INTO products (name, price, attributes) VALUES
('iPhone 15', 999.99, '{"screen_size": "6.1", "camera_mp": 48, "battery_mah": 3349}'),
('Sapiens', 19.99, '{"pages": 512, "publisher": "Vintage", "isbn": "9780099590088"}');

Kitas puikus panaudojimas – API atsakymų saugojimas. Integruojatės su išorine sistema? Vietoj to, kad bandytumėte normalizuoti visus galimus laukus, tiesiog išsaugokite visą atsakymą JSONB formatu. Vėliau, kai suprasite, kokie duomenys jums tikrai reikalingi, galėsite juos ištraukti.

Dar vienas scenarijus – audito logai ir istorija. Kai reikia saugoti, kas, kada ir ką pakeitė, JSONB leidžia išsaugoti visą objekto būseną be sudėtingų schemos keitimų.

JSONB operatoriai ir funkcijos, kuriuos turite žinoti

PostgreSQL turi įspūdingą arsenalą operatorių darbui su JSONB. Pirmą kartą susidūrus su jais, gali pasirodyti, kad tai kažkoks hieroglifų rinkinys, bet greitai tampa antra prigimtimi.

Operatorius -> grąžina JSON objektą, o ->> grąžina tekstą. Skamba panašiai, bet skirtumas esminis:


SELECT attributes->'screen_size' FROM products; -- Grąžina: "6.1" (JSON)
SELECT attributes->>'screen_size' FROM products; -- Grąžina: 6.1 (TEXT)

Norint pasiekti įdėtus objektus, naudojame kelio operatorių #> ir #>>:


-- Tarkime, turime sudėtingesnę struktūrą
INSERT INTO products (name, attributes) VALUES
('Laptop', '{"specs": {"cpu": {"model": "i7", "cores": 8}, "ram": "16GB"}}');

SELECT attributes#>'{specs,cpu,model}' FROM products; -- Grąžina: "i7"

Operatorius @> patikrina, ar JSONB objektas turi tam tikrą substruktūrą. Tai neįtikėtinai naudinga filtravimui:


-- Rasti visus produktus, kurie turi 48MP kamerą
SELECT * FROM products WHERE attributes @> '{"camera_mp": 48}';

Funkcija jsonb_set() leidžia atnaujinti konkrečius laukus nekeičiant viso objekto:


UPDATE products
SET attributes = jsonb_set(attributes, '{battery_mah}', '3500', true)
WHERE name = 'iPhone 15';

Indeksavimas – kur slypi tikroji galia

Be indeksų JSONB yra tiesiog patogus būdas saugoti duomenis. Su indeksais tai tampa galinga užklausų mašina. GIN (Generalized Inverted Index) indeksas – tai pagrindinis įrankis JSONB optimizavimui.

Paprasčiausias būdas sukurti GIN indeksą:


CREATE INDEX idx_products_attributes ON products USING GIN (attributes);

Toks indeksas pagreitins visas @>, ?, ?&, ?| operatorių užklausas. Bet jei jums reikia ieškoti tik konkrečių kelių, galite sukurti tikslesnius indeksus:


-- Indeksas tik camera_mp laukui
CREATE INDEX idx_camera ON products USING GIN ((attributes->'camera_mp'));

-- Indeksas įdėtam keliui
CREATE INDEX idx_cpu_model ON products USING GIN ((attributes#>'{specs,cpu,model}'));

Realus pavyzdys iš praktikos: turėjau lentelę su 5 milijonais įrašų, kur kiekvienas turėjo JSONB stulpelį su maždaug 20 laukų. Be indekso užklausa, ieškanti konkrečios reikšmės, užtrukdavo 15 sekundžių. Sukūrus GIN indeksą – 30 milisekundžių. Tai ne 2x ar 10x pagerinimas, tai 500x skirtumas.

Kai JSONB nėra geriausias pasirinkimas

Būkime sąžiningi – JSONB nėra sidabrinė kulka. Yra situacijų, kai tradicinė reliacinė struktūra yra geresnis pasirinkimas.

Jei jūsų duomenys turi aiškią, stabilią struktūrą, kuri nesikeičia – naudokite įprastus stulpelius. Pavyzdžiui, vartotojo lentelė su email, name, created_at – tai turėtų būti normalūs stulpeliai, ne JSONB. Kodėl? Pirma, duomenų bazė gali užtikrinti duomenų tipus ir apribojimus. Antra, užklausos bus greitesnės. Trečia, kiti programuotojai supras jūsų schemą be papildomo dokumentavimo.

JSONB taip pat nėra geriausias pasirinkimas, kai reikia daug JOIN operacijų. Jei jūsų JSONB objekte saugote user_id ir nuolat darote JOIN su users lentele, greičiausiai tas user_id turėtų būti atskiras stulpelis su foreign key constraint.

Dar vienas dalykas – agregatinės funkcijos. Jei dažnai skaičiuojate sumas, vidurkius ar kitas statistikas, normalūs stulpeliai su tinkamais indeksais bus efektyvesni. JSONB geriau tinka, kai reikia lankstumo, ne maksimalaus greičio skaičiavimams.

Migravimas iš MongoDB ar kitų NoSQL

Jei svarstote migraciją iš dokumentinės bazės į PostgreSQL su JSONB, turiu gerų žinių – tai paprastesnis procesas, nei galvojate. MongoDB dokumentai beveik tiesiogiai atitinka JSONB struktūrą.

Pagrindinis skirtumas – MongoDB automatiškai sukuria _id lauką, o PostgreSQL to nedaro. Rekomenduoju sukurti atskirą id stulpelį kaip primary key ir išsaugoti originalų MongoDB _id JSONB objekte, jei reikia suderinamumo.

MongoDB užklausų vertimas į PostgreSQL SQL:


// MongoDB
db.products.find({price: {$gt: 100}})

-- PostgreSQL
SELECT * FROM products WHERE (attributes->>'price')::numeric > 100;

// MongoDB
db.products.find({"specs.cpu.cores": {$gte: 8}})

-- PostgreSQL
SELECT * FROM products WHERE (attributes#>>'{specs,cpu,cores}')::integer >= 8;

Vienas iš didžiausių privalumų po migracijos – galite pradėti normalizuoti duomenis palaipsniui. Pradėkite su viskuo JSONB, o vėliau, kai pamatote, kokie laukai naudojami dažniausiai, iškelkite juos į atskirus stulpelius. PostgreSQL leidžia turėti ir reliacinę struktūrą, ir JSONB tame pačiame įraše.

Praktiniai patarimai iš realių projektų

Per kelerius metus dirbant su JSONB, susikroviau nemažai patirties, kuria noriu pasidalinti.

**Visada validuokite JSONB struktūrą aplikacijos lygmenyje.** PostgreSQL leis jums įrašyti bet kokį validų JSON, bet tai nereiškia, kad jis atitiks jūsų verslo logiką. Naudokite schema validation bibliotekas (JSON Schema, Joi, Zod) prieš įrašant duomenis.

**Naudokite CHECK constraints paprastoms validacijoms:**


ALTER TABLE products
ADD CONSTRAINT check_required_fields
CHECK (attributes ? 'price' AND attributes ? 'category');

**Sukurkite view’us sudėtingoms užklausoms.** Jei nuolat ištraukiate tuos pačius JSONB laukus, sukurkite view, kuris tai padaro už jus:


CREATE VIEW products_flat AS
SELECT
id,
name,
attributes->>'category' as category,
(attributes->>'price')::numeric as price,
attributes->>'screen_size' as screen_size
FROM products;

**Stebėkite JSONB stulpelių dydį.** Jei matote, kad JSONB objektai auga iki kelių megabaitų, greičiausiai darote kažką ne taip. Dideli JSONB objektai sulėtina užklausas ir apsunkina indeksavimą. Apsvarstykite duomenų skaidymą į kelias lenteles.

**Naudokite jsonb_pretty() debuginimui:**


SELECT jsonb_pretty(attributes) FROM products LIMIT 1;

Tai išveda gražiai suformatuotą JSON, kurį daug lengviau skaityti nei vieną eilutę.

Ateitis jau čia, tik nevienodai paskirstyta

JSONB PostgreSQL – tai puikus pavyzdys, kaip tradicinės technologijos gali evoliucionuoti ir prisitaikyti prie naujų poreikių. Nebereikia rinktis tarp SQL ir NoSQL – galite turėti abiejų pasaulių privalumus.

Ar tai reiškia, kad MongoDB ar kitos dokumentinės bazės nebereikalingos? Ne, jos vis dar turi savo vietą, ypač kai visas jūsų duomenų modelis yra dokumentinis. Bet daugeliui projektų, kurie turi ir struktūrizuotų, ir nestruktūrizuotų duomenų, PostgreSQL su JSONB siūlo idealų balansą.

Pradėkite mažai – pridėkite JSONB stulpelį vienai ar dviem lentelėms, kur reikia lankstumo. Eksperimentuokite su užklausomis, indeksais, suprasite, kaip tai veikia jūsų kontekste. Ir kas žino, galbūt po metų jūsų architektūra atrodys visiškai kitaip – paprasčiau, lankstesnė ir efektyvesnė. Technologijos keičiasi, bet geros idėjos lieka.

Daugiau

Python Rich: terminal UI biblioteka