MariaDB ve MySQL’de JSON Veri Tipi ve JSON_EXTRACT Kullanımı

Modern web uygulamaları ve mikroservis mimarileri dünyasında, yapılandırılmamış veya yarı yapılandırılmış verileri ilişkisel veritabanlarında saklamak giderek daha yaygın bir ihtiyaç haline geldi. MySQL 5.7 ve MariaDB 10.2 sürümleriyle birlikte gelen JSON veri tipi, bu ihtiyaca ciddi bir çözüm sunuyor. Artık uygulama katmanında JSON parse etmek yerine, doğrudan SQL sorguları içinde JSON verisiyle çalışabiliyoruz. Bu yazıda JSON veri tipinin nasıl kullanıldığını, JSON_EXTRACT ve diğer JSON fonksiyonlarını gerçek dünya senaryolarıyla birlikte ele alacağız.

JSON Veri Tipi Nedir ve Neden Kullanmalıyız?

Geleneksel yaklaşımda JSON verisini TEXT veya LONGTEXT sütunlarında saklayabiliyorduk. Ancak bu yöntemin ciddi dezavantajları vardı: veritabanı JSON formatını doğrulamıyor, içerik üzerinde indeks oluşturamıyor ve sorgu performansı oldukça düşük kalıyordu.

JSON veri tipi ise şu avantajları getiriyor:

  • Otomatik format doğrulama: Geçersiz JSON girilmeye çalışıldığında veritabanı hata fırlatır
  • Dahili optimizasyon: JSON verisi binary formatta saklanır, tekrar parse edilmesi gerekmez
  • Kısmi güncelleme desteği: Tüm JSON’u yeniden yazmak yerine sadece belirli alanları güncelleyebilirsiniz
  • Fonksiyon ekosistemi: JSON_EXTRACT, JSON_SET, JSON_ARRAY, JSON_OBJECT gibi onlarca fonksiyon kullanıma hazır
  • Sanal sütun ve indeks: JSON içindeki belirli alanlara indeks eklenebilir

Tabi her çözümün bir bedeli var. JSON sütunları şema esnekliği sağlarken, çok karmaşık sorgularda performans sorunlarına yol açabilir. Doğru kullanım senaryosunu seçmek önemli.

Temel Tablo Yapısı ve JSON Sütunu Oluşturma

Bir e-ticaret senaryosu üzerinden gidelim. Ürünlerimizin kategori bazında farklı özellikleri olduğunu düşünelim: elektronik ürünlerin garantisi, rengi ve teknik özellikleri varken, giyim ürünlerinin bedeni ve kumaş bilgisi var. Bu tür esnek veri yapıları için JSON sütunu biçilmiş kaftan.

CREATE TABLE urunler (
    id INT AUTO_INCREMENT PRIMARY KEY,
    urun_adi VARCHAR(255) NOT NULL,
    fiyat DECIMAL(10, 2) NOT NULL,
    kategori VARCHAR(100) NOT NULL,
    ozellikler JSON,
    stok_bilgisi JSON,
    olusturma_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    guncelleme_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Şimdi bu tabloya birkaç kayıt ekleyelim:

INSERT INTO urunler (urun_adi, fiyat, kategori, ozellikler, stok_bilgisi) VALUES
(
    'Samsung Galaxy S23',
    24999.99,
    'elektronik',
    '{"renk": "siyah", "depolama": "256GB", "ram": "8GB", "garanti": 24, "ekran": "6.1 inc", "kamera": {"on": "12MP", "arka": "50MP"}}',
    '{"toplam": 150, "depo": {"istanbul": 80, "ankara": 45, "izmir": 25}}'
),
(
    'Levi s 501 Jean',
    899.00,
    'giyim',
    '{"beden": "32x32", "renk": "mavi", "kumaş": "pamuk", "cinsiyet": "erkek", "yikama": "30 derece"}',
    '{"toplam": 320, "depo": {"istanbul": 120, "ankara": 100, "izmir": 100}}'
),
(
    'MacBook Pro M3',
    89999.00,
    'elektronik',
    '{"renk": "uzay grisi", "depolama": "512GB", "ram": "16GB", "garanti": 12, "ekran": "14 inc", "islemci": "Apple M3"}',
    '{"toplam": 45, "depo": {"istanbul": 30, "ankara": 10, "izmir": 5}}'
),
(
    'Nike Air Max 270',
    3299.00,
    'ayakkabi',
    '{"numara": 42, "renk": "beyaz", "materyal": "mesh", "cinsiyet": "unisex", "sezon": ["ilkbahar", "yaz"]}',
    '{"toplam": 200, "depo": {"istanbul": 90, "ankara": 70, "izmir": 40}}'
);

JSON_EXTRACT Fonksiyonu ile Veri Okuma

JSON_EXTRACT, JSON verisi içinden belirli bir değeri çekmenin temel yoludur. Sözdizimi oldukça sezgisel: JSON_EXTRACT(json_sutunu, 'yol_ifadesi').

Yol ifadeleri $ işaretiyle başlar ve nokta notasyonu ya da köşeli parantez kullanır:

  • $.alan_adi: Doğrudan bir alanı seçer
  • $.ust.alt: İç içe geçmiş alanları seçer
  • $.dizi[0]: Dizi elemanlarına erişir
  • $[*]: Tüm dizi elemanlarını seçer
-- Temel JSON_EXTRACT kullanımı
SELECT 
    urun_adi,
    JSON_EXTRACT(ozellikler, '$.renk') AS renk,
    JSON_EXTRACT(ozellikler, '$.garanti') AS garanti_ay,
    JSON_EXTRACT(stok_bilgisi, '$.toplam') AS toplam_stok
FROM urunler
WHERE kategori = 'elektronik';

MySQL ve MariaDB aynı zamanda -> operatörünü kısayol olarak sunuyor. Bu operatör JSON_EXTRACT ile tamamen eşdeğer:

-- -> operatörü ile aynı sonuç
SELECT 
    urun_adi,
    ozellikler->'$.renk' AS renk,
    ozellikler->'$.garanti' AS garanti_ay,
    stok_bilgisi->'$.toplam' AS toplam_stok
FROM urunler
WHERE kategori = 'elektronik';

Burada dikkat etmeniz gereken bir nokta var: JSON_EXTRACT ve -> operatörü string değerleri tırnak işaretleriyle döndürür. Örneğin renk alanı "siyah" şeklinde gelir. Eğer tırnaksız değer istiyorsanız ->> operatörünü ya da JSON_UNQUOTE fonksiyonunu kullanmalısınız:

-- Tırnak işaretleri olmadan değer alma
SELECT 
    urun_adi,
    ozellikler->>'$.renk' AS renk,
    JSON_UNQUOTE(JSON_EXTRACT(ozellikler, '$.renk')) AS renk_v2
FROM urunler;

İç İçe Geçmiş JSON Verilerine Erişim

Gerçek dünya verilerinde JSON genellikle çok katmanlı yapıda olur. Samsung telefon örneğimizde kamera bilgisi iç içe geçmiş bir nesne olarak tutuluyordu:

-- İç içe geçmiş nesneye erişim
SELECT 
    urun_adi,
    ozellikler->>'$.kamera.on' AS on_kamera,
    ozellikler->>'$.kamera.arka' AS arka_kamera,
    stok_bilgisi->>'$.depo.istanbul' AS istanbul_stok,
    stok_bilgisi->>'$.depo.ankara' AS ankara_stok
FROM urunler
WHERE urun_adi LIKE '%Samsung%';

Dizi değerlerine erişmek de oldukça kolay. Nike ayakkabı örneğimizde sezon bilgisi dizi olarak tutuluyordu:

-- Dizi elemanlarına erişim
SELECT 
    urun_adi,
    ozellikler->>'$.sezon[0]' AS birinci_sezon,
    ozellikler->>'$.sezon[1]' AS ikinci_sezon,
    JSON_LENGTH(ozellikler, '$.sezon') AS sezon_sayisi
FROM urunler
WHERE JSON_CONTAINS_PATH(ozellikler, 'one', '$.sezon') = 1;

WHERE Koşullarında JSON Filtreleme

JSON alanlarını sadece SELECT’te değil, WHERE koşullarında da kullanabilirsiniz. Bu, gerçek anlamda güçlü bir özellik:

-- JSON alanına göre filtreleme
SELECT urun_adi, fiyat, ozellikler->>'$.renk' AS renk
FROM urunler
WHERE ozellikler->>'$.renk' = 'siyah';

-- Sayısal karşılaştırma ile filtreleme
SELECT urun_adi, fiyat
FROM urunler
WHERE CAST(ozellikler->>'$.garanti' AS UNSIGNED) >= 24;

-- Stok durumuna göre filtreleme
SELECT 
    urun_adi,
    stok_bilgisi->>'$.toplam' AS toplam_stok
FROM urunler
WHERE CAST(stok_bilgisi->>'$.toplam' AS UNSIGNED) < 100;

JSON_CONTAINS fonksiyonu ise JSON içinde belirli bir değerin var olup olmadığını kontrol eder:

-- JSON_CONTAINS ile değer arama
SELECT urun_adi
FROM urunler
WHERE JSON_CONTAINS(ozellikler, '"ilkbahar"', '$.sezon');

-- Birden fazla koşul
SELECT urun_adi, fiyat
FROM urunler
WHERE JSON_CONTAINS(ozellikler, '{"cinsiyet": "unisex"}');

JSON Verisini Güncelleme

Sadece okumak değil, JSON verisini güncellemek de sıkça yapılan bir işlem. JSON_SET, JSON_INSERT, JSON_REPLACE ve JSON_REMOVE fonksiyonları bu iş için kullanılır.

  • JSON_SET: Değer varsa günceller, yoksa ekler
  • JSON_INSERT: Sadece yeni değer ekler, varsa dokunmaz
  • JSON_REPLACE: Sadece var olan değeri günceller, yoksa ekleme yapmaz
  • JSON_REMOVE: Belirtilen path’i siler
-- Garanti süresini güncelle
UPDATE urunler
SET ozellikler = JSON_SET(ozellikler, '$.garanti', 36)
WHERE urun_adi LIKE '%MacBook%';

-- Yeni bir alan ekle
UPDATE urunler
SET ozellikler = JSON_SET(ozellikler, '$.indirim_orani', 10)
WHERE kategori = 'elektronik';

-- İstanbul stokunu güncelle
UPDATE urunler
SET stok_bilgisi = JSON_SET(
    stok_bilgisi, 
    '$.depo.istanbul', 
    CAST(stok_bilgisi->>'$.depo.istanbul' AS UNSIGNED) - 5
)
WHERE urun_adi LIKE '%Samsung%';

-- Gereksiz alanı sil
UPDATE urunler
SET ozellikler = JSON_REMOVE(ozellikler, '$.indirim_orani')
WHERE kategori = 'giyim';

Sanal Sütunlar ve JSON İndeksleme

JSON sütununa doğrudan indeks ekleyemezsiniz, ancak generated column (sanal sütun) aracılığıyla belirli JSON alanlarını indeksleyebilirsiniz. Bu, sık sorgulanan JSON alanları için performansı dramatik biçimde artırır.

-- Mevcut tabloya sanal sütun ve indeks ekleme
ALTER TABLE urunler
ADD COLUMN renk_virtual VARCHAR(50) 
    GENERATED ALWAYS AS (ozellikler->>'$.renk') VIRTUAL,
ADD INDEX idx_renk (renk_virtual);

-- Garanti süresi için sanal sütun
ALTER TABLE urunler
ADD COLUMN garanti_virtual INT 
    GENERATED ALWAYS AS (CAST(ozellikler->>'$.garanti' AS UNSIGNED)) STORED,
ADD INDEX idx_garanti (garanti_virtual);

-- İndeks kullanımını doğrulama
EXPLAIN SELECT urun_adi, fiyat
FROM urunler
WHERE renk_virtual = 'siyah';

VIRTUAL sanal sütunlar disk üzerinde yer kaplamaz, sorgu sırasında hesaplanır. STORED ise disk üzerinde saklanır ve daha hızlı okuma sağlar ancak yazma işlemlerini yavaşlatabilir. Hangi türü seçeceğiniz okuma/yazma oranınıza göre değişir.

Gerçek Dünya Senaryosu: Kullanıcı Aktivite Logu

Bir başka yaygın senaryo, kullanıcı aktivitelerini JSON olarak loglamak. Çeşitli tip aktivitelerin farklı metadata içerdiği durumlar için JSON mükemmel uyum sağlar:

CREATE TABLE kullanici_aktivite (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    kullanici_id INT NOT NULL,
    aktivite_tipi ENUM('giris', 'cikis', 'satin_alma', 'yorum', 'profil_guncelleme') NOT NULL,
    aktivite_detay JSON,
    ip_adresi VARCHAR(45),
    islem_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_kullanici (kullanici_id),
    INDEX idx_tarih (islem_tarihi)
);

INSERT INTO kullanici_aktivite (kullanici_id, aktivite_tipi, aktivite_detay, ip_adresi) VALUES
(1001, 'satin_alma', '{"siparis_id": "ORD-2024-001", "tutar": 24999.99, "urun_id": 1, "odeme_yontemi": "kredi_karti"}', '192.168.1.100'),
(1001, 'yorum', '{"urun_id": 1, "puan": 5, "yorum": "Harika bir telefon", "onaylanmis": false}', '192.168.1.100'),
(1002, 'giris', '{"cihaz": "mobile", "tarayici": "Safari", "os": "iOS 17"}', '10.0.0.45'),
(1002, 'profil_guncelleme', '{"degistirilen_alanlar": ["email", "telefon"], "eski_email": "[email protected]"}', '10.0.0.45');

-- Belirli bir kullanıcının satın alma geçmişi
SELECT 
    islem_tarihi,
    aktivite_detay->>'$.siparis_id' AS siparis_no,
    aktivite_detay->>'$.tutar' AS tutar,
    aktivite_detay->>'$.odeme_yontemi' AS odeme
FROM kullanici_aktivite
WHERE kullanici_id = 1001
AND aktivite_tipi = 'satin_alma'
ORDER BY islem_tarihi DESC;

-- Mobil cihazdan giriş yapan kullanıcıları bul
SELECT DISTINCT kullanici_id
FROM kullanici_aktivite
WHERE aktivite_tipi = 'giris'
AND aktivite_detay->>'$.cihaz' = 'mobile';

JSON Aggregation ve Raporlama

JSON fonksiyonlarını GROUP BY ve aggregate fonksiyonlarla birleştirerek güçlü raporlar oluşturabilirsiniz:

-- Kategori bazında ortalama stok raporu
SELECT 
    kategori,
    COUNT(*) AS urun_sayisi,
    AVG(CAST(stok_bilgisi->>'$.toplam' AS UNSIGNED)) AS ort_stok,
    SUM(CAST(stok_bilgisi->>'$.toplam' AS UNSIGNED)) AS toplam_stok
FROM urunler
GROUP BY kategori;

-- JSON_ARRAYAGG ile sonuçları JSON dizisi olarak toplama
SELECT 
    kategori,
    JSON_ARRAYAGG(urun_adi) AS urun_listesi,
    JSON_ARRAYAGG(
        JSON_OBJECT('ad', urun_adi, 'fiyat', fiyat)
    ) AS detayli_liste
FROM urunler
GROUP BY kategori;

-- JSON_OBJECTAGG ile key-value çiftleri oluşturma
SELECT 
    JSON_OBJECTAGG(urun_adi, fiyat) AS fiyat_listesi
FROM urunler
WHERE kategori = 'elektronik';

Performans İpuçları ve Dikkat Edilmesi Gerekenler

Pratik deneyimlerimden derlediğim önemli noktaları paylaşayım:

  • JSON sütunlarını sık filtrelenen alanlarda doğrudan kullanmaktan kaçının: Sanal sütun + indeks kombinasyonu çok daha iyi performans verir
  • JSON boyutunu kontrol altında tutun: Tek bir JSON sütunu için max_allowed_packet sınırını göz önünde bulundurun
  • JSON_VALID ile doğrulama yapın: Harici kaynaklardan gelen verileri eklemeden önce JSON_VALID(veri) kontrolü ekleyin
  • EXPLAIN kullanın: JSON içeren sorgularda mutlaka EXPLAIN ile sorgu planını kontrol edin
  • Normalleşme kararını iyi verin: Sık sorgulanan, ilişki kurulan veriler için normal tablo sütunları hala daha iyi seçenek
-- JSON doğrulama örneği
SELECT JSON_VALID('{"isim": "test", "deger": 123}') AS gecerli;  -- 1 döner
SELECT JSON_VALID('{"isim": "test", deger: 123}') AS gecerli;    -- 0 döner

-- JSON boyutunu kontrol etme
SELECT 
    urun_adi,
    JSON_LENGTH(ozellikler) AS alan_sayisi,
    LENGTH(ozellikler) AS byte_boyutu
FROM urunler;

-- Tüm JSON anahtarlarını listeleme
SELECT 
    urun_adi,
    JSON_KEYS(ozellikler) AS anahtarlar
FROM urunler;

MariaDB ve MySQL Arasındaki Farklar

Bu noktada önemli bir not düşmek gerekiyor. MariaDB teknik olarak JSON veri tipini desteklediğini söylese de, MariaDB 10.2 – 10.5 arasında JSON aslında LONGTEXT için bir takma isimdir. Gerçek binary JSON depolama MariaDB 10.7 ile geldi. Bu durum şu anlama geliyor:

  • MariaDB 10.6 ve öncesinde format doğrulama varsayılan olarak aktif değildir
  • Sorgu fonksiyonları uyumlu olsa da depolama altyapısı farklıdır
  • MariaDB 10.7+ ile JSON_VALUE gibi ek fonksiyonlar geldi
  • Production ortamınızda sürüm kontrolü yaparak hangi özelliklerin desteklendiğini teyit edin
-- Sürüm kontrolü
SELECT VERSION();

-- MariaDB 10.7+ için JSON_VALUE kullanımı
SELECT JSON_VALUE(ozellikler, '$.renk') AS renk
FROM urunler;

Sonuç

JSON veri tipi ve JSON_EXTRACT başta olmak üzere JSON fonksiyonları, ilişkisel veritabanlarına güçlü bir esneklik katıyor. Özellikle farklı entity’lerin farklı özellikler taşıdığı e-ticaret ürünleri, kullanıcı aktivite logları, yapılandırma verileri ve metadata saklama gibi senaryolarda bu özellikler gerçekten hayat kurtarıyor.

Ancak her güçlü araç gibi, JSON sütunlarını da yerinde kullanmak önemli. Eğer bir veri alanı düzenli olarak sorgu koşullarında kullanılacaksa, JOIN yapılacaksa ya da veri tutarlılığı kritikse geleneksel normalize tablo yapısı hala doğru tercih. JSON’ı şema esnekliğinin gerçekten gerektiği yerlerde, sanal sütun ve indeks optimizasyonlarıyla birlikte kullandığınızda hem geliştirici deneyimi hem de sistem performansı açısından kazançlı çıkacaksınız.

Sanal sütun + indeks stratejisini mutlaka uygulayın, EXPLAIN ile sorgu planlarınızı düzenli kontrol edin ve MariaDB ile MySQL arasındaki sürüm farklılıklarını göz önünde bulundurun. Bu temelleri sağlam kurarsanız, JSON veri tipi projelerinizde güvenilir bir silah haline gelecek.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir