-- Crearea tabelului cu atribute dinamice
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL,
attributes JSONB NOT NULL DEFAULT '{}'::jsonb
);
-- Option A: Index GIN standard (acoperă toți operatorii @>, ?, ?&, ?|)
CREATE INDEX idx_products_attributes_gin ON products USING gin (attributes);
-- Option B: Index mai mic și mai rapid dacă folosești DOAR operatorul @>
CREATE INDEX idx_products_attributes_path ON products USING gin (attributes jsonb_path_ops);
-- Interogare care folosește indexul GIN instant
SELECT id, title, price
FROM products
WHERE attributes @> '{"brand": "Logitech", "wireless": true}';Mulți developeri pe care i-am mentorat au tentația să transforme Postgres-ul într-un document store mascat imediat ce dau de o structură mai flexibilă. Am făcut și eu greșeala asta prin 2017, la o aplicație de e-commerce cu vreo 140k produse în catalog. Rezultatul? Baza gâfâia și consuma memorie aiurea.
JSONB e o funcționalitate genială, dar trebuie folosită cu cap, nu ca scuză să nu mai faci diagramă ER.
Când are sens JSONB și când e o capcană
Să fim clari: dacă ai câmpuri fixe precum email, created_at sau status, le pui în coloane dedicate. SQL-ul clasic le optimizează perfect, iar constrângerile de tip te salvează de bug-uri stupide.
JSONB câștigă detașat în trei cazuri clare:
- Atribute dinamice sau de nișă: un laptop are
ram_gbșiprocessor, o pereche de adidași aremarime_eu. Să faci 50 de coloane nullable sau un tabel EAV (Entity-Attribute-Value) e un cosmar de mentenanță. - Payload-uri de la webhook-uri externe: când salvezi răspunsul brut de la Stripe sau PayPal pentru audit.
- Setări și preferințe de UI: structuri adânci care se citesc aproape mereu împreună.
Am văzut un proiect unde echipa pusese tot modelul de Order într-un singur câmp JSONB. Când a trebuit să scoatem rapoarte lunare pe vânzări, interogările au scăzut de la 20ms la peste 4 secunde. De ce? Pentru că la fiecare update pe un singur atribut din JSONB, engine-ul rescrie practic tot documentul pe disc (efectul MVCC).
Cum funcționează magia cu indexul GIN
JSONB nu e un simplu text (cum e tipul json). La inserare e parsat în format binar, se elimină spațiile și cheile duplicate. Scrierea e marginal mai lentă, dar citirea e extrem de rapidă.
Problema apare când ai 500k rânduri și cauți un produs cu o anumită proprietate. Fără un index, Postgres face Sequential Scan, adică citește fiecare rând de pe disc.
Aici intervine indexul GIN (Generalized Inverted Index). Spre deosebire de un B-Tree clasic care indexează valoarea întreagă a coloanei, GIN desface documentul JSONB în fiecare cheie și valoare individuală și le pune într-un index inversat.
Dacă folosești operatorul de incluziune @>, cu un index GIN configurat corect, timpii de răspuns scad dramatic. În cazul meu, pe o masă de date cu 300k înregistrări, am scăzut de la 420ms la sub 8ms.
Trade-off-uri sincere de care nu-ți spune nimeni
Nimic nu e gratuit în ingineria software. Înainte să pui GIN peste tot, ține cont de chestiile astea:
- Dimensiunea pe disc: Un index GIN pe date JSONB mari poate ajunge la 50-80% din mărimea tabelului în sine. Am avut un caz în care tabelul avea 3GB și indexul mai adăuga încă 2.2GB.
- Penalizare la scriere: La fiecare
INSERTsauUPDATE, Postgres trebuie să spargă noul JSONB și să actualizeze arborele GIN. Dacă ai scrieri intense (peste câțiva sute de ops/sec), scrierea va deveni un bottleneck. - Lipsa Foreign Keys: Nu poți pune constrângeri de integritate referențială pe o cheie aflată în interiorul unui JSONB. Validarea trebuie mutată 100% în aplicație.
Dacă ai doar 2-3 câmpuri fixe pe care le cauți des în JSONB, mai bine faci un index B-Tree pe o expresie dedicată decat un GIN pe tot documentul.
Voi cum folosiți JSONB în proiectele curente? L-ați lăsat doar pentru setări și integrări sau aveți entități principale stocate așa?