-- Structură tabel pentru produse cu atribute variabile
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL,
specs JSONB NOT NULL DEFAULT '{}'::jsonb
);
-- Index GIN optimizat pentru căutări prin contenție (@>)
-- Este mai mic pe disc și mai rapid decât GIN-ul default
CREATE INDEX idx_products_specs_path_ops
ON products USING GIN (specs jsonb_path_ops);
-- Query care profită din plin de index (operatorul @>)
SELECT id, name, price
FROM products
WHERE specs @> '{"ram": "32GB", "storage": "1TB"}';Toată lumea vrea flexibilitate până când primește un query de raportare care face full table scan pe un milion de rânduri. Am văzut echipe întregi care au tratat Postgres ca pe un Mongo mai ciudat, aruncând tot payload-ul într-o coloană data jsonb de lene să scrie migrări. Rezultatul? Bază de date umflată, lipsă de integritate referențială și nervi la fiecare WHERE.
JSONB este o unealtă excelentă, dar are un cost clar. Dacă înțelegi cum funcționează sub capotă și cum îl ajuți cu un index GIN, rezolvi probleme reale fără să compromiți viteza.
Când are sens JSONB și când e doar lene
Regula mea de bază e simplă: datele esențiale pentru business, care au o structură previzibilă și cer integritate referențială (user_id, status, total, created_at), rămân mereu coloane dedicate. Fără excepții.
Unde strălucește JSONB?
- Atribute dinamice: La un e-commerce cu 85k de produse din categorii complet diferite (pantofii au mărimi, laptopurile au RAM și frecvență CPU). Să faci model EAV (Entity-Attribute-Value) e un coșmar de JOIN-uri. Aici JSONB e salvator.
- Payloads de la terți: Webhook-uri de la Stripe sau log-uri de audit unde schema se schimbă des și ai nevoie doar să stochezi răspunsul brut pentru debug sau procesare asincronă.
- Setări de UI per utilizator: Când ai zeci de flag-uri mărunte care nu influențează direct logica de backend.
Trade-off-ul sincer: pierzi FOREIGN KEY-uri native în interiorul JSON-ului, verificările de tip devin manuale sau cer constrângeri de tip CHECK, iar dimensiunea pe disc crește simțitor din cauza duplicării cheilor pentru fiecare rând.
Indexarea GIN: diferența de la sute de milisecunde la instant
Dacă dai un simplu SELECT * FROM products WHERE specs->>'ram' = '16GB';, Postgres va citi tot tabelul de la cap la coadă. La 10k rânduri abia simți. La 800k rânduri, query-ul ăla începe să dureze 600-700ms și îți ține conexiunile blocate.
Aici intră în scenă indexul GIN (Generalized Inverted Index). GIN extrage toate cheile și valorile din document și construiește o structură internă similară unui index de căutare full-text.
Ai două abordări mari:
- Index GIN clasic (
jsonb_ops): E varianta default. Poate căuta după existența cheilor (?), a mai multor chei sau după structură completă. Problema e că indexul rezultat e masiv pe disc. - Index GIN cu
jsonb_path_ops: Asta e varianta pe care o aleg în 90% din cazuri. Creează hash-uri pe combinațiile cheie-valoare. Indexul e cu vreo 35-40% mai mic pe disc și căutările cu operatorul de contenție@>zboară.
Într-un proiect recent cu specificații hardware, o filtrare compusă după producător și memorie a scăzut de la 620ms la doar 4ms după adăugarea unui index cu jsonb_path_ops.
Atenție la scrieri
Nimic nu e gratuit. Indexul GIN este extrem de scump la INSERT și UPDATE. De fiecare dată când modifici un JSONB, Postgres trebuie să actualizeze zeci de intrări din arborele GIN. Dacă ai o tabelă cu mii de scrieri pe secundă, s-ar putea să sugrumi I/O-ul.
Dacă ai nevoie să filtrezi doar după un singur câmp din JSONB (să zicem un tenant_id rătăcit pe-acolo), nu face index GIN pe tot blob-ul. Fă un index B-Tree normal pe expresia respectivă: CREATE INDEX idx_tenant ON logs ((data->>'tenant_id'));.
Voi cum abordați datele dinamice? Ați rămas la tabele EAV clasice sau împingeți tot ce e semi-structurat în JSONB?