eduardweb.
PostgreSQLIntermediar#performance#postgresql#database#jsonb#indexing

Postgres JSONB în producție: când are sens și cum scazi query-urile de la 1.2s la 4ms cu GIN

De Cristian Barbu, 5 aug. 2026 · 7 vizualizări · 2 like-uri

Postat 5 aug. 2026
sql
-- Schema tabelului
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    attributes JSONB NOT NULL DEFAULT '{}'::jsonb
);

-- Index GIN optimizat doar pentru operatorul @>
CREATE INDEX idx_products_attributes_path 
ON products USING gin (attributes jsonb_path_ops);

-- Query rapid care folosește indexul creat (execuție ~4ms pe 150k rânduri)
SELECT id, name, attributes
FROM products
WHERE attributes @> '{"specs": {"brand": "Asus", "ram": "16GB"}}';

Recunoașteți: câți n-ați trântit un câmp data jsonb în tabel doar ca să nu mai scrieți o migrare de schemă la sprint-ul următor? Am făcut și eu asta acum vreo 4 ani pe un proiect de e-commerce cu vreo 120.000 de produse. La început a fost vis. Ulterior, când tabelul a crescut și aveam nevoie de rapoarte filtrat pe atribute, a început coșmarul.

Când are sens JSONB și când e doar lene de migrare

JSONB e excelent pentru atribute dinamice sau date eterogene. Gândește-te la un catalog de produse unde un laptop are "RAM" și "Procesor", dar o pereche de papuci are "Mărime" și "Culoare". Dacă ai încerca să normalizezi asta în coloane clasice, ai ajunge la un antipattern de tip Entity-Attribute-Value (EAV) sau la un tabel cu 200 de coloane pline de NULL-uri. JSONB salvează ziua aici.

E util și pentru payload-uri de webhooks externe pe care doar le stochezi pentru audit sau setări de UI per utilizator. Răul începe însă când pui în JSONB date structurate fix pe care le interoghezi des sau le folosești în JOIN-uri.

Trade-off-urile de care nu-ți spune nimeni

JSONB vine cu costuri ascunse. Primul e spațiul: fiecare rând salvează și cheile JSON, nu doar valorile. Dacă ai cheia "user_preferred_notification_language" repetată de 2 milioane de ori, ai aruncat câțiva megabiți buni pe geam doar pe denumiri.

Al doilea cost e scrierea. Postgres folosește MVCC. Când faci update pe o singură proprietate dintr-un JSONB de 50KB, Postgres nu modifică doar bitul ăla, ci rescrie tot documentul JSONB pe disc. Dacă ai un câmp last_active_at într-un JSONB și îl actualizezi des, o să-ți omori discul cu I/O degeaba.

Operatorii care contează și greșeala din SELECT

Ca să scoți date, ai operatorul ->> (care-ți dă text) și -> (care-ți dă JSONB). Dar dacă vrei viteză la căutare pe indecși, regele e @> (contains).

Multe query-uri pe care le-am văzut în code review arătau așa: SELECT * FROM products WHERE attributes->>'brand' = 'Asus';

Query-ul ăsta va face Sequential Scan de cele mai multe ori dacă nu ai un B-Tree index funcțional creat direct pe expresia respectivă. Dacă vrei flexibilitate și indecși generici, abordarea corectă este cu operatorul @>.

Indexul GIN și trucul cu jsonb_path_ops

Fără index, un query cu @> pe 150k de rânduri îmi făcea Sequential Scan și dura în jur de 1.2 secunde. Inacceptabil într-un API.

Soluția clasică e un index GIN standard. A scăzut timpul la ~35ms. Mult mai bine, dar putem mai mult.

Dacă știi că vei interoga JSON-ul doar cu operatorul @> (fără să verifici dacă există doar o cheie anume folosind ? sau ?|), poți folosi clasa de operatori jsonb_path_ops.

Rezultatul? Indexul devine cu aproximativ 40-50% mai mic ca dimensiune pe disc, iar query-ul nostru a scăzut de la 35ms la 4ms. De ce? Pentru că jsonb_path_ops stochează doar hash-uri ale căilor complete (ex: attributes.brand.Asus), în loc să indexeze fiecare cheie și valoare separat ca în opțiunea default.

Voi cum gestionați datele semi-structurate în Postgres? Ați rămas pe JSONB sau ați simțit nevoia să treceți înapoi la tabele relaționale după ce a crescut volumul?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.