-- 1. Index GIN clasic (atentie: e destul de mare ca dimensiune)
CREATE INDEX idx_produse_atribute_gin ON produse USING gin (atribute);
-- 2. Varianta optimizata cu path_ops (mai mic, query-uri mai rapide de tipul @>)
CREATE INDEX idx_produse_atribute_path_ops ON produse USING gin (atribute jsonb_path_ops);
-- 3. Index functional (B-Tree) daca interoghezi des doar o cheie specifica
CREATE INDEX idx_produse_brand ON produse ((atribute ->> 'brand'));Am văzut prea des două extreme în producție: fie normalizare până la demență cu zeci de tabele de legătură, fie lene totală unde totul e aruncat într-un singur câmp JSONB. Am trecut prin ambele scenarii, iar la un proiect cu peste 15.000 de produse și atribute dinamice, JSONB ne-a salvat de la un coșmar arhitectural.
Când are sens JSONB și când e o greșeală masivă
Regula mea de aur e simplă: dacă ai nevoie de constrângeri de integritate (foreign keys), tipuri stricte de date sau faci JOIN-uri frecvente pe acele valori, folosește coloane clasice. Nu pune user_id sau status în JSONB doar pentru că ești prea leneș să rulezi o migrare.
În schimb, JSONB strălucește când ai structuri de date polimorfice sau atribute dinamice. Gândește-te la specificațiile unui magazin online: un laptop are „RAM” și „Procesor”, dar o pereche de pantofi are „Mărime” și „Material”. Dacă ai face tabele clasice EAV (Entity-Attribute-Value) pentru asta, query-urile tale ar arăta ca o caracatiță de JOIN-uri care îți îngenunchează baza de date. Cu JSONB, pui totul într-o singură coloană numită atribute și ai rezolvat problema elegant.
Trade-off-ul despre care nu vorbește nimeni: Disk I/O și UPDATE-uri
JSONB-ul nu vine pe degeaba. Citirea e rapidă pentru că datele sunt stocate într-un format binar parsat, dar scrierea doare. Spre deosebire de o coloană normală, la fiecare UPDATE pe o cheie din interiorul JSONB-ului, Postgres trebuie să rescrie întregul document JSONB în spate.
Am pățit asta la un sistem de tracking unde salvam starea unui job în JSONB și o actualizam la fiecare 5 secunde. În doar o săptămână, tabelul a crescut la 40GB din cauza fenomenului de dead tuples (MVCC în Postgres creează o copie nouă a rândului la fiecare update). Am mutat starea într-o coloană VARCHAR normală și baza de date a respirat ușurată, reducând dimensiunea pe disc cu 70%.
GIN: Cum faci căutările instantanee
Dacă ai 100.000 de rânduri și cauți în JSONB fără index, Postgres va face seq scan. Adică va citi tot tabelul de pe disc, ceea ce e groaznic de lent. Pentru asta avem indexul GIN (Generalized Inverted Index).
Există două moduri mari de a folosi GIN:
- GIN clasic (
jsonb_ops): Indexează toate cheile, valorile și sub-obiectele. E bun dacă nu știi dinainte ce vei căuta, dar indexul devine uriaș. - GIN optimizat (
jsonb_path_ops): Indexează doar perechile cheie-valoare sub formă de hash. Ocupă cu vreo 30% mai puțin spațiu pe disc și query-urile de căutare exactă sunt mult mai rapide.
Dacă știi că vei căuta mereu după o singură cheie specifică, de exemplu după brand, nici măcar nu ai nevoie de GIN pe tot JSON-ul. Poți crea un index B-Tree clasic direct pe acea cheie extrasă. E mult mai mic și mai rapid de întreținut la inserții.
Voi cum abordați asta în proiectele voastre? Mergeți pe schema strictă până în pânzele albe sau mai trișați cu JSONB când clienții cer câmpuri custom peste noapte?