CREATE TABLE user_events (
id SERIAL PRIMARY KEY,
event_name VARCHAR(100),
metadata JSONB
);
-- Creăm un index GIN optimizat folosind jsonb_path_ops
CREATE INDEX idx_user_events_metadata_path ON user_events USING gin (metadata jsonb_path_ops);
-- Exemplu de căutare rapidă care folosește indexul
SELECT * FROM user_events
WHERE metadata @> '{"browser": "Chrome", "page": "home"}';Salutare! Văd tot mai des în codul pe care îl recenzez o tendință periculoasă: transformarea Postgres-ului în MongoDB prin abuzul de JSONB. Am avut un proiect acum doi ani, un SaaS de e-commerce cu vreo 80.000 de produse, unde echipa anterioară pusese aproape toate atributele produselor într-un singur câmp JSONB pentru că "e flexibil". Evident, când baza de date a trecut de 10GB, interogările au început să se târâie apocaliptic.
JSONB este o unealtă genială, dar doar dacă înțelegi când să o folosești și cum să o ajuți cu indecși potriviți.
Când are sens JSONB și când e doar lene?
Regula mea de aur e simplă: dacă ai scheme de date care se schimbă des, vin de la API-uri externe sau reprezintă atribute dinamice (cum sunt specificațiile tehnice pentru produse diferite - e.g. "capacitate baterie" pentru telefoane vs "număr de viteze" pentru biciclete), folosește JSONB.
Dacă ai câmpuri pe care le folosești constant în WHERE, JOIN sau care au nevoie de constrângeri stricte (cum ar fi foreign keys sau NOT NULL), ține-le în coloane relaționale clasice.
Trade-off-ul e destul de dureros dacă greșești:
- Avantaj: Flexibilitate maximă. Adaugi câmpuri noi fără migrări de schemă.
- Dezavantaj: JSONB ocupă mult mai mult spațiu pe disc (stochează cheile pentru fiecare rând) și consumă mai mult CPU la scriere și citire pentru că Postgres trebuie să serializeze/deserializeze datele. În plus, pierzi complet validarea tipurilor de date la nivel de bază de date.
Cum facem interogările rapide: Indecșii GIN
Dacă doar arunci date în JSONB și apoi dai query-uri clasice, Postgres va face un full table scan. La 5.000 de rânduri nu simți, dar la 500.000 o să-ți sune Alerta în PagerDuty.
Aici intervin indecșii GIN (Generalized Inverted Index). Aceștia permit indexarea componentelor interne dintr-un document JSONB.
Aveam două moduri principale de a defini un index GIN:
jsonb_ops(implicit): Indexează toate cheile, valorile și sub-elementele. E foarte flexibil, dar indexul devine uriaș pe disc.jsonb_path_ops: Indexează doar perechile cheie-valoare sub formă de hash-uri. Este mult mai mic (am salvat cam 40% din spațiul pe disc la proiectul de care vă ziceam) și mai rapid la căutări, dar suportă doar operatorul@>(contains).
Optimizare avansată: Indecși parțiali
Dacă știi că ai o singură cheie din JSONB după care filtrezi masiv (de exemplu, un status intern), nu indexa tot documentul JSON. Fă un index parțial B-Tree pe acea expresie specifică:
CREATE INDEX idx_events_status ON user_events ((metadata->>'status'));
Asta salvează enorm de mult spațiu și e la fel de rapid ca un index pe o coloană normală.
Cum interogăm eficient?
Dacă ai creat un index GIN cu jsonb_path_ops, trebuie să folosești operatorul @> ca indexul să fie luat în considerare de query planner. De exemplu, query-ul de mai jos va fi instant. Dacă folosești operatorul clasic ->>, Postgres s-ar putea să ignore indexul GIN complet. Am pățit asta pe un query care rula în 400ms și care a scăzut la sub 5ms doar după ce am rescris clauza WHERE să folosească @>.
Voi cum gestionați datele semistructurate? Mergeți pe JSONB în Postgres sau preferați să separați complet și să folosiți un document store dedicat când lucrurile devin complexe?