Veritabanı Yönetimi

    MySQL JSON Kolonları Kullanımı

    JSON kolonlarını doğru kullanma, sorgulama ve generated column ile indeksleme rehberi.

    10 dk okuma Güncellendi: 25 Ağustos 2026

    MySQL JSON kolonları, ilişkisel bir şemanın içine esnek yapıda veri koymanın resmi yoludur. Ürün özellikleri, kullanıcı tercihleri, üçüncü parti API yanıtları, olay kayıtları — her biri farklı alanlara sahip olabilen ve şemayı sürekli değiştirmek istemediğin veriler için biçilmiş kaftan gibi görünür. Doğru kullanıldığında gerçekten öyledir; yanlış kullanıldığında ise tablo hızla sorgulanamaz bir metin çöplüğüne döner.

    Bu yazıda JSON kolon tipinin TEXT içine JSON yazmaktan farkını, verinin nasıl sorgulanacağını, en kritik konu olan indekslemeyi (çünkü JSON kolonuna doğrudan index açılamaz), dizileri satıra çeviren JSON_TABLE fonksiyonunu ve "bunu JSON'da mı tutmalıyım yoksa normal kolon mu açmalıyım" sorusunun cevabını anlatacağım. Örnekler MySQL 8.0 üzerinden; MariaDB'nin farklı davrandığı yerleri ayrıca işaretleyeceğim.

    JSON Tipi ile TEXT İçine JSON Yazmak Arasındaki Fark#

    JSON kolonuna yazılan veri, düz metin olarak değil ayrıştırılmış ikili (binary) biçimde saklanır. Bu üç somut fayda getirir: yazma anında doğrulama yapılır, okuma sırasında yeniden ayrıştırma maliyeti ortadan kalkar ve bir alana erişmek için tüm belgeyi taramak gerekmez.

    CREATE TABLE urun (
      id       INT UNSIGNED NOT NULL AUTO_INCREMENT,
      ad       VARCHAR(200) NOT NULL,
      ozellik  JSON NULL,               -- ikili biçimde saklanır, doğrulanır
      PRIMARY KEY (id)
    ) ENGINE=InnoDB;
    
    -- Geçersiz JSON kabul edilmez
    INSERT INTO urun (ad, ozellik) VALUES ('SSD 1TB', '{bozuk json');
    -- ERROR 3140: Invalid JSON text
    

    TEXT kolonda böyle bir koruma yoktur; uygulama hatalı bir dize yazarsa hata aylar sonra okuma sırasında ortaya çıkar. Ayrıca JSON tipinde anahtarlar sıralanır ve tekrarlananlar temizlenir, bu yüzden yazdığın belge ile okuduğun birebir aynı sırada olmayabilir — bu normaldir ve veri kaybı değildir.

    Bir kısıt önemlidir: JSON kolonu için varsayılan değer tanımlanamaz (MySQL 8.0.13 öncesinde hiç, sonrasında yalnızca ifade biçiminde). Ayrıca JSON kolonuna doğrudan UNIQUE veya normal index açamazsın; bunun çözümünü ayrı bir bölümde ele alacağım.

    -- Örnek veri
    INSERT INTO urun (ad, ozellik) VALUES
    ('NVMe SSD 1TB', '{"marka":"Kingston","kapasite_gb":1000,"arayuz":"NVMe","etiket":["ssd","hizli"]}'),
    ('DDR4 16GB',    '{"marka":"Corsair","kapasite_gb":16,"hiz_mhz":3200,"etiket":["ram"]}');
    

    Veriyi Sorgulamak: Ok Operatörleri ve Fonksiyonlar#

    JSON içindeki bir değere erişmek için yol (path) ifadesi kullanılır. $ belgenin kökünü, .anahtar bir nesne alanını, [0] bir dizi elemanını temsil eder. MySQL iki kısayol operatörü sunar ve aralarındaki fark sürekli hata kaynağıdır:

    -- -> JSON değer döner (dizeler tırnaklı gelir)
    SELECT ad, ozellik->'$.marka' FROM urun;
    -- "Kingston"
    
    -- ->> tırnakları söker, düz metin döner  (JSON_UNQUOTE(JSON_EXTRACT(...)) kısayolu)
    SELECT ad, ozellik->>'$.marka' FROM urun;
    -- Kingston
    
    -- Karşılaştırmada ->> kullan, yoksa eşleşme tutmaz
    SELECT ad FROM urun WHERE ozellik->>'$.marka' = 'Kingston';
    

    Sık kullanacağın fonksiyonlar şunlardır:

    FonksiyonNe yaparÖrnek
    JSON_EXTRACT(doc, path)Yoldaki değeri JSON olarak dönerJSON_EXTRACT(ozellik,'$.hiz_mhz')
    JSON_UNQUOTE(val)Dizeden tırnakları sökerJSON_UNQUOTE(ozellik->'$.marka')
    JSON_SET(doc, path, val)Varsa günceller, yoksa eklerJSON_SET(ozellik,'$.stok',12)
    JSON_INSERT(doc, path, val)Yalnızca yoksa eklerJSON_INSERT(ozellik,'$.stok',12)
    JSON_REPLACE(doc, path, val)Yalnızca varsa güncellerJSON_REPLACE(ozellik,'$.stok',5)
    JSON_REMOVE(doc, path)Alanı silerJSON_REMOVE(ozellik,'$.eski')
    JSON_CONTAINS(doc, val, path)İçerik kontrolüJSON_CONTAINS(ozellik->'$.etiket','"ssd"')
    JSON_LENGTH(doc, path)Eleman sayısıJSON_LENGTH(ozellik,'$.etiket')
    JSON_KEYS(doc)Anahtar listesiJSON_KEYS(ozellik)

    Kısmi güncelleme yaparken tüm belgeyi yeniden yazmak yerine ilgili fonksiyonu kullanmak önemlidir; MySQL 8 bazı durumlarda yerinde (in-place) kısmi güncelleme yapabilir ve bu hem daha hızlıdır hem ikili günlüğü daha az şişirir:

    -- İyi: yalnızca ilgili alan güncellenir
    UPDATE urun SET ozellik = JSON_SET(ozellik, '$.stok', 12) WHERE id = 1;
    
    -- Kötü: uygulama belgeyi okuyup tamamını geri yazar
    UPDATE urun SET ozellik = '{"marka":"Kingston", ... , "stok":12}' WHERE id = 1;
    

    JSON Kolonunu Indekslemek: Generated Column Yöntemi#

    En kritik bölüm burasıdır. JSON kolonuna doğrudan index açılamaz. Bir JSON alanına göre filtreleme yapıyorsan ve index yoksa, her sorgu tabloyu baştan sona tarar ve her satır için JSON'u ayrıştırır. Çözüm, ilgili alanı sanal bir kolona çıkarıp o kolona index açmaktır.

    -- Sanal (VIRTUAL) kolon: diskte yer kaplamaz, okuma anında hesaplanır
    ALTER TABLE urun
      ADD COLUMN marka VARCHAR(100)
        AS (ozellik->>'$.marka') VIRTUAL,
      ADD KEY ix_marka (marka);
    
    -- Artık bu sorgu index kullanır
    SELECT id, ad FROM urun WHERE marka = 'Kingston';
    

    VIRTUAL ve STORED arasındaki fark önemlidir:

    VIRTUALSTORED
    Disk kullanımıYok (yalnızca index yer kaplar)Satırda saklanır
    Yazma maliyetiDüşükHer yazmada hesaplanır ve yazılır
    Index açılabilir miEvetEvet
    Ne zaman tercih edilirÇoğu durumdaHesaplama pahalıysa ve sık okunuyorsa

    Neredeyse her zaman VIRTUAL doğru seçimdir; index zaten değeri fiziksel olarak saklar, STORED ile satırda ikinci bir kopya tutmanın anlamı kalmaz.

    Sayısal alanlar için tip seçimine dikkat et; JSON'dan çekilen değer varsayılan olarak metin gibi davranır ve yanlış tip karşılaştırmayı bozar:

    ALTER TABLE urun
      ADD COLUMN kapasite_gb INT UNSIGNED
        AS (CAST(ozellik->>'$.kapasite_gb' AS UNSIGNED)) VIRTUAL,
      ADD KEY ix_kapasite (kapasite_gb);
    
    SELECT ad FROM urun WHERE kapasite_gb >= 500;
    

    Diziler için ise MySQL 8.0.17 ve sonrasında çoklu değer (multi-valued) index kullanabilirsin; tek satırdaki bir dizinin her elemanı için ayrı index girdisi oluşturur:

    ALTER TABLE urun
      ADD KEY ix_etiket ( (CAST(ozellik->'$.etiket' AS CHAR(50) ARRAY)) );
    
    -- MEMBER OF ile arama index kullanır
    SELECT ad FROM urun WHERE 'ssd' MEMBER OF (ozellik->'$.etiket');
    

    Index'in gerçekten devreye girip girmediğini her zaman doğrulaman gerekir; EXPLAIN çıktısında key sütununun boş olmadığını görmelisin. Plan okumanın ayrıntısı MySQL EXPLAIN ile sorgu analizi yazısında, index seçim kriterleri ise SQL index tasarımı yazısında.

    JSON_TABLE ile Diziyi Satıra Çevirmek#

    Bazen JSON içindeki bir diziyi normal satırlarmış gibi işlemen gerekir: gruplamak, birleştirmek, filtrelemek. JSON_TABLE tam olarak bunu yapar ve JSON'u geçici bir ilişkisel tabloya çevirir.

    -- Her ürünün etiketlerini ayrı satırlar hâline getir
    SELECT u.id, u.ad, e.etiket
    FROM urun u,
    JSON_TABLE(
      u.ozellik, '$.etiket[*]'
      COLUMNS (etiket VARCHAR(50) PATH '$')
    ) AS e;
    
    -- Etiket bazında ürün sayısı
    SELECT e.etiket, COUNT(*) AS adet
    FROM urun u,
    JSON_TABLE(u.ozellik, '$.etiket[*]' COLUMNS (etiket VARCHAR(50) PATH '$')) AS e
    GROUP BY e.etiket
    ORDER BY adet DESC;
    

    Daha karmaşık yapılar için NESTED PATH ve sıra numarası üreten FOR ORDINALITY kullanabilirsin:

    SELECT s.id, k.sira, k.urun_kodu, k.adet
    FROM siparis s,
    JSON_TABLE(
      s.kalemler, '$[*]'
      COLUMNS (
        sira      FOR ORDINALITY,
        urun_kodu VARCHAR(50) PATH '$.kod',
        adet      INT         PATH '$.adet' DEFAULT '0' ON EMPTY
      )
    ) AS k;
    

    JSON_TABLE güçlüdür ama ucuz değildir: her satır için JSON ayrıştırılır ve index kullanılamaz. Küçük ve orta ölçekli kümelerde harika, milyonlarca satırlık tabloda tam tarama demektir. MariaDB tarafında JSON_TABLE desteği 10.6 ile geldiği için daha eski sürümlerde bu yaklaşım kullanılamaz.

    Ne Zaman JSON, Ne Zaman Normal Kolon#

    Bu sorunun cevabı JSON kullanımının başarısını belirler. Basit ölçüt şudur: veriyi düzenli olarak filtreliyor, sıralıyor veya birleştiriyorsan normal kolon aç. JSON, "sakla ve bütün olarak geri ver" verisi içindir.

    VeriNerede saklanmalıGerekçe
    Sipariş tutarı, durum, tarihNormal kolonSürekli filtrelenir, index gerekir, tip güvenliği şart
    Ürün kategorisine göre değişen teknik özelliklerJSONHer kategoride farklı alanlar; şema patlaması olurdu
    Kullanıcı arayüz tercihleriJSONTek kullanıcı için okunur, sorgulanmaz
    Üçüncü parti API ham yanıtıJSONDenetim izi; yapısı bizim kontrolümüzde değil
    E-posta adresi, telefonNormal kolonTekillik ve doğrulama gerekir
    Nadiren kullanılan ama filtre gereken alanJSON + generated columnİkisinin ortası

    JSON'a taşımanın gizli maliyetlerini de bil. Birincisi, veritabanı tip kontrolü yapmaz: "kapasite_gb": "1000" ile "kapasite_gb": 1000 farklı şeylerdir ve uygulama bu tutarsızlığı sessizce üretir. İkincisi, yabancı anahtar kısıtı koyamazsın; ilişkisel bütünlük tamamen uygulamanın sorumluluğuna geçer. Üçüncüsü, JSON kolonları satırı büyütür ve büyük belgeler satır dışı (off-page) saklanmaya başlayınca okuma maliyeti artar.

    Aynı esneklik ihtiyacı için belge veritabanı da bir seçenektir; ilişkisel ve belge modellerinin farkını değerlendirirken MySQL ve PostgreSQL karşılaştırması yazısı da işine yarayabilir, çünkü PostgreSQL'in jsonb tipi doğrudan indekslenebilir ve bu iki sistem arasındaki en belirgin farklardan biridir.

    Sık Yapılan Hatalar#

    Her şeyi tek bir JSON kolonuna doldurmak. Tabloda veri JSON diye tek kolon olması, ilişkisel veritabanını anahtar-değer deposu gibi kullanmaktır. Bu tasarım ilk üç ay hızlı ilerletir, sonrasında her rapor sorgusunu tam taramaya çevirir.

    -> yerine ->> kullanmayı unutmak. Karşılaştırmada -> kullanırsan değer tırnaklı JSON dizesi olarak gelir ve = 'Kingston' eşleşmez. Bu, "sorgu boş dönüyor ama veri orada" şikâyetinin bir numaralı sebebidir.

    Generated column açmadan filtrelemek. JSON alanına göre WHERE yazıp index açmamak, tabloyu her sorguda baştan sona ayrıştırmak demektir. Küçük tabloda fark edilmez, büyüdüğünde sunucuyu boğar.

    Belgeyi uygulamada okuyup tamamını geri yazmak. Bu hem yarış koşulu (race condition) yaratır — iki eşzamanlı güncelleme birbirini ezer — hem de ikili günlüğü gereksiz yere şişirir. JSON_SET ve JSON_REMOVE ile hedefli güncelleme yap.

    Çok büyük belgeler saklamak. JSON kolonu LONGTEXT sınırlarına kadar veri alabilir ama megabaytlık belgeler satır okuma maliyetini ve replikasyon trafiğini ciddi biçimde artırır. Büyük yapılar için ayrı bir tablo ya da nesne depolama daha doğrudur.

    -- Belge boyutlarını kontrol et: ortalama ve en büyük
    SELECT ROUND(AVG(JSON_LENGTH(ozellik))) AS ort_alan,
           ROUND(AVG(LENGTH(ozellik)))      AS ort_bayt,
           MAX(LENGTH(ozellik))             AS max_bayt
    FROM urun;
    

    Sıkça Sorulan Sorular#

    JSON kolonuna doğrudan index açabilir miyim#

    Hayır, MySQL JSON tipindeki bir kolona doğrudan index oluşturmana izin vermez. Standart çözüm, ilgili alanı VIRTUAL bir generated column'a çıkarıp o kolona index açmaktır. Diziler için MySQL 8.0.17 ve sonrasında çoklu değer index'i kullanabilir ve MEMBER OF operatörüyle indeksli arama yapabilirsin. Her iki durumda da index'in gerçekten kullanıldığını EXPLAIN çıktısıyla doğrulamalısın.

    JSON kolonu TEXT kolondan daha mı hızlı#

    Okuma tarafında genellikle evet, çünkü veri ayrıştırılmış ikili biçimde saklanır ve bir alana erişmek için tüm belgeyi yeniden ayrıştırmak gerekmez. Yazma tarafında ise JSON tipi doğrulama yaptığı için çok küçük bir ek maliyet vardır. Asıl kazanç hızdan çok veri bütünlüğüdür: geçersiz JSON tabloya hiç giremez, bu da hataların aylar sonra değil yazma anında yakalanmasını sağlar.

    MariaDB'de JSON tipi MySQL ile aynı mı çalışır#

    Hayır, önemli bir fark var. MariaDB'de JSON aslında LONGTEXT üzerine bir takma addır ve bir CHECK kısıtı ile doğrulama yapılır; ikili saklama yoktur. Fonksiyonların büyük kısmı ortaktır ama performans karakteristiği farklıdır ve JSON_TABLE yalnızca 10.6 ve sonrasında bulunur. İki sistem arasında taşıma yapıyorsan JSON kullanan sorguları mutlaka test etmelisin.

    JSON alanına göre sıralama yapabilir miyim#

    Teknik olarak evet, ORDER BY ozellik->>'$.fiyat' çalışır. Ancak bu ifade index kullanamaz, dolayısıyla sonuç kümesi tamamen belleğe alınıp sıralanır ve büyük tablolarda çok pahalıdır. Ayrıca metin olarak sıralanacağı için "100" değeri "20" değerinden önce gelir. Doğru yöntem, alanı uygun tiple bir generated column'a çıkarıp o kolona index açmaktır.

    JSON belgesinin tamamını mı yoksa tek alanı mı güncellemeliyim#

    Mümkün olduğunca tek alanı güncelle: JSON_SET, JSON_REPLACE ve JSON_REMOVE fonksiyonları hedefli değişiklik yapar. Belgenin tamamını uygulamada okuyup geri yazmak iki soruna yol açar; eşzamanlı iki güncelleme birbirini ezebilir ve ikili günlüğe her seferinde tüm belge yazıldığı için replikasyon trafiği gereksiz yere artar.

    JSON kolonunda NULL ile JSON null aynı şey mi#

    Hayır ve bu ayrım kafa karıştırıcıdır. Kolonun kendisinin SQL NULL olması "değer yok" demektir; belge içindeki null ise geçerli bir JSON değeridir. ozellik IS NULL ile ozellik->'$.alan' = CAST('null' AS JSON) tamamen farklı sorgulardır. Uygulamada hangisini kastettiğini net tutmalısın, aksi hâlde eksik veri ile bilinçli olarak boş bırakılmış veri birbirine karışır.

    Ne kadar büyük JSON belgeleri saklayabilirim#

    Teknik sınır çok yüksektir (max_allowed_packet ve LONGTEXT sınırları içinde), ama pratik sınır çok daha düşüktür. Birkaç kilobaytlık belgeler sorunsuzdur; yüzlerce kilobayta çıkınca satır okuma maliyeti, bellek kullanımı ve replikasyon trafiği hissedilir biçimde artar. Belgeler büyüyorsa yapıyı ayrı bir ilişkisel tabloya bölmeyi ya da gerçekten büyük içerikleri nesne depolamaya taşımayı değerlendirmelisin.

    Kapanış#

    JSON kolonlarından iyi sonuç almak birkaç net kurala bağlı: filtrelediğin her alanı generated column'a çıkarıp index aç, karşılaştırmalarda -> değil ->> kullan, belgeyi tamamen yeniden yazmak yerine JSON_SET ile hedefli güncelle ve düzenli olarak sorguladığın veriyi JSON'da tutmakta ısrar etme — o veri normal kolonu hak ediyordur. JSON'u şemanın yerine değil, şemanın kenarındaki esneklik alanı olarak düşün.

    Esnek şemalı ve büyüyen veri kümeleri disk ve bellek tarafında hızla baskı yaratır. Veritabanını NVMe diskli, kaynağı garanti edilmiş bir makinede çalıştırmak istersen VDS ve bulut sunucu paketlerimiz uygun bir zemin sunar; JSON çıktılarıyla uğraşırken JSON formatlayıcı aracımız da günlük işini kolaylaştırır. Kurulum, ayar ve yedekleme tarafını bize bırakmak isterseniz sunucu yönetimi hizmetimiz bu işi üstlenir.

    MySQLJSONŞema Tasarımı

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.