-- Crearea unui index GIN optimizat folosind jsonb_path_ops
CREATE INDEX idx_orders_metadata_path
ON orders USING gin (metadata jsonb_path_ops);
-- Query extrem de rapid care folosește indexul de mai sus
SELECT id, metadata
FROM orders
WHERE metadata @> '{"source": "newsletter", "campaign": {"id": 42}}';Am văzut prea des oameni care aruncă totul în JSONB doar pentru că "e la modă" sau "nu știm ce schemă o să avem". Am pățit-o și eu acum câțiva ani pe un proiect de e-commerce și am plătit scump la performanță. În postarea asta îți arăt când merită să folosești JSONB în Postgres, când e o idee proastă și cum pui un index GIN ca să nu plângă baza de date la query-uri.
Când e bun JSONB și când e o capcană
Făceam un sistem de atribute dinamice pentru produse (mărimi, culori, specificații tehnice). Aveam vreo 800.000 de produse în bază. Varianta clasică cu tabelă de legătură de tip EAV (Entity-Attribute-Value) devenise un coșmar la join-uri și query-uri de agregare. Aici a strălucit JSONB. Am putut stoca toate specificațiile într-un singur câmp document.
Dar atenție la trade-off-ul sincer: pierzi complet constrângerile de integritate la nivel de bază de date. Nu poți pune un Foreign Key pe un ID ascuns în interiorul unui JSONB. Nu ai tipuri de date stricte. Dacă un coleg scrie din greșeală {"price": "100"} (ca string) în loc de {"price": 100} (ca number), Postgres le stochează fericit pe ambele, iar tu o să ai erori ciudate la runtime în aplicație.
Regula mea de aur e simplă: dacă ai date structurate care reprezintă nucleul business-ului (un user_id, un email, un status de comandă), folosește coloane normale. Dacă ai metadate, loguri de audit, payload-uri de la API-uri externe sau atribute extrem de dinamice, JSONB e rege.
Cum interoghezi eficient
Mulți developeri folosesc operatorul ->> pentru a extrage valori ca text și apoi le compară clasic în clauza WHERE. Merge bine pe tabele mici, dar pe tabele mari e un dezastru pentru că forțează Postgres să facă un full table scan.
Pentru a beneficia de indexare, cel mai bine este să folosești operatorul de incluziune @>. Acest operator verifică dacă documentul JSONB din stânga conține structura din dreapta.
Indexarea GIN (Generalized Inverted Index)
Fără un index dedicat, Postgres va scana fiecare rând în parte pentru a găsi cheia din JSONB. La un proiect cu 5 milioane de rânduri de loguri, un query simplu pe un câmp neindexat ne lua cam 3.5 secunde. Inacceptabil pentru producție.
Salvarea vine de la indexul GIN. Acesta practic "sparge" documentul JSONB în chei și valori individuale și le pune într-un index inversat. Avem două opțiuni mari de clase de operatori pentru GIN:
- jsonb_ops (default): Indexează absolut tot (chei, valori, sub-obiecte). Este extrem de flexibil, dar indexul devine uriaș pe disc.
- jsonb_path_ops: Indexează doar căile complete (ex:
{"user": {"id": 10}}devine o singură intrare de tip hash). Este considerabil mai mic, query-urile sunt mai rapide, dar funcționează exclusiv cu operatorul de incluziune@>.
Pe proiectul menționat, după ce am trecut de la jsonb_ops la jsonb_path_ops, dimensiunea indexului a scăzut cu 40%, iar timpul de query a coborât de la 3.5 secunde la doar 8ms.
Totuși, există un cost: write performance-ul scade cu vreo 15-20% pentru că Postgres trebuie să recalculeze acești indecși complecși la fiecare insert sau update. Dacă ai o tabelă în care se scrie masiv în fiecare secundă și se citește rar, s-ar putea ca un index GIN să-ți blocheze baza de date.
Voi cum gestionați schema drift-ul când folosiți JSONB? Faceți validare strictă în aplicație (cu librării gen Zod sau Pydantic) sau lăsați baza de date liberă și sperați că totul va fi bine?