-- Crearea tabelei cu coloana JSONB
CREATE TABLE produse (
id SERIAL PRIMARY KEY,
nume VARCHAR(255) NOT NULL,
specificatii JSONB NOT NULL DEFAULT '{}'::jsonb
);
-- Index GIN optimizat pentru operatorul @> (contains)
CREATE INDEX idx_produse_specificatii_path_ops
ON produse USING gin (specificatii jsonb_path_ops);
-- Query rapid care utilizeaza indexul creat
SELECT id, nume
FROM produse
WHERE specificatii @> '{"culoare": "negru", "stoc": true}';Mulți dezvoltatori folosesc JSONB în Postgres doar pentru că le e lene să facă o migrare de schemă. Am văzut asta la zeci de proiecte și, de fiecare dată, s-a lăsat cu query-uri care rulau în câteva secunde în loc de milisecunde. Astăzi îți arăt când e o idee bună să folosești JSONB și cum pui un index GIN ca să nu îți blochezi baza de date când crește traficul.
Când are sens JSONB și când e doar lene
Am avut cazul acum doi ani la un SaaS de e-commerce cu vreo 12.000 de produse active. Fiecare categorie avea atribute complet diferite: pantofii aveau mărime și material, laptopurile aveau RAM și procesor. Să faci tabele de legătură pentru fiecare atribut (clasicul sistem EAV - Entity-Attribute-Value) e un coșmar de query-uri cu zeci de JOIN-uri. Aici JSONB strălucește. Îți permite să stochezi structuri flexibile fără să complici schema.
Totuși, mulți cad în capcana de a pune totul în JSONB. Am văzut tabele unde inclusiv ID-ul de user sau data creării erau băgate în JSON. E o greșeală uriașă. Pierzi validarea tipurilor de date, pierzi constrângerile de tip Foreign Key și, cel mai important, pierzi din performanță.
Regula mea de aur e simplă: dacă ai nevoie de integritate referențială, dacă faci des JOIN-uri după acel câmp sau dacă valoarea e comună pentru 95% din înregistrări, folosește o coloană normală. Dacă ai date dinamice, setări de utilizator care se schimbă des ca structură sau răspunsuri de la API-uri externe, JSONB e răspunsul.
Indexarea GIN: Secretul vitezei
Dacă interoghezi o coloană JSONB fără index, Postgres va scana toată tabela de la cap la coadă. La 10.000 de rânduri poate nu simți, dar la un milion o să îți moară baza de date. Aici intervine indexul GIN (Generalized Inverted Index).
Există două moduri mari de a crea un index GIN pe JSONB: cel default (jsonb_ops) și cel optimizat (jsonb_path_ops).
Pentru majoritatea cazurilor practice, recomand jsonb_path_ops. De ce? Pentru că este mult mai mic ca dimensiune pe disc și mult mai rapid la căutări de tipul coloana @> '{"cheie": "valoare"}'. Am testat asta pe o tabelă cu 500k de înregistrări și am redus timpul de căutare de la 1.2 secunde la doar 4 milisecunde. Singurul dezavantaj e că nu poți face căutări doar după existența unei chei simple (folosind operatorul ?), dar rar ai nevoie doar de asta fără să te intereseze valoarea.
Trade-off-ul ascuns (Ce nu-ți spune manualul)
Nimic nu e gratis în ingineria software. Indexul GIN este lent la scriere. La fiecare INSERT sau UPDATE, Postgres trebuie să spargă JSON-ul în bucăți și să actualizeze arborele de indecși. Am pățit ca un script de import masiv de date să meargă de trei ori mai încet doar din cauza unui index GIN pe o coloană JSONB mare.
În plus, JSONB ocupă mai mult spațiu pe disc decât datele normalizate, deoarece stochează cheile de fiecare dată, pentru fiecare rând în parte. Dacă ai cheia "temperatura_ambientala_senzori" repetată pe 10 milioane de rânduri, mănânci gigabiți de pomană.
Voi cum gestionați datele semistructurate? Rămâneți pe schema clasică, ultra-normalizată, sau aruncați totul într-un JSONB și lăsați indexul GIN să își facă magia?