-- Crearea tabelului cu coloană JSONB
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE,
metadata JSONB
);
-- Crearea indexului GIN folosind jsonb_path_ops (mai rapid și mai mic pe disc)
CREATE INDEX idx_users_metadata_path_ops ON users USING gin (metadata jsonb_path_ops);
-- Query-ul corect care folosește indexul GIN (operatorul de incluziune)
SELECT * FROM users
WHERE metadata @> '{"preferences": {"theme": "dark"}}';
-- Query-ul care NU va folosi acest index GIN specific:
-- SELECT * FROM users WHERE metadata->'preferences'->>'theme' = 'dark';Am văzut prea des oameni care folosesc JSONB în Postgres doar pentru că le-a fost lene să facă o migrare calumea. E o capcană în care am picat și eu acum vreo opt ani, la un proiect cu peste 15.000 de utilizatori activi, unde am zis că "lasă, punem setările și profilul într-un JSON că e mai flexibil". Mare greșeală, ne-am chinuit luni de zile cu query-uri lente și blocaje.
De atunci, am învățat că JSONB e o unealtă genială, dar numai dacă știi exact când să o folosești și, mai ales, cum să o indexezi ca să nu îți pui baza de date în cap.
Când are sens JSONB și care e prețul plătit
Regula mea de aur e simplă: folosește coloane normale (text, int, boolean) pentru tot ce înseamnă date structurate, entități de bază și câmpuri pe care faci join-uri frecvente.
JSONB strălucește în două scenarii:
- Atribute dinamice: De exemplu, într-un magazin online unde vinzi și televizoare (care au diagonală, rezoluție) și haine (care au mărime, material). E un coșmar să faci tabele de legătură EAV (Entity-Attribute-Value) pentru asta.
- Payload-uri de la API-uri externe: Când integrezi un Stripe sau un webhook extern și vrei să salvezi exact ce ți-au trimis ei, fără să știi dinainte dacă vor adăuga câmpuri noi luna viitoare.
Dar există un trade-off major. JSONB consumă mult mai mult spațiu pe disc decât coloanele clasice. În plus, orice update pe o proprietate din interiorul JSON-ului forțează Postgres să scrie din nou tot documentul (datorită modului în care funcționează MVCC). Dacă ai update-uri dese pe câmpuri din JSONB, o să ai probleme mari cu disk I/O-ul.
Magia indexării: GIN în loc de B-Tree
Dacă ai o tabelă cu 2 milioane de rânduri și cauți după o cheie din interiorul JSON-ului, Postgres va face un sequential scan de toată frumusețea. Aici intră în scenă indexul GIN (Generalized Inverted Index).
Spre deosebire de un index B-Tree clasic (care e bun pentru valori unice și ordonate), GIN "despachetează" documentul JSONB și creează intrări pentru fiecare cheie și valoare din interior.
Avem două moduri mari de indexare cu GIN:
jsonb_ops(cel default): Indexează absolut tot (chei, valori, sub-obiecte). E flexibil, dar indexul devine uriaș pe disc.jsonb_path_ops: Indexează doar perechile cheie-valoare sub formă de hash. Indexul e mult mai mic (am salvat și 40% din spațiu pe unele proiecte) și căutările sunt mai rapide, dar ești limitat la operatorul de incluziune (@>).
La un proiect recent, un query neindexat pe un JSONB de 1.2 milioane de rânduri dura cam 850ms. După ce am adăugat un index GIN cu jsonb_path_ops, același query a scăzut la 3.4ms. Diferența e de la cer la pământ.
Cum scrii query-ul ca să folosească indexul
Marea capcană aici este operatorul folosit. Dacă scrii un query folosind operatorul săgeată (->> sau ->), Postgres s-ar putea să ignore indexul GIN complet. Cel mai sigur mod de a te asigura că indexul e folosit este să folosești operatorul de incluziune @>. Acest operator verifică dacă JSON-ul din stânga conține structura din dreapta.
Puteți vedea în exemplul de cod cum se creează indexul optimizat și cum se scrie query-ul corect pentru a profita de el.
Voi cum folosiți JSONB? Îl țineți doar pentru loguri și chestii secundare, sau aveți bucăți din core-ul aplicației care se bazează pe el?