-- Crearea unui index GIN eficient folosind jsonb_path_ops
CREATE INDEX idx_products_metadata_path ON products
USING GIN (metadata jsonb_path_ops);
-- Query optimizat care va folosi indexul de mai sus
-- Operatorul @> verifică dacă obiectul din stânga conține structura din dreapta
SELECT id, name
FROM products
WHERE metadata @> '{"category": "electronics", "brand": "Sony"}';
-- Exemplu de check constraint pentru a asigura o structură minimă
ALTER TABLE products
ADD CONSTRAINT check_metadata_schema
CHECK (jsonb_typeof(metadata->'price') = 'number');Am văzut prea mulți developeri care aruncă totul într-un câmp JSONB doar pentru că le e lene să scrie o migrare de schemă sau pentru că „așa e la modă”. E o capcană în care am căzut și eu acum vreo 6 ani la un proiect de e-commerce unde atributele produselor se schimbau de la o săptămână la alta. Am crezut că am găsit soluția magică, dar m-am trezit rapid cu un storage care exploda și query-uri care începeau să agațe.
JSONB în Postgres e o unealtă incredibilă, dar trebuie să înțelegi ce se întâmplă sub capotă. Spre deosebire de tipul JSON simplu (care e doar un string validat), JSONB e stocat într-un format binar descompus. Asta înseamnă că e mai lent la insert, pentru că Postgres trebuie să proceseze structura, dar e mult mai rapid la procesare și, cel mai important, suportă indexare.
Când să alegi JSONB și când să rămâi la coloane
Regula mea de aur e simplă: dacă datele tale au o structură fixă și faci des operații de tip JOIN sau agregări (SUM, AVG) pe acele câmpuri, folosește coloane normale. Am avut un caz la un sistem de analytics cu 10 milioane de rânduri unde am mutat „price” dintr-un obiect JSONB în coloană numerică dedicată. Am economisit cam 15% spațiu pe disc și am câștigat o viteză de calcul de 3 ori mai mare pentru rapoartele lunare.
Folosesc JSONB doar pentru date semi-structurate. Gândește-te la setări de user, metadate de la API-uri externe sau atribute de produs care diferă enorm de la o categorie la alta. Dacă ai un tabel de „Produse” și unul are „tensiune_alimentare” iar altul are „numar_pagini”, JSONB e sfânt. Te scapă de un tabel de tip EAV (Entity-Attribute-Value) care e un coșmar la query-uri și performanță.
Magia indexării GIN
Dacă dai un query pe un câmp JSONB fără index, Postgres face un full table scan. La 8.000 de useri nu simți, dar la 500.000 o să înceapă să urle procesorul. Aici intră în scenă GIN (Generalized Inverted Index). GIN funcționează ca un index la finalul unei cărți: știe exact în ce „pagini” (rânduri) se află o anumită cheie sau valoare.
Există două tipuri mari de operatori pentru GIN pe JSONB: jsonb_ops (default) și jsonb_path_ops. Primul e mai flexibil, suportă operatori ca ?, ?| și ?&. Al doilea, jsonb_path_ops, e mult mai rapid și mai mic ca dimensiune pe disc, dar suportă doar operatorul de incluziune @>. Pe proiectul de e-commerce de care ziceam, am trecut la jsonb_path_ops și am văzut o reducere a indexului cu vreo 30% și query-urile au devenit vizibil mai snappy.
Trade-off-ul de care nu-ți zice nimeni
Cel mai mare minus la JSONB e legat de update-uri. Postgres folosește MVCC (Multi-Version Concurrency Control). Când faci update la un singur câmp mic dintr-un obiect JSONB de 100KB, Postgres scrie practic o versiune nouă a întregului rând. Dacă ai un workflow cu multe scrieri pe obiecte mari, o să te lovești de write amplification și o să mănânci IOPS-ul instanței de DB imediat.
Un alt aspect e lipsa constrângerilor de tip data integrity la nivel de engine. Da, poți pune un check constraint cu o funcție care validează schema, dar devine greoi. În general, dacă te regăsești scriind prea multe validări manuale, poate că acele date meritau să fie coloane de tip integer sau boolean de la bun început.
În concluzie, JSONB câștigă detașat când ai flexibilitate maximă de care chiar ai nevoie, dar coloanele clasice rămân baza pentru datele tranzacționale core. Voi cum decideți unde trageți linia între schemă fixă și JSONB?