JSON Veri Tipi ile MySQL’de Esnek Veri Saklama

Klasik ilişkisel veritabanı yapısında her şeyi önceden tanımlamak zorundasınızdır. Ürün tablosuna yeni bir özellik eklemek mi istiyorsunuz? ALTER TABLE, migration, uygulama kodunda güncelleme… Haftalarca sürebilecek bir süreç. MySQL 5.7 ile hayatımıza giren JSON veri tipi ise bu katı yapıya esnek bir alternatif sunuyor. Özellikle değişken yapılı verileri, farklı özelliklere sahip ürün kataloglarını veya kullanıcı tercihlerini saklamak için JSON kolonu kullanmak, doğru senaryoda inanılmaz zaman kazandırıyor. Gelin bu özelliği gerçek dünya örnekleriyle inceleyelim.

JSON Veri Tipinin Avantajları ve Dezavantajları

Her teknolojide olduğu gibi, JSON kolonları da her derde deva değil. Önce neye girdiğimizi anlayalım.

Avantajlar:

  • Şema değişikliği gerektirmeden yeni alanlar eklenebilir
  • Farklı yapıdaki nesneler tek tabloda saklanabilir
  • MySQL, JSON içindeki verileri doğrular ve geçersiz JSON kaydetmez
  • Yerleşik JSON fonksiyonlarıyla güçlü sorgulama imkanı
  • Sanal kolonlar ve indexler sayesinde performans sorunu yaşanmaz

Dezavantajlar:

  • Normal kolonlara göre daha fazla disk alanı kullanır
  • Karmaşık JSON sorgular okunabilirliği azaltır
  • Her alanı ayrı ayrı indexlemek gerekir
  • JOIN işlemlerinde zorluklar çıkabilir

Kural şu: Yapısı kesin ve değişmeyecek veriler için normal kolonlar kullanın. Değişken, opsiyonel veya heterojen yapıdaki veriler için JSON’ı tercih edin.

Temel Kurulum ve Hazırlık

MySQL 5.7+ veya MariaDB 10.2.7+ sürümünü kullanıyor olmanız gerekiyor. Sürümünüzü kontrol edelim:

mysql -u root -p -e "SELECT VERSION();"

Test ortamı için bir veritabanı oluşturalım:

mysql -u root -p << 'EOF'
CREATE DATABASE IF NOT EXISTS json_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE json_test;

CREATE TABLE urunler (
    id INT AUTO_INCREMENT PRIMARY KEY,
    sku VARCHAR(50) NOT NULL UNIQUE,
    ad VARCHAR(200) NOT NULL,
    kategori VARCHAR(100) NOT NULL,
    fiyat DECIMAL(10,2) NOT NULL,
    ozellikler JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

DESCRIBE urunler;
EOF

Görüldüğü gibi ozellikler kolonunu JSON tipi olarak tanımladık. Bu kolona kategori bazında tamamen farklı yapıda veriler yazabileceğiz. Bir laptop’ın özellikleri ile bir tişörtün özellikleri aynı alanda yaşayacak.

Veri Ekleme ve JSON Fonksiyonları

İlk verilerimizi ekleyelim. JSON yazarken MySQL’in doğrulama yaptığını göreceksiniz:

mysql -u root -p json_test << 'EOF'

-- Laptop ürünü ekle
INSERT INTO urunler (sku, ad, kategori, fiyat, ozellikler) VALUES (
    'LAP-001',
    'ProBook 15 Laptop',
    'elektronik',
    12500.00,
    JSON_OBJECT(
        'islemci', 'Intel i7-12700H',
        'ram', '16GB DDR5',
        'depolama', '512GB NVMe SSD',
        'ekran', JSON_OBJECT('boyut', '15.6 inc', 'cozunurluk', '1920x1080', 'panel', 'IPS'),
        'batarya', '72Wh',
        'agirlik', '1.8kg',
        'renkler', JSON_ARRAY('Uzay Grisi', 'Gümüş'),
        'garanti', 24,
        'wifi6', true,
        'bluetooth', '5.2'
    )
);

-- Tişört ekle - tamamen farklı özellikler
INSERT INTO urunler (sku, ad, kategori, fiyat, ozellikler) VALUES (
    'TSR-001',
    'Basic Cotton Tişört',
    'giyim',
    199.90,
    JSON_OBJECT(
        'malzeme', '%100 Pamuk',
        'bedenler', JSON_ARRAY('XS', 'S', 'M', 'L', 'XL', 'XXL'),
        'renkler', JSON_ARRAY('Siyah', 'Beyaz', 'Lacivert', 'Kırmızı'),
        'yikama', '30 derecede yıkayınız',
        'mensei', 'Türkiye',
        'sertifika', JSON_ARRAY('OEKO-TEX', 'GOTS')
    )
);

-- Kahve makinesi ekle
INSERT INTO urunler (sku, ad, kategori, fiyat, ozellikler) VALUES (
    'KAH-001',
    'Espresso Pro 3000',
    'elektronik',
    4750.00,
    JSON_OBJECT(
        'guc', '1450W',
        'basinc', '15 bar',
        'su_deposu', '1.8L',
        'renk', 'Siyah/Gümüş',
        'kapsul_uyumlu', JSON_ARRAY('Nespresso', 'Dolce Gusto'),
        'otomatik_kapama', true,
        'garanti', 12,
        'boyutlar', JSON_OBJECT('genislik', '28cm', 'yukseklik', '32cm', 'derinlik', '42cm')
    )
);

SELECT id, sku, ad, JSON_PRETTY(ozellikler) FROM urunler LIMIT 1;
EOF

JSON_OBJECT() ve JSON_ARRAY() fonksiyonları JSON oluştururken tip güvenliği sağlıyor. Doğrudan string olarak da JSON yazabilirsiniz ama bu fonksiyonları kullanmak hata riskini azaltır.

JSON Verisini Sorgulama

JSON kolonundaki verilere erişmek için iki operatör kullanılır:

-> : JSON değerini döndürür (tırnak işaretleriyle) ->> : JSON değerini text olarak döndürür (tırnak işaretleri olmadan)

mysql -u root -p json_test << 'EOF'

-- Temel erişim
SELECT 
    sku,
    ad,
    ozellikler->>'$.islemci' AS islemci,
    ozellikler->>'$.ram' AS ram,
    ozellikler->>'$.garanti' AS garanti_ay
FROM urunler
WHERE kategori = 'elektronik';

-- İç içe JSON erişimi
SELECT 
    sku,
    ad,
    ozellikler->>'$.ekran.boyut' AS ekran_boyutu,
    ozellikler->>'$.ekran.panel' AS panel_tipi
FROM urunler
WHERE sku = 'LAP-001';

-- JSON_EXTRACT ile aynı işlem
SELECT 
    sku,
    JSON_EXTRACT(ozellikler, '$.ekran.cozunurluk') AS cozunurluk
FROM urunler
WHERE JSON_EXTRACT(ozellikler, '$.ekran') IS NOT NULL;

-- JSON dizisinden eleman erişimi
SELECT 
    sku,
    ad,
    ozellikler->>'$.renkler[0]' AS ilk_renk,
    ozellikler->>'$.renkler[1]' AS ikinci_renk,
    JSON_LENGTH(ozellikler->'$.renkler') AS renk_sayisi
FROM urunler;

EOF

JSON_TABLE ile İlişkisel Sorgular

MySQL 8.0 ile gelen JSON_TABLE() fonksiyonu, JSON dizilerini satırlara dönüştürür. Bu özellik gerçekten güçlü:

mysql -u root -p json_test << 'EOF'

-- JSON dizisini satırlara çevir
SELECT 
    u.sku,
    u.ad,
    renk_tablosu.renk
FROM urunler u,
JSON_TABLE(
    u.ozellikler,
    '$.renkler[*]' COLUMNS (
        renk VARCHAR(50) PATH '$'
    )
) AS renk_tablosu
WHERE u.ozellikler->'$.renkler' IS NOT NULL;

-- Laptop bedenlerini listele (tişört için)
SELECT 
    u.sku,
    u.ad,
    bedenler.beden
FROM urunler u,
JSON_TABLE(
    u.ozellikler,
    '$.bedenler[*]' COLUMNS (
        beden VARCHAR(10) PATH '$'
    )
) AS bedenler
WHERE u.sku = 'TSR-001';

EOF

Bu sorgu şöyle bir çıktı üretir: Her renk için ayrı bir satır. Rapor üretmek veya JOIN yapmak için çok kullanışlı.

JSON Kolonlarına Index Eklemek

En yaygın yapılan hata: JSON kolonu oluşturup index eklemeden sorgulama yapmak. JSON kolonu indexlenemez ama sanal kolon (generated column) üzerinden index ekleyebilirsiniz:

mysql -u root -p json_test << 'EOF'

-- Garanti süresi üzerine sanal kolon ve index
ALTER TABLE urunler
ADD COLUMN garanti_ay INT GENERATED ALWAYS AS (
    CAST(ozellikler->>'$.garanti' AS UNSIGNED)
) VIRTUAL;

ALTER TABLE urunler
ADD INDEX idx_garanti (garanti_ay);

-- Artık bu sorgu index kullanıyor
EXPLAIN SELECT * FROM urunler WHERE garanti_ay >= 12;

-- Kategori + özellik kombinasyonu için
ALTER TABLE urunler
ADD COLUMN islemci_tipi VARCHAR(100) GENERATED ALWAYS AS (
    ozellikler->>'$.islemci'
) VIRTUAL;

ALTER TABLE urunler
ADD INDEX idx_islemci (kategori, islemci_tipi);

-- JSON_OVERLAPS ile dizi kontrolü (MySQL 8.0+)
SELECT sku, ad 
FROM urunler 
WHERE JSON_OVERLAPS(ozellikler->'$.kapsul_uyumlu', '["Nespresso"]');

EOF

Gerçek Dünya Senaryosu: E-Ticaret Ürün Katalogu

Şimdi daha gerçekçi bir örnek kuralım. Yüzlerce farklı kategoride ürün satan bir e-ticaret sitesi düşünün:

mysql -u root -p json_test << 'EOF'

-- Kullanıcı tercihleri tablosu
CREATE TABLE kullanici_tercihleri (
    id INT AUTO_INCREMENT PRIMARY KEY,
    kullanici_id INT NOT NULL,
    tercihler JSON NOT NULL,
    UNIQUE KEY uk_kullanici (kullanici_id)
);

-- Kullanıcı tercihlerini kaydet
INSERT INTO kullanici_tercihleri (kullanici_id, tercihler) VALUES
(101, JSON_OBJECT(
    'bildirimler', JSON_OBJECT(
        'email', true,
        'sms', false,
        'push', true,
        'kampanya', true,
        'siparis_guncelleme', true
    ),
    'gorunum', JSON_OBJECT(
        'tema', 'koyu',
        'dil', 'tr',
        'para_birimi', 'TRY',
        'urun_gosterim', 'izgara'
    ),
    'adresler', JSON_ARRAY(
        JSON_OBJECT('tip', 'ev', 'sehir', 'Istanbul', 'ilce', 'Kadıköy', 'varsayilan', true),
        JSON_OBJECT('tip', 'is', 'sehir', 'Istanbul', 'ilce', 'Maslak', 'varsayilan', false)
    ),
    'odeme', JSON_OBJECT(
        'varsayilan_yontem', 'kredi_karti',
        'taksit_tercihi', 3
    )
));

-- Tercih güncellemesi - sadece bir alanı değiştir
UPDATE kullanici_tercihleri
SET tercihler = JSON_SET(
    tercihler,
    '$.bildirimler.sms', true,
    '$.gorunum.tema', 'acik'
)
WHERE kullanici_id = 101;

-- Tercih sorgulama
SELECT 
    kullanici_id,
    tercihler->>'$.gorunum.tema' AS tema,
    tercihler->>'$.gorunum.dil' AS dil,
    tercihler->>'$.bildirimler.email' AS email_bildirimi,
    tercihler->>'$.odeme.varsayilan_yontem' AS odeme_yontemi
FROM kullanici_tercihleri
WHERE kullanici_id = 101;

EOF

JSON Güncelleme Fonksiyonları

JSON verisini kısmen güncellemek için farklı fonksiyonlar mevcut:

JSON_SET: Var olan değeri günceller, yoksa ekler JSON_INSERT: Sadece yoksa ekler, varsa dokunmaz JSON_REPLACE: Sadece varsa günceller, yoksa dokunmaz JSON_REMOVE: Belirtilen yolu siler JSON_ARRAY_APPEND: Diziye eleman ekler

mysql -u root -p json_test << 'EOF'

-- Ürüne yeni özellik ekle (ALTER TABLE gerektirmez!)
UPDATE urunler
SET ozellikler = JSON_SET(
    ozellikler,
    '$.stok', 150,
    '$.indirim_orani', 0,
    '$.populer', false
)
WHERE sku = 'LAP-001';

-- Sadece yoksa ekle (mevcut değerleri korur)
UPDATE urunler
SET ozellikler = JSON_INSERT(
    ozellikler,
    '$.stok', 0
)
WHERE JSON_EXTRACT(ozellikler, '$.stok') IS NULL;

-- Diziye yeni eleman ekle
UPDATE urunler
SET ozellikler = JSON_ARRAY_APPEND(
    ozellikler,
    '$.renkler', 'Bordo'
)
WHERE sku = 'TSR-001';

-- Gereksiz alanı kaldır
UPDATE urunler
SET ozellikler = JSON_REMOVE(ozellikler, '$.agirlik')
WHERE sku = 'LAP-001';

-- Toplu güncelleme: Tüm elektronik ürünlere KDV oranı ekle
UPDATE urunler
SET ozellikler = JSON_SET(ozellikler, '$.kdv_orani', 20)
WHERE kategori = 'elektronik';

SELECT sku, ozellikler->>'$.kdv_orani' AS kdv FROM urunler WHERE kategori = 'elektronik';

EOF

JSON Schema Doğrulama (MySQL 8.0.17+)

Üretim ortamında JSON yapısını kontrol altında tutmak için CHECK constraint kullanabilirsiniz:

mysql -u root -p json_test << 'EOF'

-- Elektronik ürünler için zorunlu alan doğrulaması
CREATE TABLE elektronik_urunler (
    id INT AUTO_INCREMENT PRIMARY KEY,
    sku VARCHAR(50) NOT NULL,
    ozellikler JSON NOT NULL,
    -- garanti alanının varlığını zorunlu kıl
    CONSTRAINT chk_garanti CHECK (
        JSON_TYPE(JSON_EXTRACT(ozellikler, '$.garanti')) = 'INTEGER'
        AND JSON_EXTRACT(ozellikler, '$.garanti') > 0
    )
);

-- Bu kayıt başarılı olur
INSERT INTO elektronik_urunler (sku, ozellikler) VALUES (
    'TEST-001',
    JSON_OBJECT('garanti', 24, 'marka', 'TestBrand')
);

-- Bu kayıt hata verir (garanti eksik)
INSERT INTO elektronik_urunler (sku, ozellikler) VALUES (
    'TEST-002',
    JSON_OBJECT('marka', 'TestBrand')
);

EOF

Performans İpuçları ve İzleme

JSON sorgularını production’a almadan önce mutlaka EXPLAIN ile analiz edin:

mysql -u root -p json_test << 'EOF'

-- Slow query tespiti için önce genel bakış
EXPLAIN FORMAT=JSON 
SELECT sku, ad, ozellikler->>'$.garanti' 
FROM urunler 
WHERE ozellikler->>'$.garanti' >= 12;

-- Index kullanımını doğrula
EXPLAIN 
SELECT sku, ad, garanti_ay 
FROM urunler 
WHERE garanti_ay >= 12;

-- JSON boyutlarını kontrol et
SELECT 
    sku,
    LENGTH(ozellikler) AS json_boyut_bytes,
    JSON_DEPTH(ozellikler) AS json_derinligi,
    JSON_LENGTH(ozellikler) AS ust_seviye_alan_sayisi
FROM urunler
ORDER BY json_boyut_bytes DESC;

EOF

Üretim ortamında JSON kullanırken dikkat edilecek noktalar:

  • Derin iç içe yapılardan kaçının: 3-4 seviyeyi geçmeyin, sorgular karmaşıklaşır
  • Sık sorgulanan alanları indexleyin: Sanal kolon + index kombinasyonu şart
  • JSON boyutunu takip edin: Büyük JSON nesneleri bellek ve I/O sorununa yol açar
  • JSON alanlarını SELECT * ile çekmeyin: Sadece ihtiyacınız olan alanları alın
  • Atomik güncellemeler için transaction kullanın: JSON_SET işlemleri atomik değil

Yedekleme ve Migrasyon Senaryosu

JSON içeren tabloları mysqldump ile yedeklerken özel bir şey yapmanıza gerek yok, ama büyük JSON verileri dump boyutunu artırır:

# Sadece JSON kolonlarını kompakt halde tut
mysqldump -u root -p 
  --single-transaction 
  --compact 
  json_test urunler > urunler_yedek.sql

# JSON yapısını değiştirip aktarmak için
mysql -u root -p json_test << 'EOF'
-- Eski yapıdan yeni yapıya JSON dönüşümü
CREATE TABLE urunler_v2 AS
SELECT 
    id,
    sku,
    ad,
    kategori,
    fiyat,
    JSON_SET(
        ozellikler,
        '$.versiyon', 2,
        '$.guncelleme_tarihi', DATE_FORMAT(NOW(), '%Y-%m-%d')
    ) AS ozellikler
FROM urunler;
EOF

Sonuç

MySQL’in JSON veri tipi, doğru kullanıldığında şema esnekliğini ilişkisel veritabanının gücüyle birleştiriyor. E-ticaret ürün katalogları, kullanıcı tercihleri, log verileri, yapılandırma ayarları gibi değişken yapılı veriler için ideal bir çözüm sunuyor.

Ancak şunu net söylemek gerekiyor: JSON kolonları bir sihir değnegi değil. Her şeyi JSON’a doldurmaya kalkarsanız, birkaç ay sonra sorgulama cehennemiyle karşı karşıya kalırsınız. Temel kural basit: Yapısı sabit ve sık sorgulanan veriler için normal kolon kullanın. Değişken, opsiyonel ve heterojen yapıdaki ekstra veriler için JSON kolonu kullanın.

Üretim ortamında JSON kullanmaya karar verdiyseniz şu adımları atlamayın: Sanal kolonlar oluşturun, kritik alanlara index ekleyin, EXPLAIN ile sorgularınızı analiz edin ve büyük JSON yapılarından kaçının. Bu kurallara uyduğunuzda JSON veri tipi, veritabanı tasarımınıza gerçek bir esneklik katacak ve çok sayıda şema değişikliğinden sizi kurtaracak.

Yorum yapın