1. EAV (Entity-Attribute-Value) Modeli Neden İlişkisel Veritabanının Katilidir?
E-ticaret katalogları dinamik doğası gereği heterojendir. Bir tekstil ürününde Beden (S, M, L) ve Renk (Kırmızı, Siyah) varken; bir bilgisayarda İşlemci, RAM, SSD Kapasitesi, Ekran Yenileme Hızı yer alır.
Bu değişkenliği çözmek için 15 yıl önce popüler olan EAV (Entity-Attribute-Value) modelinde veriler şu tablolara bölünür:
-- EAV Şeması: Sonsuz JOIN Labirenti
products (id, name, base_price)
attributes (id, code, label)
attribute_values (id, product_id, attribute_id, value)
Kullanıcı "16GB RAM'e sahip, 512GB SSD'li ve Siyah renkli laptopları" listelemek istediğinde veritabanı aynı tabloyu 3 kez JOIN etmek zorunda kalır:
SELECT p.id, p.name
FROM products p
JOIN attribute_values av1 ON p.id = av1.product_id AND av1.attribute_id = 'ram' AND av1.value = '16GB'
JOIN attribute_values av2 ON p.id = av2.product_id AND av2.attribute_id = 'ssd' AND av2.value = '512GB'
JOIN attribute_values av3 ON p.id = av3.product_id AND av3.attribute_id = 'color' AND av3.value = 'Black'
WHERE p.is_active = true;
Katalogda 200.000 ürün ve 3 milyon özellik satırı olduğunda bu sorgu veritabanının CPU'sunu kilitler, disk okuma hızını (IOPS) tüketir ve yanıt süresi 1.5 - 3 saniyeye fırlar.
2. Stilmerce Hibrit Modeli: İlişkisel Güç + JSONB Esnekliği
Stilmerce mühendisleri, finansal ve mutlak tutarlılık gerektiren verileri (Fiyat, Stok, Kategori, Vergi, SKU) geleneksel ilişkisel sütunlarda tutar. Dinamik ve filtrelenebilir tüm teknik özellikleri ise PostgreSQL'in ikili (binary) formatta sakladığı JSONB sütununa emanet eder:
-- Stilmerce Hibrit Tablo Tasarımı
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sku VARCHAR(64) UNIQUE NOT NULL,
price_cents BIGINT NOT NULL,
stock INT NOT NULL DEFAULT 0,
category_id UUID REFERENCES categories(id),
-- Dinamik varyantlar ve teknik özellikler JSONB olarak saklanır
specs JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Örnek bir ürünün specs içeriği:
{
"ram_gb": 16,
"storage_gb": 512,
"color": "Uzay Grisi",
"screen_inches": 14.2,
"tags": ["m4_pro", "apple_silicon"]
}3. GIN İndeksleme Mucizesi: jsonb_path_ops ile Süper Hız
Ham bir JSON sütununda arama yapmak normalde tüm tabloyu baştan sona tarar (Sequential Scan). Ancak PostgreSQL'in GIN (Generalized Inverted Index) özelliği sayesinde JSONB içindeki her bir anahtar ve değer kombinasyonu tersine çevrilmiş bir indeks ağacına dönüştürülür:
-- GIN jsonb_path_ops İndeksi Oluşturma
CREATE INDEX idx_products_specs_gin ON products USING GIN (specs jsonb_path_ops);
jsonb_ops vs jsonb_path_ops
Varsayılan jsonb_ops indeksi her anahtarı ve değeri ayrı ayrı indeksler ve diskte devasa yer tutar. Stilmerce'in kullandığı jsonb_path_ops ise doğrudan specs @> '{"ram_gb": 16, "color": "Uzay Grisi"}' containment sorgularına odaklanır. İndeks boyutu %60 daha küçüktür ve sorgular 2 kat daha hızlı çalışır.
Artık tek bir JOIN bile yapmadan mikro saniyeler içinde 4 farklı özelliğe göre filtreleme yapabiliriz:
-- Sıfır JOIN, İndeks Destekli Filtreleme Sorgusu
SELECT id, sku, price_cents
FROM products
WHERE category_id = 'c111-222'
AND specs @> '{"ram_gb": 16, "storage_gb": 512}';4. Performans ve Depolama Benchmark Karşılaştırması
500.000 ürün ve 3.5 milyon özellik içeren sentetik veri tabanında yapılan test sonuçları:
| Ölçüt / Kriter | Geleneksel EAV Modeli | MongoDB (Saf NoSQL) | PostgreSQL JSONB + GIN |
|---|---|---|---|
| 3 Kriterli Filtreleme Süresi | 1.480 ms (Yavaş) | 18 ms | 3.4 ms (Ultra Hızlı) |
| Finansal ACID Garantisi | Var (İlişkisel) | Zayıf / Karmaşık | Tam ACID Uyumluluğu |
| İndeks Boyutu (Disk Alanı) | 420 MB (Join tabloları) | 310 MB | 94 MB (jsonb_path_ops) |
| Şema Değiştirme Kolaylığı | Zor ve hantal | Kolay | Sıfır Migration (Anında Kullanım) |
Mühendislik Çıkarımı
PostgreSQL JSONB hibrit mimarisi sayesinde, MongoDB'nin getirdiği ACID risklerine girmeden ve EAV modelinin JOIN felaketine uğramadan; e-ticarette sınırsız varyant esnekliği ve milisaniyelik arama hızı elde edilir.