PostgreSQL'de indeks CREATE INDEX ad ON tablo (sutun) ile oluşturulur ve türü belirtilmezse B-tree olur. B-tree eşitlik, aralık, sıralama ve baştan sabitlenmiş LIKE aramalarında; Hash yalnızca eşitlikte; GIN dizi, JSONB ve tam metin gibi çok parçalı değerlerde; GiST ve SP-GiST geometri, aralık ve en yakın komşu aramalarında; BRIN ise fiziksel sırayla uyumlu çok büyük tablolarda (örneğin artan tarih) kullanılır. Doğru türü sorgunuzdaki operatör belirler.
Bu yazı indekslerin mekaniğine odaklanıyor: altı indeks türü ve hangi operatörleri desteklediği, çok sütunlu indeks ve PostgreSQL 18 ile gelen skip scan, ifade indeksi, kısmi indeks, UNIQUE ve NULLS NOT DISTINCT, INCLUDE ile kapsayan indeks ve index-only scan, CREATE INDEX CONCURRENTLY, REINDEX ve kullanılmayan indeksleri bulma. Hangi sorguya hangi indeksin ekleneceğine dair genel strateji ve yapılandırma önerileri PostgreSQL performans optimizasyonu yazısında; plan çıktısını okumanın ayrıntıları EXPLAIN ANALYZE yazısında. Serinin tamamı PostgreSQL öğrenme rehberinde.
İndeks ne yapar, bedeli nedir?
Kılavuz indeksi bir kitabın sonundaki dizine benzetir: aranan terimi tüm kitabı okumadan bulmayı sağlar. İndeks yoksa PostgreSQL koşulu sağlayan birkaç satırı bulmak için tüm tabloyu satır satır okumak zorunda kalır. İndeks bir kez oluşturulduktan sonra ek müdahale gerekmez: tablo değiştikçe sistem indeksi günceller ve planlayıcı, sıralı taramadan daha ucuz olduğunu düşündüğünde indeksi kullanır. Planlayıcının doğru karar vermesi için istatistiklerin güncel olması gerekir; bunu ANALYZE sağlar.
Bedeli de vardır. Her indeks, veri değiştirme işlemlerinde ek iş demektir ve kılavuz, indekslerin HOT (heap-only tuple) güncellemelerini de engelleyebileceğini belirtir. Bu yüzden nadiren ya da hiç kullanılmayan indeksler kaldırılmalıdır. Büyük bir tabloda indeks oluşturmak uzun sürebilir; varsayılan olarak bu sürede okumalar devam eder ama INSERT, UPDATE, DELETE engellenir. İndeksler UPDATE ve DELETE komutlarının arama koşullarını ve JOIN koşullarını da hızlandırabilir.
Önce 200 bin satırlık bir sipariş tablosu oluşturalım ve basit bir B-tree indeksin plana etkisine bakalım. Plan çıktılarını sürümden ve makineden bağımsız tutmak için COSTS OFF kullanıyoruz:
CREATE TABLE siparis (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
musteri_id integer NOT NULL,
durum text NOT NULL,
tarih date NOT NULL,
eposta text NOT NULL,
tutar numeric(10,2) NOT NULL,
etiketler text[] NOT NULL
);
INSERT INTO siparis (musteri_id, durum, tarih, eposta, tutar, etiketler)
SELECT (i % 20000) + 1,
CASE WHEN i % 100 = 0 THEN 'bekliyor' ELSE 'tamam' END,
date '2024-01-01' + (i / 300),
'Musteri' || (i % 20000) || '@Example.com',
(i % 500) + 0.99,
CASE WHEN i % 7 = 0 THEN ARRAY['kargo', 'hediye'] ELSE ARRAY['kargo'] END
FROM generate_series(1, 200000) AS i;
ANALYZE siparis;
EXPLAIN (COSTS OFF) SELECT * FROM siparis WHERE musteri_id = 42;
CREATE INDEX siparis_musteri_idx ON siparis (musteri_id);
EXPLAIN (COSTS OFF) SELECT * FROM siparis WHERE musteri_id = 42; QUERY PLAN
-----------------------------
Seq Scan on siparis
Filter: (musteri_id = 42)
(2 rows)
CREATE INDEX
QUERY PLAN
------------------------------------------------
Bitmap Heap Scan on siparis
Recheck Cond: (musteri_id = 42)
-> Bitmap Index Scan on siparis_musteri_idx
Index Cond: (musteri_id = 42)
(4 rows)İndeks yokken plan tüm tabloyu okuyan bir Seq Scan idi. İndeksten sonra planlayıcı Bitmap Index Scan ile eşleşen satırların yerini indeksten bulup Bitmap Heap Scan ile tabloya gidiyor. Kılavuz, bir indeksin indeksli_sutun operator karsilastirma_degeri biçimindeki koşullarda kullanılabildiğini anlatır: operatör indeksin operatör sınıfına ait olmalı, karşılaştırma değeri de VOLATILE olmayan ve indeksin tablosuna başvurmayan bir ifade olmalıdır.
İndeks türleri ve desteklediği operatörler

| Tür | Tipik operatörler | Ne zaman |
|---|---|---|
| B-tree (varsayılan) | < <= = >= > , BETWEEN, IN, IS NULL, baştan sabit LIKE | Sıralanabilir veri, eşitlik ve aralık, ORDER BY |
| Hash | = | Yalnızca eşitlik |
| GiST | << &< &> >> @> <@ ~= && ve <-> (sıralama) | Geometri, aralık tipleri, en yakın komşu; altyapıdır, operatörler sınıfa göre değişir |
| SP-GiST | << >> ~= <@ <<| |>> | Dengesiz yapılar: quadtree, k-d tree, radix tree |
| GIN | <@ @> = && (diziler için) | Çok parçalı değerler: dizi, JSONB, tam metin |
| BRIN | < <= = >= > | Değeri fiziksel sırayla uyumlu çok büyük tablolar |
Türü USING ile seçersiniz: CREATE INDEX ... USING gin (sutun). Birkaç ayrıntı tabloya sığmıyor. B-tree, LIKE 'abc%' veya ~ '^abc' gibi başa sabitlenmiş kalıplarda kullanılabilir, LIKE '%abc' gibi baştan açık kalıplarda kullanılamaz. Veritabanınız C yerel ayarını kullanmıyorsa, kalıp araması için indeksi text_pattern_ops gibi özel bir operatör sınıfıyla oluşturmanız gerekir; bu sınıflar karakterleri yerel ayarın sıralama kurallarına göre değil tek tek karşılaştırır, ancak sıradan < ve > karşılaştırmalarında kullanılamaz. Collation ve yerel ayar konusu collation ve Türkçe sıralama yazısında.
GiST bir indeks türünden çok bir altyapıdır: hangi operatörlerin indekslenebileceğini operatör sınıfı belirler ve dağıtımla gelen sınıfların yanında contrib ya da ayrı projelerden başka sınıflar da eklenebilir. GiST ve SP-GiST, ORDER BY konum <-> nokta LIMIT 10 gibi en yakın komşu sorgularını da hızlandırabilir. GIN bir ters indekstir (inverted index): değerin her bileşeni için ayrı bir kayıt tutar ve "bu bileşen var mı?" sorusuna hızlı cevap verir. JSONB ile GIN kullanımı JSONB kullanımı, dizilerle kullanımı dizi tipi, tam metin aramayla kullanımı full text search yazılarında.
İfade indeksi ve kısmi indeks
İndeks sütunu tablonun bir sütunu olmak zorunda değildir; bir veya daha fazla sütundan hesaplanan bir fonksiyon ya da ifade olabilir. Kılavuzun klasik örneği büyük/küçük harf duyarsız aramadır: lower(sutun) üzerine indeks kurulur ve sorgu da aynı ifadeyi kullanır. İfade fonksiyon çağrısından ibaret değilse parantez içine yazılmalıdır: ((ad || ' ' || soyad)). İfadede kullanılan fonksiyonların IMMUTABLE olması şarttır; bu kural ve nedeni CREATE FUNCTION yazısında örneklendi.
Kısmi indeks, tablonun yalnızca WHERE koşulunu sağlayan satırlarını içerir. Kılavuz üç kullanım sayar: çok sık geçen ve zaten indeksle aranmayacak değerleri dışarıda bırakmak, sık sorgulanan küçük bir alt kümeyi (örneğin faturası kesilmemiş siparişler) hedeflemek ve yalnızca bir alt kümede benzersizlik zorlamak. Örnekte sipariş durumlarının yüzde biri "bekliyor":
-- İfade indeksi
CREATE INDEX siparis_eposta_lower_idx ON siparis (lower(eposta));
EXPLAIN (COSTS OFF) SELECT id FROM siparis WHERE lower(eposta) = 'musteri42@example.com';
EXPLAIN (COSTS OFF) SELECT id FROM siparis WHERE eposta = 'Musteri42@Example.com';
-- Kısmi indeks
CREATE INDEX siparis_bekleyen_idx ON siparis (tarih) WHERE durum = 'bekliyor';
EXPLAIN (COSTS OFF) SELECT id, tarih FROM siparis
WHERE durum = 'bekliyor' AND tarih >= '2025-10-01';
EXPLAIN (COSTS OFF) SELECT id, tarih FROM siparis
WHERE durum = 'tamam' AND tarih = '2025-10-01';
SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) AS boyut
FROM pg_stat_user_indexes WHERE relname = 'siparis' ORDER BY indexrelname; QUERY PLAN
---------------------------------------------------------------------
Bitmap Heap Scan on siparis
Recheck Cond: (lower(eposta) = 'musteri42@example.com'::text)
-> Bitmap Index Scan on siparis_eposta_lower_idx
Index Cond: (lower(eposta) = 'musteri42@example.com'::text)
(4 rows)
QUERY PLAN
----------------------------------------------------
Seq Scan on siparis
Filter: (eposta = 'Musteri42@Example.com'::text)
(2 rows)
CREATE INDEX
QUERY PLAN
--------------------------------------------------
Index Scan using siparis_bekleyen_idx on siparis
Index Cond: (tarih >= '2025-10-01'::date)
(2 rows)
QUERY PLAN
----------------------------------------------------------------------------
Gather
Workers Planned: 1
-> Parallel Seq Scan on siparis
Filter: ((durum = 'tamam'::text) AND (tarih = '2025-10-01'::date))
(4 rows)
indexrelname | boyut
--------------------------+---------
siparis_bekleyen_idx | 48 kB
siparis_eposta_lower_idx | 2320 kB
siparis_musteri_idx | 1856 kB
siparis_pkey | 4408 kB
(4 rows)Dört sonuç da kılavuzun kurallarını doğruluyor. lower(eposta) içeren sorgu ifade indeksini kullandı; aynı sütunu ifadesiz arayan sorgu kullanamadı ve tabloyu taradı. Bekleyen siparişleri arayan sorgu kısmi indeksi kullandı; "tamam" durumunu arayan sorgu kullanamadı, çünkü o satırlar indekste yok. Boyut listesi de farkı gösteriyor: kısmi indeks 48 kB, tüm satırları içeren diğer indeksler megabayt düzeyinde.
İfade indeksleri her ekleme ve HOT olmayan güncellemede ifadenin yeniden hesaplanmasını gerektirdiği için bakımı görece pahalıdır; buna karşılık aramada ifade yeniden hesaplanmaz, çünkü değer indekste saklıdır. Kılavuz bu yüzden ifade indekslerini, okuma hızının yazma hızından önemli olduğu durumlar için önerir.
Çok sütunlu indeks ve PostgreSQL 18 skip scan

Çok sütunlu indeksi B-tree, GiST, GIN ve BRIN destekler; bir indeks INCLUDE sütunları dahil en fazla 32 sütun içerebilir. Çok sütunlu B-tree en verimli hâlini, soldaki (baştaki) sütunlarda koşul olduğunda gösterir. Kılavuzun kesin kuralı şudur: baştaki sütunlardaki eşitlik koşulları ve eşitlik koşulu olmayan ilk sütundaki eşitsizlik koşulu, taranacak indeks bölümünü daraltır; daha sağdaki sütunların koşulları indeks içinde denetlenir ve tabloya gitmeyi azaltır ama taranan bölümü küçültmeyebilir.
PostgreSQL 18 sürüm notları, B-tree indekslerde skip scan desteğinin geldiğini belirtir: ilk sütunlarda koşul olmasa bile sonraki sütunlardaki koşullar indeks gezinmesinde kullanılabilir. Kılavuz bunu, ilk sütunun her olası değeri için içeride dinamik bir eşitlik koşulu üretilmesi olarak açıklar ve bunun genellikle ilk sütunda çok az farklı değer olduğunda seçildiğini ekler. Bizim (durum, tarih) indeksimizde durum yalnızca iki değer alıyor; sorgu ise yalnızca tarih üzerinde koşul veriyor:
-- Çok sütunlu indeks ve PostgreSQL 18 skip scan
VACUUM siparis;
CREATE INDEX siparis_durum_tarih_idx ON siparis (durum, tarih);
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
SELECT count(*) FROM siparis WHERE tarih = '2025-06-01';
-- Kapsayan (covering) indeks: INCLUDE ve index-only scan
CREATE INDEX siparis_musteri_tutar_idx ON siparis (musteri_id) INCLUDE (tutar);
EXPLAIN (COSTS OFF) SELECT musteri_id, tutar FROM siparis WHERE musteri_id = 42; QUERY PLAN
---------------------------------------------------------------------------------------------
Aggregate (actual rows=1.00 loops=1)
-> Index Only Scan using siparis_durum_tarih_idx on siparis (actual rows=300.00 loops=1)
Index Cond: (tarih = '2025-06-01'::date)
Heap Fetches: 0
Index Searches: 5
(5 rows)
CREATE INDEX
QUERY PLAN
------------------------------------------------------------
Index Only Scan using siparis_musteri_tutar_idx on siparis
Index Cond: (musteri_id = 42)
(2 rows)İlk planda indeks, ilk sütunu koşulsuz olduğu hâlde kullanıldı. Index Searches satırı PostgreSQL 18 ile EXPLAIN ANALYZE çıktısına eklenen, indeks taraması başına yapılan indeks arama sayısıdır; birden büyük olması indeksin tek bir aralıkla değil, birden çok yeniden konumlanmayla tarandığını gösterir. İlk sütunda çok sayıda farklı değer olsaydı tüm indeksin taranması gerekecekti ve kılavuza göre planlayıcı çoğu durumda sıralı taramayı tercih ederdi. EXPLAIN seçenekleri ve PG18 ile ANALYZE kullanırken BUFFERS bilgisinin varsayılan hâle gelmesi EXPLAIN yazısında anlatılıyor; burada çıktıyı kısa tutmak için BUFFERS OFF yazdık.
INCLUDE ile kapsayan indeks ve index-only scan
PostgreSQL'deki tüm indeksler ikincildir: tablonun ana veri alanından (heap) ayrı saklanır ve sıradan bir indeks taramasında her satır için hem indekse hem tabloya gidilir. Index-only scan bu ikinci adımı atlar ve sonucu yalnızca indeksten verir. İki koşulu vardır: indeks türü bunu desteklemeli (B-tree her zaman destekler, GiST ve SP-GiST bazı operatör sınıflarında, GIN hiç desteklemez) ve sorgu yalnızca indekste saklanan sütunlara başvurmalıdır.
Üçüncü bir koşul daha var: satırın sorguya görünür olduğu bilinmelidir. Görünürlük bilgisi indekste değil tabloda tutulur; PostgreSQL her sayfa için "tüm satırlar herkese görünür" bilgisini görünürlük haritasında (visibility map) saklar. Bu bit ayarlıysa tabloya gidilmez. Bu yüzden index-only scan, sık değişmeyen ve VACUUM görmüş tablolarda kazandırır. Yukarıdaki skip scan planındaki Heap Fetches: 0 satırı, VACUUM sonrasında hiçbir satır için tabloya gidilmediğini gösteriyordu. VACUUM ve görünürlük haritası VACUUM ve autovacuum yazısında.
INCLUDE (sutun) ile anahtar olmayan "yük" sütunları indekse eklenir: bu sütunlar aramada kullanılmaz ama index-only scan için indekste bulunur. Yukarıdaki siparis_musteri_tutar_idx indeksi musteri_id üzerinden arayıp tutar değerini de verdiği için plan Index Only Scan oldu. Kılavuzun uyarıları: geniş sütunları eklerken tutumlu olun, indeks satırı türün izin verdiği en büyük boyutu aşarsa ekleme başarısız olur; yük sütunları indeksi şişirir; INCLUDE sütunu ifade olamaz ve yalnızca B-tree, GiST ve SP-GiST tarafından desteklenir. Benzersiz bir indekste benzersizlik yalnızca anahtar sütunlara uygulanır, INCLUDE sütunlarına uygulanmaz.
GIN, BRIN ve Hash örnekleri
Aynı tabloda üç farklı türü deneyelim. Etiketler dizisinde "hediye" arayan sorgu için GIN, tarih sütunu için BRIN ve e-posta eşitliği için Hash:
-- GIN: dizi elemanı arama
CREATE INDEX siparis_etiket_gin ON siparis USING gin (etiketler);
EXPLAIN (COSTS OFF) SELECT count(*) FROM siparis WHERE etiketler @> ARRAY['hediye'];
-- BRIN ve B-tree: aynı sütun, farklı boyut
CREATE INDEX siparis_tarih_brin ON siparis USING brin (tarih);
CREATE INDEX siparis_tarih_btree ON siparis (tarih);
SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) AS boyut
FROM pg_stat_user_indexes
WHERE indexrelname IN ('siparis_tarih_brin', 'siparis_tarih_btree', 'siparis_etiket_gin')
ORDER BY indexrelname;
DROP INDEX siparis_tarih_btree;
DROP INDEX siparis_durum_tarih_idx;
EXPLAIN (COSTS OFF) SELECT count(*) FROM siparis
WHERE tarih BETWEEN '2025-03-01' AND '2025-03-07';
-- Hash: yalnızca eşitlik
CREATE INDEX siparis_eposta_hash ON siparis USING hash (eposta);
EXPLAIN (COSTS OFF) SELECT id FROM siparis WHERE eposta = 'Musteri42@Example.com';
EXPLAIN (COSTS OFF) SELECT id FROM siparis WHERE eposta > 'Musteri9'; QUERY PLAN
-------------------------------------------------------------
Aggregate
-> Bitmap Heap Scan on siparis
Recheck Cond: (etiketler @> '{hediye}'::text[])
-> Bitmap Index Scan on siparis_etiket_gin
Index Cond: (etiketler @> '{hediye}'::text[])
(5 rows)
CREATE INDEX
CREATE INDEX
indexrelname | boyut
---------------------+---------
siparis_etiket_gin | 288 kB
siparis_tarih_brin | 24 kB
siparis_tarih_btree | 1352 kB
(3 rows)
DROP INDEX
DROP INDEX
QUERY PLAN
---------------------------------------------------------------------------------------------
Aggregate
-> Bitmap Heap Scan on siparis
Recheck Cond: ((tarih >= '2025-03-01'::date) AND (tarih <= '2025-03-07'::date))
-> Bitmap Index Scan on siparis_tarih_brin
Index Cond: ((tarih >= '2025-03-01'::date) AND (tarih <= '2025-03-07'::date))
(5 rows)
CREATE INDEX
QUERY PLAN
--------------------------------------------------------------
Bitmap Heap Scan on siparis
Recheck Cond: (eposta = 'Musteri42@Example.com'::text)
-> Bitmap Index Scan on siparis_eposta_hash
Index Cond: (eposta = 'Musteri42@Example.com'::text)
(4 rows)
QUERY PLAN
---------------------------------------
Seq Scan on siparis
Filter: (eposta > 'Musteri9'::text)
(2 rows)GIN, @> (içerir) operatörlü sorguda kullanıldı. Boyut karşılaştırması BRIN'in ne kadar küçük olduğunu gösteriyor: aynı tarih sütunu için B-tree 1352 kB, BRIN 24 kB. BRIN her satırı değil, ardışık blok aralıklarının özetini (sıralanabilir tiplerde en küçük ve en büyük değer) saklar; bu yüzden değerleri tablonun fiziksel sırasıyla güçlü biçimde ilişkili olan sütunlarda etkilidir. Örneğimizde siparişler tarih sırasıyla eklendi. Ardından B-tree'yi ve tarih aramasını üstlenebilecek birleşik indeksi silince aralık sorgusu BRIN ile çalıştı.
Hash indeks 32 bitlik bir özet (hash) değeri saklar ve yalnızca = operatörüyle kullanılabilir. Eşitlik sorgusunda kullanıldı, > içeren sorguda kullanılamadı ve tablo tarandı. Kılavuza göre benzersiz indeks yalnızca B-tree ile oluşturulabilir; bu yüzden hash indeksle benzersizlik zorlanamaz.
UNIQUE, NULLS NOT DISTINCT ve benzersiz ifade indeksi
CREATE UNIQUE INDEX aynı indeks değerine sahip birden çok satırı engeller. Varsayılan olarak NULL değerler birbirine eşit sayılmaz, yani benzersiz sütunda birden çok NULL olabilir. NULLS NOT DISTINCT bunu değiştirir ve NULL değerleri eşit kabul eder. Çok sütunlu benzersiz indeks, yalnızca tüm sütunlar eşit olduğunda reddeder. Kılavuz, PRIMARY KEY ve UNIQUE kısıtlarının zaten otomatik olarak benzersiz indeks oluşturduğunu, bu sütunlara elle indeks eklemenin yalnızca kopya yaratacağını vurgular.
-- UNIQUE ve NULLS NOT DISTINCT
CREATE TABLE kupon (kod text, musteri_id integer);
CREATE UNIQUE INDEX kupon_kod_uq ON kupon (kod) NULLS NOT DISTINCT;
INSERT INTO kupon VALUES ('YAZ25', 1), (NULL, 2);
INSERT INTO kupon VALUES (NULL, 3);
-- Benzersiz ifade indeksi: büyük/küçük harf farkını yok say
CREATE UNIQUE INDEX kupon_kod_lower_uq ON kupon (lower(kod));
INSERT INTO kupon VALUES ('yaz25', 4);
-- CONCURRENTLY transaction bloğunda çalışmaz
BEGIN;
CREATE INDEX CONCURRENTLY kupon_musteri_idx ON kupon (musteri_id);
ROLLBACK;
CREATE INDEX CONCURRENTLY kupon_musteri_idx ON kupon (musteri_id);
REINDEX INDEX CONCURRENTLY kupon_musteri_idx;
SELECT indexrelid::regclass AS indeks, indisvalid, indisunique
FROM pg_index WHERE indrelid = 'kupon'::regclass ORDER BY 1;CREATE TABLE
CREATE INDEX
INSERT 0 2
ERROR: duplicate key value violates unique constraint "kupon_kod_uq"
DETAIL: Key (kod)=(null) already exists.
CREATE INDEX
ERROR: duplicate key value violates unique constraint "kupon_kod_lower_uq"
DETAIL: Key (lower(kod))=(yaz25) already exists.
BEGIN
ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
ROLLBACK
CREATE INDEX
REINDEX
indeks | indisvalid | indisunique
--------------------+------------+-------------
kupon_kod_uq | t | t
kupon_kod_lower_uq | t | t
kupon_musteri_idx | t | f
(3 rows)İkinci NULL reddedildi, çünkü indeks NULLS NOT DISTINCT ile tanımlandı. lower(kod) üzerindeki benzersiz ifade indeksi de yalnızca harf büyüklüğü farklı olan "yaz25" kodunu engelledi; kılavuz bu tekniği, basit benzersiz kısıtla tanımlanamayan kuralları zorlamanın yolu olarak gösterir. Kısıtların kendisi PRIMARY KEY, FOREIGN KEY ve kısıtlar yazısında.
CREATE INDEX CONCURRENTLY ve REINDEX
Normal CREATE INDEX çalışırken tabloya yazmalar engellenir. CONCURRENTLY seçeneği yazmaları engellemeden indeks kurar; karşılığında tabloyu iki kez tarar ve indeksi kullanabilecek mevcut transaction'ların bitmesini bekler. Kılavuz bunu daha fazla toplam iş ve belirgin şekilde daha uzun süre olarak tarif eder, ama üretim ortamında indeks eklemek için yararlı bulur. Yukarıdaki örnekte görüldüğü gibi CREATE INDEX CONCURRENTLY bir transaction bloğu içinde çalıştırılamaz ve bir tabloda aynı anda yalnızca bir eşzamanlı indeks oluşturma yapılabilir.
Eşzamanlı oluşturma sırasında kilitlenme (deadlock) veya benzersizlik ihlali gibi bir sorun çıkarsa komut başarısız olur ve geride INVALID işaretli bir indeks bırakır. Bu indeks sorgularda kullanılmaz ama güncellemelerde bakım yükü getirmeye devam eder; psql'in \d çıktısında INVALID olarak görünür ve pg_index.indisvalid sütunu false olur. Kılavuzun önerdiği çözüm indeksi silip yeniden CREATE INDEX CONCURRENTLY çalıştırmak ya da REINDEX INDEX CONCURRENTLY ile yeniden kurmaktır.
REINDEX bir indeksi tablodaki veriden yeniden oluşturur. Kılavuz şu durumları sayar: indeksin yazılım hatası veya donanım arızası nedeniyle bozulması, B-tree indeksin bazı seyrek erişim kalıplarında boş ya da yarı boş sayfalarla şişmesi, fillfactor gibi bir depolama parametresinin değiştirilmesi ve başarısız eşzamanlı oluşturmadan kalan geçersiz indeks. REINDEX indeks, tablo, şema, veritabanı veya sistem katalogları düzeyinde çalışabilir; CONCURRENTLY ile yazmaları engellemeden yapılır. Bölümlenmiş tablolarda CREATE INDEX varsayılan olarak tüm bölümlere iner; ayrıntılar partitioning yazısında.
Kullanılmayan indeksleri bulmak
Kılavuz, indekslerin bakım veya ayar gerektirmediğini ama gerçek iş yükünde hangilerinin kullanıldığının kontrol edilmesi gerektiğini söyler. Tek bir sorgu için EXPLAIN, sunucu genelinde ise istatistik görünümleri kullanılır. pg_stat_user_indexes görünümündeki idx_scan sütunu indeksin kaç kez tarandığını sayar. EXPLAIN (ANALYZE olmadan) sorguyu çalıştırmadığı için sayaca katkı yapmaz; aşağıdaki sayılar üç gerçek sorgudan sonra alındı:
SELECT count(*) FROM siparis WHERE musteri_id = 7; -- sonuç: 10
SELECT count(*) FROM siparis WHERE etiketler @> ARRAY['hediye']; -- sonuç: 28571
SELECT count(*) FROM siparis WHERE tarih = '2025-03-01'; -- sonuç: 300-- Birkaç gerçek sorgu çalıştırıldıktan sonra (yeni bir oturumda):
SELECT indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE relname = 'siparis'
ORDER BY idx_scan DESC, indexrelname; indexrelname | idx_scan
---------------------------+----------
siparis_etiket_gin | 1
siparis_musteri_idx | 1
siparis_tarih_brin | 1
siparis_bekleyen_idx | 0
siparis_eposta_hash | 0
siparis_eposta_lower_idx | 0
siparis_musteri_tutar_idx | 0
siparis_pkey | 0
(8 rows)Gerçek bir sunucuda bu listeyi uzun bir süre boyunca izlemek gerekir; sıfır görünen bir indeks belki ayda bir çalışan bir rapor tarafından kullanılıyordur. Ayrıca benzersizlik veya birincil anahtar sağlayan indeksler (burada siparis_pkey) hiç taranmasa bile kısıtı uyguladıkları için silinmemelidir. İzleme görünümlerinin geneli pg_stat_activity ve pg_stat_statements yazısında.
Kılavuzun indeks denemeleri için verdiği ipuçları da önemlidir: her zaman önce ANALYZE çalıştırın; gerçek veriyle deneyin, çünkü çok küçük test tabloları yanıltıcıdır (100 satırın tamamı tek bir disk sayfasına sığabilir ve onu sıralı okumaktan daha iyi bir plan yoktur); indeks kullanılmıyorsa test amacıyla enable_seqscan gibi ayarları kapatıp planlayıcının neden o indeksi seçmediğini anlamaya çalışın.
Sık yapılan hatalar

- Sorgudaki ifadeyle indeks ifadesini eşleştirmemek:
lower(eposta)indeksieposta = ...koşulunda kullanılmaz. - Kısmi indeks koşulunu parametreyle beklemek: eşleştirme planlama anında yapılır.
- UNIQUE veya PRIMARY KEY sütununa ikinci indeks eklemek: otomatik indeksin kopyası olur.
- Çok sütunlu indekste sütun sırasını önemsememek: baştaki sütunlardaki koşullar taranan bölümü belirler; skip scan yalnızca baştaki sütunda az farklı değer olduğunda işe yarar.
- Rastgele sıralı sütuna BRIN kurmak: BRIN fiziksel sırayla uyumlu verilerde anlamlıdır.
- Üretimde CONCURRENTLY kullanmadan büyük indeks kurmak: yazmalar indeks bitene kadar bekler.
- Başarısız eşzamanlı oluşturmadan kalan INVALID indeksi unutmak: sorguya yaramaz ama yazma maliyeti getirir.
- Her sütuna indeks eklemek: her indeks yazma işlemlerini yavaşlatır ve yer kaplar.
Sık Sorulan Sorular
İndeks oluşturdum ama sorgu hâlâ Seq Scan yapıyor, neden?
Planlayıcı, sorgunun tablonun büyük bir kısmını döndüreceğini tahmin ediyorsa sıralı tarama daha ucuzdur. İstatistikler eski olabilir (ANALYZE çalıştırın), sorgudaki koşul indeks ifadesiyle eşleşmiyor olabilir ya da operatör indeksin operatör sınıfında olmayabilir. Ayrıntılı tanı için EXPLAIN ANALYZE yazısına bakın.
Yabancı anahtar sütununa indeks gerekir mi?
Yabancı anahtarın başvurduğu sütunlar birincil anahtar ya da benzersiz kısıt olmak zorunda olduğu için o tarafta her zaman bir indeks bulunur. Başvuran sütunlar için ise indeks otomatik oluşturulmaz. Kısıtlar bölümü, başvurulan tablodan satır silmenin veya anahtarı güncellemenin başvuran tabloda tarama gerektirdiğini, bu yüzden başvuran sütunları indekslemenin çoğu zaman iyi bir fikir olduğunu belirtir.
Hash mi B-tree mi?
Hash yalnızca eşitlik sorgularında kullanılabilir ve benzersiz olamaz. B-tree eşitliğin yanında aralık, sıralama ve kalıp aramalarını da desteklediği için varsayılan seçimdir.
İndeksin boyutunu nasıl görürüm?
pg_relation_size(indeks_oid) ile bayt cinsinden, pg_size_pretty ile okunabilir biçimde; psql'de \di+ komutu da boyutları listeler.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: PostgreSQL öğrenme rehberi · EXPLAIN ANALYZE · Performans optimizasyonu · JSONB ve GIN · VACUUM ve bloat · SQL sorgu optimizasyonu