-- Crearea indexului optimizat pentru căutări rapide
CREATE INDEX idx_products_specs_path_ops
ON products USING gin (specifications jsonb_path_ops);
-- Query-ul care va folosi eficient acest index (folosind operatorul @>)
SELECT id, name, specifications->>'brand' as brand
FROM products
WHERE specifications @> '{"category": "Electronics", "specs": {"color": "black"}}';Am trecut cu toții prin asta: clientul vrea "câmpuri dinamice" și prima reacție e să arunci un JSONB în Postgres și să-ți vezi de viață. Sună a libertate, dar dacă nu ești atent, te trezești rapid cu query-uri de 5 secunde pe o tabelă de doar 150k rânduri. Hai să vorbim pe cifre și pe cazuri reale despre când merită să folosești JSONB și cum îl faci să zboare cu un index GIN.
Când e JSONB o idee bună (și când e o capcană)
Anul trecut am refăcut un modul de catalog pentru un magazin online cu vreo 25.000 de produse active. Fiecare categorie avea atribute complet diferite: televizoarele aveau diagonală și rezoluție, pantofii aveau mărime și material, iar hrana de câini avea greutate și ingrediente.
Dacă aș fi mers pe varianta clasică EAV (Entity-Attribute-Value), aș fi avut nevoie de 3 tabele extra și join-uri masive doar ca să afișez o pagină de produs. Am ales JSONB pentru coloana specifications. A fost decizia corectă: flexibilitate maximă la importul de date și zero join-uri.
Dar am avut și reversul medaliei la un alt proiect, cu 800k de useri. Cineva decisese să bage setările de profil (chestii simple: dark_mode: boolean, notifications: boolean) într-un JSONB numit settings. Eroare majoră. Făceam mii de update-uri pe secundă pe acele booleene. În Postgres, JSONB-ul se rescrie complet la fiecare update. Ne-am trezit cu un write amplification masiv și un vacuum care nu mai făcea față. Am mutat setările în coloane normale boolean și am redus IOPS-ul pe baza de date cu aproape 40%.
Regula mea de aur: dacă faci des UPDATE pe o valoare, sau dacă ai nevoie de constrângeri de tip (foreign keys, NOT NULL), folosește coloane clasice. Dacă ai date eterogene, care vin din surse externe și nu se modifică des, JSONB e perfect.
Cum indexezi ca să nu plângi în producție
Dacă ai decis că JSONB e calea, nu lăsa interogările la voia întâmplării. Un query simplu cu operatorul ->> pe o coloană neindexată va face un secvențial scan pe toată tabela, adică va citi fiecare rând de pe disc.
Soluția este indexul GIN (Generalized Inverted Index). Acesta creează intrări pentru fiecare element din interiorul JSON-ului. Avem două moduri principale de a defini un index GIN, iar diferența dintre ele e critică pentru performanță:
jsonb_ops(cel default): Indexează cheile, căile și valorile. E foarte flexibil, dar e mare pe disc.jsonb_path_ops: Indexează doar hash-urile întregii căi (cheie + valoare). Este considerabil mai mic (am economisit cam 30% la dimensiunea indexului pe disc într-un proiect recent) și adesea mai rapid la căutări de tipul "conține".
Uită-te la exemplul de cod de mai jos. Folosim jsonb_path_ops pentru că, de cele mai multe ori, ne interesează dacă un document conține o anumită pereche cheie-valoare.
Trade-off-ul de care nu vorbește nimeni
Indexul GIN e genial pentru citire, dar are un cost ascuns uriaș la scriere. Fiecare insert sau update pe acel câmp JSONB va fi mult mai lent, deoarece Postgres trebuie să actualizeze multiple intrări în indexul GIN. Pe o tabelă cu write-uri intense, un index GIN pe o coloană JSONB mare poate deveni rapid un bottleneck.
De asemenea, pierzi complet constrângerile de integritate la nivel de bază de date. Nu poți pune un Foreign Key în interiorul unui JSONB. Dacă ștergi un user, referința din JSONB-ul altei tabele rămâne acolo, orfană, și trebuie să te ocupi de asta din codul aplicației.
Voi cum gestionați datele semistructurate? Ați migrat vreodată înapoi de la JSONB la coloane clasice din motive de performanță?