Veritabanı Yönetimi

    PostgreSQL Index Türleri: B-tree, GIN, GiST ve BRIN

    PostgreSQL'deki altı index türünün ne işe yaradığı ve doğru olanı seçme yöntemi.

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

    PostgreSQL'de indeks demek çoğu kişi için B-tree demektir; CREATE INDEX yazarsınız, PostgreSQL B-tree oluşturur ve iş biter. Bu, sorgularınızın büyük çoğunluğu için doğru seçimdir. Ama bir JSONB alanının içinde arama yapıyorsanız, bir metin sütununda LIKE '%kelime%' çalıştırıyorsanız ya da 500 milyon satırlık bir log tablosunda tarih aralığı sorguluyorsanız, B-tree ya işe yaramaz ya da gereksiz yere devasa olur.

    PostgreSQL altı farklı indeks türü sunar ve her biri belirli bir soru tipini ucuza cevaplamak için tasarlanmıştır. Bu rehberde PostgreSQL index türlerini tek tek ele alacağım: hangi veri tipiyle ve hangi operatörle çalıştıklarını, ne kadar yer kapladıklarını, ne zaman B-tree'den daha iyi olduklarını ve seçim yaparken bakmanız gereken ölçütleri anlatacağım. Ayrıca indeks eklemenin bedava olmadığını, yazma maliyetini nasıl artırdığını da göstereceğim.

    Genel Bakış ve Seçim Tablosu#

    Aşağıdaki tablo, hangi indeksi ne zaman düşüneceğinizin kısa özeti. Ayrıntılar sonraki bölümlerde.

    TürNe içinDesteklediği operatörlerBoyutTipik kullanım
    B-treeSıralanabilir değerler=, <, >, BETWEEN, IN, ön ek LIKEOrtaVarsayılan, birincil anahtar, tarih
    HashYalnızca eşitlik=KüçükUzun metin anahtarların eşitliği
    GINBir değerde birden çok öğe@>, ?, @@, dizi ve JSONBBüyükJSONB, dizi, tam metin arama
    GiSTÇakışma ve yakınlık&&, @>, <->, aralık ve geometriOrtaAralık tipleri, coğrafi veri
    SP-GiSTDengesiz, bölünen veriÖn ek eşleşmesi, nokta verisiKüçükIP ön ekleri, metin ön ekleri
    BRINFiziksel sırayla korele veri<, >, BETWEENÇok küçükDevasa log ve zaman serisi tabloları

    Karar verirken sorulacak üç soru şudur: sütunumda tek bir değer mi var yoksa birden çok öğe mi (dizi, JSON, metin belgesi)? Sorgum eşitlik mi, aralık mı, içerme mi arıyor? Tablo fiziksel olarak sıralı mı yazılıyor?

    B-tree: Varsayılan ve Çoğu Zaman Doğru#

    B-tree, sıralanabilir her veri tipiyle çalışır ve dengeli bir ağaç yapısıyla logaritmik zamanda arama yapar. CREATE INDEX komutuna tür belirtmezseniz bunu alırsınız.

    -- Basit B-tree
    CREATE INDEX idx_siparisler_musteri ON siparisler (musteri_id);
    
    -- Bileşik indeks: sütun SIRASI kritiktir
    CREATE INDEX idx_siparisler_musteri_tarih ON siparisler (musteri_id, olusturulma DESC);
    

    Bileşik indekste sıra kuralı şudur: indeks yalnızca soldan başlayan bir ön ek için kullanılabilir. Yukarıdaki indeks WHERE musteri_id = 5 ve WHERE musteri_id = 5 AND olusturulma > '2026-01-01' sorgularına yarar; ama tek başına WHERE olusturulma > '2026-01-01' sorgusuna yaramaz. Eşitlik aranan sütunları sola, aralık aranan sütunları sağa koyun.

    İki güçlü B-tree varyantı var. Kısmi indeks (partial index) yalnızca belirli satırları kapsar ve devasa tasarruf sağlar:

    -- Sadece bekleyen siparişler indekslensin; tablonun %2'si bile olabilir
    CREATE INDEX idx_siparisler_bekleyen ON siparisler (olusturulma)
    WHERE durum = 'bekliyor';
    

    Kapsayan indeks (covering index) ise sorgunun ihtiyaç duyduğu ek sütunları indeksin yaprak düğümlerine taşır; böylece tabloya hiç gidilmez:

    CREATE INDEX idx_siparisler_kapsayan ON siparisler (musteri_id) INCLUDE (toplam_tutar, durum);
    

    Bir de ifade indeksi var; sütun üzerinde fonksiyon uyguladığınızda normal indeks kullanılamaz:

    -- Bu sorgu normal indeksi KULLANMAZ:
    -- SELECT * FROM kullanicilar WHERE lower(eposta) = '[email protected]';
    CREATE INDEX idx_kullanicilar_eposta_lower ON kullanicilar (lower(eposta));
    

    B-tree, metin sütunlarında yalnızca ön ekli LIKE sorgularını hızlandırır (LIKE 'ank%'), ortada arama (LIKE '%ank%') için işe yaramaz. Ayrıca varsayılan olmayan bir karşılaştırma düzeni (collation) kullanıyorsanız text_pattern_ops sınıfına ihtiyacınız olur:

    CREATE INDEX idx_urunler_ad_prefix ON urunler (ad text_pattern_ops);
    

    GIN: Bir Değerin İçindeki Öğeler#

    GIN (Generalized Inverted Index), ters indeks mantığıyla çalışır: her öğe için, o öğeyi içeren satırların listesini tutar. Bu yüzden bir sütunda birden fazla değer barındıran tiplerde vazgeçilmezdir: diziler, JSONB ve tam metin arama vektörleri.

    -- Dizi sütununda içerme sorgusu
    CREATE INDEX idx_makaleler_etiketler ON makaleler USING GIN (etiketler);
    SELECT * FROM makaleler WHERE etiketler @> ARRAY['postgresql'];
    
    -- JSONB üzerinde anahtar/değer arama
    CREATE INDEX idx_olaylar_veri ON olaylar USING GIN (veri jsonb_path_ops);
    SELECT * FROM olaylar WHERE veri @> '{"tur":"odeme","durum":"basarili"}';
    

    jsonb_path_ops operatör sınıfı, varsayılan jsonb_ops sınıfından belirgin biçimde küçük ve hızlıdır ama yalnızca @> içerme operatörünü destekler. Anahtar varlığı sorguları (?, ?|, ?&) da kullanacaksanız varsayılan sınıfta kalın. JSONB tarafını daha derin incelemek için PostgreSQL JSONB kullanımı yazısına bakın.

    GIN'in en yaygın ikinci kullanımı pg_trgm eklentisiyle ortada metin aramadır:

    CREATE EXTENSION IF NOT EXISTS pg_trgm;
    CREATE INDEX idx_urunler_ad_trgm ON urunler USING GIN (ad gin_trgm_ops);
    
    -- Artık bu sorgu indeks kullanır
    SELECT * FROM urunler WHERE ad ILIKE '%klavye%';
    

    GIN'in bedeli yazma maliyetidir: her ekleme, değerin içindeki tüm öğeleri indekse dağıtır. PostgreSQL bunu hafifletmek için bekleyen liste (pending list) kullanır:

    -- Toplu yükleme sırasında yazmayı hızlandırır, okuma biraz yavaşlar
    ALTER INDEX idx_olaylar_veri SET (fastupdate = on, gin_pending_list_limit = '8MB');
    

    Tam metin arama için GIN kullanımını PostgreSQL full-text arama yazısında ayrıntılı bulacaksınız.

    GiST: Çakışma, Aralık ve Yakınlık#

    GiST (Generalized Search Tree), bir çerçeve indekstir: veri tipi kendi karşılaştırma mantığını sağlar. En çok aralık tipleri, coğrafi veri (PostGIS) ve "birbirine yakın olanı bul" sorgularında kullanılır.

    En sevdiğim kullanımı, çakışan rezervasyonları veritabanı seviyesinde imkânsız kılmaktır:

    CREATE EXTENSION IF NOT EXISTS btree_gist;
    
    CREATE TABLE rezervasyonlar (
        id       bigserial PRIMARY KEY,
        oda_id   int NOT NULL,
        aralik   tstzrange NOT NULL,
        -- Aynı odada zaman aralıkları çakışamaz
        EXCLUDE USING GIST (oda_id WITH =, aralik WITH &&)
    );
    

    Bu tek satır, uygulama katmanında yazacağınız onlarca satırlık çakışma kontrolünün yerine geçer ve eşzamanlı isteklerde bile hata yapmaz. btree_gist eklentisi, GiST indeksinin oda_id gibi sıradan bir tamsayıyı da kapsayabilmesi için gerekir.

    GiST ayrıca pg_trgm ile benzerlik sorgularında kullanılır ve GIN'e göre daha küçük, daha hızlı yazılan ama okumada daha yavaş bir alternatiftir:

    CREATE INDEX idx_urunler_ad_trgm_gist ON urunler USING GiST (ad gist_trgm_ops);
    
    -- En benzer 5 ürün adı (yazım hatası toleranslı arama)
    SELECT ad, similarity(ad, 'kablosuz klavye') AS skor
    FROM urunler
    ORDER BY ad <-> 'kablosuz klavye'
    LIMIT 5;
    

    <-> operatörü mesafe döndürür ve GiST bunu doğrudan indeksten sıralayabilir; buna "en yakın komşu taraması" denir ve GIN bunu yapamaz.

    BRIN: Devasa Tablolarda Neredeyse Bedava İndeks#

    BRIN (Block Range Index), tablonun her blok grubu için yalnızca minimum ve maksimum değeri saklar. Yani milyarlarca satırlık bir tablo için indeks birkaç yüz kilobayt olabilir. Karşılığında tek bir koşulu vardır: veri, fiziksel yazım sırasıyla korele olmalıdır.

    Log tabloları bu koşulu doğal olarak sağlar; kayıtlar zaman sırasıyla eklenir ve diskte de o sırada durur.

    CREATE TABLE erisim_loglari (
        id        bigserial,
        kayit_ani timestamptz NOT NULL DEFAULT now(),
        ip        inet,
        yol       text,
        durum     smallint
    );
    
    -- 128 sayfalık bloklar halinde min/max sakla
    CREATE INDEX idx_loglar_zaman_brin ON erisim_loglari
    USING BRIN (kayit_ani) WITH (pages_per_range = 128);
    

    Boyut farkı çarpıcıdır. 300 milyon satırlık bir log tablosunda kayit_ani üzerindeki B-tree indeksi birkaç gigabayt yer kaplarken, aynı sütun için BRIN birkaç yüz kilobayttır ve tarih aralığı sorgularında B-tree'ye yakın sonuç verir.

    BRIN'in çöktüğü yer, verinin korelasyonunun bozulmasıdır. Tabloda çok fazla UPDATE yapılıyor ve satırlar diskte dağınık yerlere yazılıyorsa BRIN neredeyse hiç eleme yapamaz. Korelasyonu kontrol edin:

    SELECT attname, correlation
    FROM pg_stats
    WHERE tablename = 'erisim_loglari' AND attname = 'kayit_ani';
    

    correlation değeri 1'e (ya da -1'e) yakınsa BRIN mükemmel çalışır; 0'a yakınsa BRIN kullanmayın. Ayrıca BRIN'in özeti, yeni bloklar eklendikçe otomatik güncellenmez; toplu yüklemeden sonra özeti tazeleyin:

    SELECT brin_summarize_new_values('idx_loglar_zaman_brin');
    -- ya da indeksi otomatik özetleyecek şekilde oluşturun
    -- CREATE INDEX ... USING BRIN (kayit_ani) WITH (autosummarize = on);
    

    Hash ve SP-GiST: Dar Ama Gerçek Kullanım Alanları#

    Hash indeks, yalnızca = operatörünü destekler. PostgreSQL 10'dan itibaren WAL'a yazıldığı için artık çökme güvenlidir ve replikasyonla taşınır. Kazancı, çok uzun metin anahtarlarında ortaya çıkar: B-tree tüm değeri saklarken hash yalnızca 32 bitlik özeti saklar.

    -- Uzun URL'lerin eşitlik sorgusu için
    CREATE INDEX idx_baglantilar_url_hash ON baglantilar USING HASH (url);
    

    Yine de çoğu durumda B-tree tercih edilir, çünkü hem eşitliği hem sıralamayı destekler ve bileşik indekse girebilir. Hash indeks birincil anahtar ya da eşsizlik kısıtı olamaz.

    SP-GiST (Space-Partitioned GiST), dengesiz dallanan veri yapıları içindir: metin ön ekleri, IP ağ ön ekleri, nokta koordinatları. En pratik kullanımı inet sütunlarıdır:

    CREATE INDEX idx_loglar_ip_spgist ON erisim_loglari USING SPGIST (ip inet_ops);
    
    -- Bir alt ağdaki tüm kayıtlar
    SELECT count(*) FROM erisim_loglari WHERE ip << inet '185.12.34.0/24';
    

    Alt ağ hesaplarıyla uğraşıyorsanız subnet hesaplayıcı aracımız aralıkları doğrulamanızda işinize yarar.

    Doğru İndeksi Seçmek ve Ölçmek#

    İndeks eklemeden önce sorgunun gerçekten indekse ihtiyacı olduğunu doğrulayın. EXPLAIN (ANALYZE, BUFFERS) çıktısı bunun tek dürüst kaynağıdır:

    EXPLAIN (ANALYZE, BUFFERS)
    SELECT * FROM siparisler WHERE musteri_id = 4172 AND olusturulma > now() - interval '30 days';
    

    Seq Scan görüp de satır sayısı azsa indeks eksik demektir. Ama küçük tablolarda Seq Scan normaldir ve indeks eklemek işleri yavaşlatır; planlayıcı boşuna seçmiyor.

    İndeks eklemenin bedeli yazmadır. Her INSERT, tablodaki tüm indeksleri günceller. Beş indeksli bir tabloya yazmak, indeksiz bir tabloya yazmaktan kat kat pahalıdır. Bu yüzden kullanılmayan indeksleri bulup silin:

    SELECT
        s.relname       AS tablo,
        s.indexrelname  AS indeks,
        s.idx_scan      AS tarama_sayisi,
        pg_size_pretty(pg_relation_size(s.indexrelid)) AS boyut
    FROM pg_stat_user_indexes s
    JOIN pg_index i ON i.indexrelid = s.indexrelid
    WHERE s.idx_scan = 0 AND NOT i.indisunique
    ORDER BY pg_relation_size(s.indexrelid) DESC;
    

    idx_scan = 0 olan ve eşsizlik kısıtı olmayan indeksler, istatistikler sıfırlandığından beri hiç kullanılmamış demektir. En az birkaç haftalık veri biriktikten sonra karar verin.

    Üretimde indeks oluştururken tabloyu kilitlememek için mutlaka CONCURRENTLY kullanın:

    CREATE INDEX CONCURRENTLY idx_siparisler_durum ON siparisler (durum) WHERE durum != 'tamamlandi';
    

    Bu komut bir işlem bloğu içinde çalışmaz ve başarısız olursa geçersiz bir indeks bırakır; \di+ çıktısında INVALID görürseniz silip yeniden oluşturun. İndekslerin zamanla şişmesi de gerçek bir sorundur; ayrıntı için PostgreSQL VACUUM ve autovacuum yazısındaki REINDEX CONCURRENTLY bölümüne bakın.

    Sıkça Sorulan Sorular#

    Hangi index türünü seçmeliyim#

    Kısa cevap: emin değilseniz B-tree. Sütununuz tek bir sıralanabilir değer tutuyorsa ve eşitlik ya da aralık sorguluyorsanız B-tree doğru seçimdir. Sütun içinde birden fazla öğe varsa (dizi, JSONB, metin belgesi) GIN, çakışma ve yakınlık sorguları için GiST, fiziksel olarak sıralı devasa tablolarda tarih aralığı için BRIN düşünün.

    GIN mi GiST mi daha hızlı#

    Okuma tarafında GIN belirgin biçimde daha hızlıdır; yazma tarafında ve indeks boyutunda GiST daha ekonomiktir. Tam metin arama ve JSONB içerme sorguları için GIN standarttır. Buna karşılık "en yakın komşu" sıralaması (<-> operatörü) yalnızca GiST ile mümkündür ve sık güncellenen tablolarda GiST'in yazma maliyeti daha katlanılabilirdir.

    BRIN index ne zaman işe yarar#

    Yalnızca sütun değerleri satırların fiziksel diziliş sırasıyla korele olduğunda. Zaman damgasıyla sürekli sonuna eklenen log ve olay tabloları en tipik örnektir. pg_stats görünümündeki correlation değeri 1'e yakınsa BRIN mükemmel iş çıkarır; 0'a yakınsa hiçbir işe yaramaz ve boşuna eklemiş olursunuz.

    Çok fazla index performansı düşürür mü#

    Evet, ve bu genellikle hafife alınır. Her indeks, tabloya yapılan her ekleme ve güncellemede güncellenmek zorundadır; on indeksli bir tabloda yazma hızı belirgin biçimde düşer. Ayrıca indeksler disk ve önbellek yeri kaplar. pg_stat_user_indexes görünümünde idx_scan değeri sıfır olan indeksleri düzenli olarak gözden geçirip silin.

    LIKE sorgularını nasıl hızlandırırım#

    Sorgunuz LIKE 'ank%' gibi ön ek eşleşmesiyse normal B-tree yeterlidir; varsayılan olmayan karşılaştırma düzeni kullanıyorsanız text_pattern_ops operatör sınıfını ekleyin. LIKE '%ank%' gibi ortada arama içinse pg_trgm eklentisini kurup gin_trgm_ops ile bir GIN indeksi oluşturmanız gerekir; başka türlü indeks kullanılamaz.

    Index oluştururken tablo kilitlenir mi#

    Normal CREATE INDEX komutu tablo üzerinde yazma kilidi alır; süresince INSERT, UPDATE ve DELETE bekler. Üretimde bunu önlemek için CREATE INDEX CONCURRENTLY kullanın; tabloyu iki kez tarar, daha uzun sürer ama yazmaları engellemez. Bu komut bir işlem bloğu içinde çalıştırılamaz ve hata alırsa geçersiz bir indeks bırakabilir, bu yüzden sonrasında durumu kontrol edin.

    Kapanış#

    PostgreSQL'in indeks çeşitliliği, "her soruya aynı araçla cevap verme" tuzağından kurtulmanızı sağlar. B-tree çoğu iş yükünün belkemiğidir; GIN, bir sütunun içindeki öğeleri aramanın tek verimli yoludur; GiST çakışma ve yakınlık sorularının cevabıdır; BRIN ise devasa ve sıralı tablolarda neredeyse bedava bir hızlanma sunar. Aklınızda kalması gereken dört alışkanlık: indeks eklemeden önce EXPLAIN (ANALYZE, BUFFERS) ile gerçekten gerektiğini doğrulayın, bileşik indekslerde sütun sırasını eşitlikten aralığa doğru kurun, üretimde her zaman CONCURRENTLY kullanın ve idx_scan = 0 olan indeksleri düzenli temizleyin.

    İndeks oluşturma ve bakımı yoğun disk ve bellek işidir; maintenance_work_mem ile birlikte altyapının hızı süreyi doğrudan belirler. Kendi PostgreSQL sunucunuzu ayarlarıyla yönetmek isterseniz VDS ve bulut sunucu paketlerimiz esnek bir zemin sunar; ayar, izleme ve bakım işini üstlenmemizi isterseniz sunucu yönetimi hizmetimiz bu yükü alır. Büyük indeksli veritabanlarının düzenli kopyalanması için yedekleme sayfamıza da göz atabilirsiniz.

    PostgreSQLİndeksPerformans

    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.