MySQL JSON_ARRAY ile Dinamik JSON Üretmek

Selamlar, bu yazımda MySQL tarafında epey işime yarayan bir fonksiyona, JSON_ARRAY() fonksiyonuna bakacağız. Konu basit gibi duruyor ama API cevabı hazırlarken ya da rapor sorgularında bir araya gelen alanları tek bir JSON dizisine paketlerken hayatınızı gerçekten kolaylaştıran bir araç bu. Lafı çok uzatmadan başlayayım.

İşin özü şu: elinizde birkaç değer var, bunları virgülle birleştirilmiş bir string yerine düzgün, tip-bilinçli bir JSON dizisi olarak istiyorsunuz. Eskiden GROUP_CONCAT ile uğraşıp tırnak kaçırma derdine düşüyorduk, MySQL 5.7.8'den beri JSON_ARRAY() bu işi tertemiz hallediyor.

JSON_ARRAY() nedir?

JSON_ARRAY(val1, val2, ..., valN) çağrısı, kendisine verdiğiniz argümanları sırayla bir JSON dizisine yerleştirip döndürür. Argümansız çağrılırsa boş dizi yani [] üretir. Bu kadar.

Hadi minik bir örnek görelim:

SELECT JSON_ARRAY(1, 'iki', NULL, TRUE, 3.14);
-- Sonuc: [1, "iki", null, true, 3.14]

Burada dikkat edilecek şey: integer integer kalıyor, string JSON string'ine dönüyor, SQL NULL değeri JSON'un null literali oluyor. Yani tip dönüşümünü siz uğraşmadan motor halletmiş oluyor. Bu, elle string birleştirip '[' || ... || ']' yazdığımız günlerin bittiği yer.

Sütun verileriyle dinamik diziler

İşin asıl tadı, satır verilerini diziye paketlerken çıkıyor. Diyelim kullanıcıların iletişim bilgilerini API'ye tek alan olarak yollamak istiyorsunuz:

SELECT
  id,
  JSON_ARRAY(first_name, last_name, email) AS iletisim
FROM users
WHERE active = 1;

Backend tarafında bu kolonu olduğu gibi parse edip kullanabilirsiniz, ekstra ayıklama yok.

GROUP_CONCAT yerine JSON_ARRAYAGG

Sıkça karşılaşılan ihtiyaç şu: bir kullanıcının siparişlerini tek bir alanda toplayalım. Önce klasik yaklaşım:

SELECT user_id, GROUP_CONCAT(order_id) AS siparisler
FROM orders
GROUP BY user_id;

Bu size '101,102,103' gibi bir string verir. Sonra uygulamada split çekersiniz, sayıya çevirirsiniz, içinde virgül olan bir değer varsa bozulursunuz. Bence artık bunu bırakma vakti:

SELECT
  user_id,
  JSON_ARRAYAGG(order_id) AS siparisler
FROM orders
GROUP BY user_id;

Çıktı: [101, 102, 103]. Tipi sayısal, parse'ı yok, kaçırılması gereken karakter yok. Açıkçası GROUP_CONCAT'in separator ayarıyla uğraşmaktan çok daha temiz duruyor.

JSON_OBJECT ile birleşik kullanım

Asıl güç, JSON_ARRAY() ile JSON_OBJECT() iç içe geçtiğinde ortaya çıkıyor. Bir takım listesi düşünün:

SELECT JSON_ARRAYAGG(
  JSON_OBJECT(
    'id',   id,
    'isim', name,
    'rol',  role
  )
) AS takim
FROM members
WHERE team_id = 7;

Tek sorgudan tertemiz bir dizi-içinde-nesne çıkıyor. Frontend'e direkt gönderilebilecek halde. Bu deseni özellikle dashboard endpoint'lerinde sıkça kullanıyorum.

Indeksleme tarafındaki tuzaklar

Şimdi gel gör ki, JSON tipi MySQL'de indekslenmiyor. Yani tags JSON kolonunuzda JSON_CONTAINS(tags, '"elektronik"') ile filtre çekerseniz, planner her satırı tarar. Küçük tablolarda fark etmez ama veri büyüdükçe canınız yanar.

Çözüm: generated column + üzerine normal indeks. Şöyle ki:

ALTER TABLE products
  ADD COLUMN first_tag VARCHAR(64)
    GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(tags, '$[0]'))) STORED,
  ADD INDEX idx_first_tag (first_tag);

Artık ilk etiket üzerinden yapılan sorgular indeksten faydalanır. Çoklu değer aramalarında ise MySQL 8.0.17 ile gelen multi-valued index var, ona ayrı bir yazıda bakarız.

Sık karşılaşılan tuzaklar

  • JSON_ARRAY() ile JSON_ARRAYAGG()'i karıştırmak: Birincisi argümanlarınızı tek satırda paketler, ikincisi GROUP BY ile çoklu satırı toplar. Yanlış olanı seçtiğinizde ya tek elemanlı dizi ya da N tane satır üretirsiniz.
  • JSON kolonunu olduğu gibi indekslemeye çalışmak: CREATE INDEX ... ON tbl(json_col) çalışmaz. Generated column'a ya da fonksiyonel indekse ihtiyacınız var.
  • Tarih alanlarını farkında olmadan string'e atmak: NOW() JSON içinde string olur. Tarafları bu konuda hizalamazsanız client tarafında parse hatası alırsınız.
  • NULL ile JSON null'u eşit sanmak: SQL NULL'u JSON dizisinde null literali olarak görünür ama dış tarafta IS NULL kontrolü ile yakalayamazsınız.

Kapanış

JSON_ARRAY() küçük ama hayat kurtaran bir araç; özellikle JSON_OBJECT ve JSON_ARRAYAGG ile birleşince elle JSON elle tutuşturma çağı kapanıyor. Bence yeni yazdığınız endpoint'lerde GROUP_CONCAT'i tamamen rafa kaldırın, indeks ihtiyacı olan kolonları ise generated column'la besleyin. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.