Veritabanı Yönetimi

    PostgreSQL JSONB Kullanımı

    JSONB sütunlarını sorgulama, indeksleme ve doğru senaryoda kullanma rehberi.

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

    Şemaya sığmayan verilerle karşılaşmak, ilişkisel veritabanıyla çalışan herkesin başına gelir. Her ürün tipinin farklı özellikleri vardır, her entegrasyon farklı alanlar döner, her olay kaydının içeriği değişir. Bu noktada çoğu ekip iki hatalı yoldan birini seçer: ya on beş boş sütunlu devasa bir tablo kurar, ya da anahtar-değer tablosuna kaçıp her sorguda on birleşim yapar. PostgreSQL JSONB, üçüncü ve genellikle en doğru yolu sunar.

    JSONB, JSON belgelerini ikili (binary) biçimde saklayan bir PostgreSQL veri tipidir. Belgeyi ayrıştırılmış hâlde tuttuğu için içindeki alanlara doğrudan erişebilir, indeksleyebilir ve sorgulayabilirsiniz. Bu rehberde JSONB ile JSON tipi arasındaki farkı, operatörlerini, GIN indeksleriyle nasıl hızlandırılacağını, güncelleme fonksiyonlarını ve — en önemlisi — hangi durumlarda JSONB kullanmamanız gerektiğini anlatacağım.

    JSON ve JSONB Arasındaki Fark#

    PostgreSQL iki JSON tipi sunar ve aralarındaki fark yüzeysel değildir.

    Özellikjsonjsonb
    Saklama biçimiMetin, aynenAyrıştırılmış ikili yapı
    Yazma hızıDaha hızlıAyrıştırma maliyeti var
    Okuma/sorgulamaHer erişimde yeniden ayrıştırırDoğrudan erişir, çok hızlı
    Boşluk ve sıraGirdiğiniz gibi korunurNormalleştirilir, anahtar sırası değişir
    Yinelenen anahtarHepsi saklanırSon değer kalır
    İndekslenebilir miYalnızca ifade indeksiGIN ile tam destek

    Kural basittir: veriyi sorgulayacaksanız jsonb, sadece saklayıp aynen geri vereceğinizde json. Uygulamada json tipinin gerçek kullanım alanı çok dardır (ham API yanıtını birebir arşivlemek gibi), bu yüzden neredeyse her zaman jsonb seçin.

    Farkı somut görelim:

    SELECT '{"b":1, "a":2, "a":3}'::json  AS json_tipi,
           '{"b":1, "a":2, "a":3}'::jsonb AS jsonb_tipi;
    

    json sütunu girdiyi aynen döndürür; jsonb ise {"a": 3, "b": 1} verir — yinelenen anahtarın sonuncusu kalmış, sıra normalleştirilmiştir.

    Tablo Kurmak ve Veri Yazmak#

    Tipik bir kullanım, sabit alanları normal sütunlarda tutup değişken kısmı JSONB'ye koymaktır. Bu melez yaklaşım, saf JSONB'den de saf ilişkiselden de iyidir.

    CREATE TABLE urunler (
        id          bigserial PRIMARY KEY,
        sku         text NOT NULL UNIQUE,
        ad          text NOT NULL,
        fiyat       numeric(12,2) NOT NULL,
        kategori_id int NOT NULL,
        -- Kategoriye göre değişen teknik özellikler
        ozellikler  jsonb NOT NULL DEFAULT '{}'::jsonb,
        olusturulma timestamptz NOT NULL DEFAULT now()
    );
    
    INSERT INTO urunler (sku, ad, fiyat, kategori_id, ozellikler) VALUES
    ('KLV-001', 'Mekanik Klavye', 1450.00, 3,
     '{"switch":"kirmizi","tus_sayisi":104,"kablosuz":false,"renkler":["siyah","beyaz"]}'),
    ('MON-220', '27 inc Monitor', 6200.00, 5,
     '{"panel":"IPS","cozunurluk":"2560x1440","yenileme_hz":165,"portlar":["HDMI","DisplayPort"]}');
    

    NOT NULL DEFAULT '{}' seçimi bilinçlidir: JSONB sütununda NULL ile boş nesne farklı şeylerdir ve NULL ile çalışan sorgular sürprizler üretir. Boş nesneyi varsayılan yapmak, sorgularınızı sadeleştirir.

    Veri yapısını kısıtlamak isterseniz CHECK kısıtı koyabilirsiniz:

    ALTER TABLE urunler ADD CONSTRAINT ozellikler_nesne_olmali
    CHECK (jsonb_typeof(ozellikler) = 'object');
    

    Bu, birinin yanlışlıkla bir dizi ya da düz metin yazmasını engeller. Daha katı doğrulama için bazı kurulumlarda JSON şema eklentileri kullanılır, ama çekirdek PostgreSQL'de tam şema doğrulaması yoktur; bu, JSONB'nin bilinçli bir tasarım tercihidir.

    Sorgulama Operatörleri#

    JSONB operatörlerini karıştırmak en yaygın hata kaynağıdır. Şu tabloyu bir kez oturtun, gerisi kolaydır:

    OperatörNe yaparDöndürdüğü tipÖrnek
    ->Alanı alırjsonbozellikler -> 'switch'
    ->>Alanı alır, metne çevirirtextozellikler ->> 'switch'
    #>Yol ile derin alanjsonbveri #> '{musteri,adres}'
    #>>Yol ile derin alan, metintextveri #>> '{musteri,adres,il}'
    @>İçerir mibooleanozellikler @> '{"kablosuz":true}'
    ?Anahtar var mıbooleanozellikler ? 'panel'
    ?|Anahtarlardan biri var mıbooleanozellikler ?| ARRAY['panel','switch']
    @?JSONPath eşleşmesi var mıbooleanveri @? '$.satirlar[*] ? (@.adet > 5)'

    -> ile ->> arasındaki fark, yeni başlayanların en çok takıldığı yerdir. -> size hâlâ JSONB döndürür, yani metin değeri tırnaklıdır:

    SELECT
        ozellikler -> 'switch'   AS jsonb_olarak,   -- "kirmizi"  (tırnaklı!)
        ozellikler ->> 'switch'  AS metin_olarak    -- kirmizi
    FROM urunler WHERE sku = 'KLV-001';
    

    Bu yüzden karşılaştırmada ->> kullanmalısınız; ozellikler -> 'switch' = 'kirmizi' karşılaştırması tip uyuşmazlığı verir.

    Sayısal alanlarda tür dönüşümü gerekir:

    -- Yenileme hızı 120'den büyük monitörler
    SELECT ad, ozellikler ->> 'panel' AS panel
    FROM urunler
    WHERE (ozellikler ->> 'yenileme_hz')::int > 120;
    

    Dizi alanlarını satırlara açmak için jsonb_array_elements kullanılır:

    -- Her ürünün desteklediği portları ayrı satırlar hâlinde listele
    SELECT u.ad, port
    FROM urunler u,
         LATERAL jsonb_array_elements_text(u.ozellikler -> 'portlar') AS port
    WHERE u.ozellikler ? 'portlar';
    

    jsonb_array_elements_text metin döndürür, jsonb_array_elements ise JSONB. LATERAL sayesinde her satır kendi dizisiyle eşleşir.

    İndeksleme: Asıl Kazanç Burada#

    İndekssiz bir JSONB sütununda arama, her satırın belgesini açıp bakmak demektir; yani tam tablo taraması. İki indeksleme stratejisi vardır ve doğru olanı seçmek büyük fark yaratır.

    Genel amaçlı GIN indeksi, tüm anahtarları ve değerleri kapsar:

    CREATE INDEX idx_urunler_ozellikler ON urunler USING GIN (ozellikler);
    
    -- Bu sorgular indeksi kullanır
    SELECT * FROM urunler WHERE ozellikler @> '{"kablosuz":true}';
    SELECT * FROM urunler WHERE ozellikler ? 'panel';
    

    jsonb_path_ops GIN indeksi ise belirgin biçimde daha küçük ve daha hızlıdır, ama yalnızca @> operatörünü destekler:

    CREATE INDEX idx_urunler_ozellikler_path ON urunler USING GIN (ozellikler jsonb_path_ops);
    

    Yalnızca içerme sorgusu yapıyorsanız jsonb_path_ops seçin; anahtar varlığı sorguları da kullanacaksanız varsayılan sınıfta kalın.

    Üçüncü ve çoğu zaman en verimli seçenek, tek bir alan için ifade indeksi kurmaktır. Sürekli aynı alanı sorguluyorsanız devasa bir GIN yerine küçük bir B-tree yeter:

    CREATE INDEX idx_urunler_panel ON urunler ((ozellikler ->> 'panel'));
    
    -- Bu sorgu artık küçük ve hızlı bir B-tree kullanır
    SELECT * FROM urunler WHERE ozellikler ->> 'panel' = 'IPS';
    

    Sayısal alanlarda tür dönüşümünü indekste de yapın, yoksa kullanılmaz:

    CREATE INDEX idx_urunler_hz ON urunler (((ozellikler ->> 'yenileme_hz')::int));
    

    İndeksin gerçekten kullanıldığını mutlaka doğrulayın:

    EXPLAIN (ANALYZE, BUFFERS)
    SELECT * FROM urunler WHERE ozellikler @> '{"panel":"IPS"}';
    

    Bitmap Index Scan on idx_urunler_ozellikler satırını görmelisiniz; Seq Scan görüyorsanız ya indeks operatörle uyumsuz ya da tablo indeks kullanmaya değmeyecek kadar küçük. İndeks türlerinin genel mantığı için PostgreSQL index türleri yazısına bakın.

    Güncelleme ve Değiştirme#

    JSONB belgesinin bir parçasını güncellemek için jsonb_set kullanılır. Dikkat: tüm belge yeniden yazılır, bu yüzden büyük belgelerde sık güncelleme pahalıdır.

    -- Tek bir alanı güncelle
    UPDATE urunler
    SET ozellikler = jsonb_set(ozellikler, '{yenileme_hz}', '180'::jsonb)
    WHERE sku = 'MON-220';
    
    -- Olmayan bir yolu oluşturmak için son parametre true olmalı
    UPDATE urunler
    SET ozellikler = jsonb_set(ozellikler, '{garanti_yil}', '2'::jsonb, true)
    WHERE sku = 'KLV-001';
    

    Birleştirme operatörü || daha pratiktir; yüzeysel birleştirme yapar:

    UPDATE urunler
    SET ozellikler = ozellikler || '{"stok_durumu":"var","kargo_gun":2}'::jsonb
    WHERE kategori_id = 5;
    

    Anahtar silmek için - operatörü:

    UPDATE urunler SET ozellikler = ozellikler - 'kargo_gun' WHERE kategori_id = 5;
    -- Birden fazla anahtar
    UPDATE urunler SET ozellikler = ozellikler - ARRAY['stok_durumu','kargo_gun'];
    

    Kritik bir uyarı: || derin birleştirme yapmaz. İç içe bir nesnenin tek alanını değiştirmek isterseniz, || tüm iç nesneyi yenisiyle değiştirir ve diğer alanları siler. Derin güncelleme için jsonb_set ile tam yolu vermelisiniz.

    JSONB'yi ilişkisel bir görünüme çevirmek için jsonb_to_record çok kullanışlıdır:

    SELECT u.ad, o.panel, o.yenileme_hz
    FROM urunler u,
         jsonb_to_record(u.ozellikler) AS o(panel text, yenileme_hz int)
    WHERE u.kategori_id = 5;
    

    Bu, JSONB verisini raporlama sorgularına sokmanın en temiz yoludur; bir view içine koyarsanız uygulama tarafı JSONB olduğunu hiç bilmez. Bu tekniği view ve materialized view farkı yazısındaki yaklaşımla birleştirerek pahalı JSONB raporlarını önceden hesaplayabilirsiniz.

    Ne Zaman JSONB Kullanmamalısınız#

    JSONB güçlü olduğu için fazla kullanılmaya çok müsaittir. Şu durumlarda normal sütun kullanın:

    Alan her satırda varsa ve sorgulanıyorsa. fiyat, durum, kullanici_id gibi alanlar JSONB içinde durmamalıdır. Normal sütun daha az yer kaplar, daha hızlı indekslenir, tür güvenliği sağlar ve NOT NULL gibi kısıtlar koyabilirsiniz.

    Yabancı anahtar ilişkisi gerekiyorsa. JSONB içindeki bir id'ye yabancı anahtar kısıtı koyamazsınız; referans bütünlüğünü veritabanı garanti edemez.

    Sık güncellenen küçük alanlar için. JSONB güncellemesi belgenin tamamını yeniden yazar. Sayaç gibi saniyede defalarca artan bir değer JSONB içinde durursa, hem yazma maliyeti hem ölü satır üretimi patlar. Bu, tablo şişmesinin sinsi kaynaklarından biridir; PostgreSQL VACUUM ve autovacuum yazısı bunun sonuçlarını anlatıyor.

    Toplama ve karşılaştırma yoğun raporlarda. Her satırda (ozellikler ->> 'tutar')::numeric dönüşümü yapmak, normal numeric sütuna göre kat kat pahalıdır ve istatistik toplayıcı bu ifadeler için iyi tahmin üretemez, dolayısıyla planlar kötüleşir.

    Doğru desen genelde şudur: sorguladığınız her şey sütun, sakladığınız ama nadiren filtrelediğiniz her şey JSONB. Bir alanı JSONB'de tutmaya başlayıp sonra sürekli sorguladığınızı fark ederseniz, onu sütuna terfi ettirin:

    ALTER TABLE urunler ADD COLUMN panel text;
    UPDATE urunler SET panel = ozellikler ->> 'panel' WHERE ozellikler ? 'panel';
    ALTER TABLE urunler ALTER COLUMN panel SET NOT NULL;  -- veriniz uygunsa
    

    Belge veritabanı ile ilişkisel arasındaki genel tercihi düşünüyorsanız MongoDB kurulumu yazısı diğer tarafın nasıl çalıştığını gösteriyor; çoğu Türk e-ticaret ve SaaS projesinde PostgreSQL + JSONB birleşimi, ayrı bir belge veritabanı işletmekten daha az bakım maliyeti çıkarır.

    Sıkça Sorulan Sorular#

    JSON mu JSONB mi kullanmalıyım#

    Neredeyse her zaman jsonb. Veriyi sorgulayacak, filtreleyecek veya indeksleyecekseniz jsonb tek makul seçenektir; ayrıştırılmış saklandığı için erişim çok daha hızlıdır ve GIN indeksleriyle çalışır. json tipini yalnızca gelen belgeyi anahtar sırası ve boşluklarıyla birebir arşivlemeniz gerekiyorsa kullanın; bu, pratikte çok nadir bir ihtiyaçtır.

    JSONB sorguları neden yavaş çalışıyor#

    En yaygın sebep indeks eksikliğidir; indekssiz her JSONB filtresi tam tablo taraması yapar. İkinci sebep, kurduğunuz indeksin kullandığınız operatörle uyumsuz olmasıdır: jsonb_path_ops yalnızca @> ile çalışır, ->> karşılaştırmaları ise ancak ifade indeksiyle hızlanır. EXPLAIN (ANALYZE, BUFFERS) çıktısına bakıp gerçekten indeks taraması yapılıp yapılmadığını doğrulayın.

    JSONB içindeki bir alanı nasıl indekslerim#

    Sürekli aynı alanı sorguluyorsanız en verimli yol ifade indeksidir: CREATE INDEX idx ON tablo ((sutun ->> 'alan'));. Sayısal karşılaştırma yapıyorsanız indekste de aynı tür dönüşümünü uygulayın. Belgenin herhangi bir yerinde arama yapıyorsanız GIN indeksi kullanın; yalnızca içerme sorgusu yapıyorsanız jsonb_path_ops sınıfı hem daha küçük hem daha hızlıdır.

    JSONB alanını nasıl güncellerim#

    Tek bir alanı değiştirmek için jsonb_set(sutun, '{anahtar}', 'deger'::jsonb) kullanın; yol yoksa oluşturması için son parametreyi true verin. Birden fazla alanı aynı anda eklemek veya değiştirmek için || birleştirme operatörü daha pratiktir. Anahtar silmek için - operatörünü kullanın. Unutmayın, her güncelleme belgenin tamamını yeniden yazar.

    JSONB kullanmak şemasız çalışmak mı demek#

    Hayır ve bu ayrımı yapmak önemlidir. JSONB size esneklik verir ama doğrulama yapmaz: yanlış yazılmış bir anahtar adı ya da metin olarak yazılmış bir sayı hata vermeden kaydedilir. Bu yüzden veri şeklini uygulama katmanında doğrulamalı, mümkünse CHECK kısıtlarıyla temel kuralları veritabanına da yazmalısınız. Şemasızlık bir özgürlük değil, taşıdığınız bir sorumluluktur.

    JSONB yerine MongoDB kullanmalı mıyım#

    Zaten PostgreSQL kullanıyorsanız ve belge ihtiyacınız uygulamanızın bir bölümüyle sınırlıysa, JSONB neredeyse her zaman daha az maliyetlidir: tek bir sistem yönetir, tek yedekleme akışı kurar, işlem bütünlüğünü kaybetmezsiniz. Ayrı bir belge veritabanı, veri modeliniz baştan sona belge tabanlıysa ve yatay ölçekleme ihtiyacınız gerçekse anlamlı hâle gelir.

    Kapanış#

    JSONB, PostgreSQL'i esnek şema ihtiyaçlarında bir belge veritabanı kadar rahat kullanılabilir hâle getirir; üstelik işlem bütünlüğünden, birleşimlerden ve tanıdık araçlardan vazgeçmeden. Aklınızda tutmanız gereken dört alışkanlık: sorguladığınız alanları JSONB'de değil normal sütunda tutun, -> ile ->> farkını karıştırmayın, indeksi kullandığınız operatöre göre seçin ve büyük belgeleri sık güncellemekten kaçının.

    JSONB verisi zamanla büyür ve indeksleri belleğe sığmadığında sorgular hissedilir biçimde yavaşlar; bellek ve disk hızı bu iş yükünde doğrudan belirleyicidir. Kaynakları garantili VDS ve bulut sunucu paketlerimizde PostgreSQL'i kendi ayarlarınızla işletebilir, ayar ve izleme yükünü sunucu yönetimi hizmetimize devredebilirsiniz. Büyüyen belge tablolarınız için yedekleme çözümlerimize de göz atın.

    PostgreSQLJSONBSQL

    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.