PostgreSQL, id < 11 için indeks kullanıp id > 11 için sıralı tarama yapabilir; çünkü ilk koşul çok az satırla eşleşirken ikincisi tablonun neredeyse tamamıyla eşleşir. Her iki operatör de B-tree indekslerini destekler; planlayıcı tahmini maliyetlerini karşılaştırır.

Bu kılavuz, Mac üzerinde çalışan bir geliştiricinin bu tercihi sentetik makale satırlarıyla yeniden oluşturmasına, planları okumasına ve farkın dikkate alınması gerekip gerekmediğini kontrol etmesine yardımcı olur. PostgreSQL 18’in güncel belgelerini ve node-postgres’in güncel belgelerini temel alır. Plan ayrıntılarını karşılaştırmadan önce sunucu sürümünüzü kontrol edin; örnekler farklı sürümlerde veya makinelerde planların aynı olacağını garanti etmez. PostgreSQL: EXPLAIN Kullanımı

Aynı indeks neden farklı tarama tercihleri doğurabilir?

Bir B-tree indeksi hem küçüktür hem büyüktür karşılaştırmalarını destekler. Karşılaştırma yönünün değişmesi, indeksi kullanılamaz hâle getirmez. Değişen şey, değer aralığı ve dolayısıyla planlayıcının getirilmesini beklediği satır sayısıdır. PostgreSQL: İndeks Türleri

Ardışık 1 ile 100.000 arasındaki tam sayı kimliklerine sahip, tam 100.000 makale satırı içeren örnek bir tabloyu ele alalım:

Koşul Eşleşen satır sayısı Tablo içindeki payı
id < 11 10 0,01%
id > 11 99.989 99,989%
id = 11 1 0,001%

Bu sayılar, mevcut bir veritabanının ölçümlerinden değil, sentetik girdiden elde edilir. Her iki aralık da 11 kimliğini içermez. İlk aralıktaki on eşleşen kimliği indeks verimli biçimde bulabilir. İkinci aralıkta ise zaten neredeyse her satırı bulacaktır.

Sıradan bir PostgreSQL indeks taraması, indeksteki girdileri ve tablonun ana depolama alanında, yani heap’te bulunan karşılık gelen satırları getirir. Bu heap satırları birçok sayfaya dağılmış olabilir. Neredeyse her sayfa gerekiyorsa tabloyu doğrudan okumak, indeks içinde gezinip başvurduğu satırları getirmekten daha düşük maliyetli olabilir. PostgreSQL: Yalnızca İndeks Taramaları ve Kapsayan İndeksler

Dolayısıyla 11 sayısının planlayıcı açısından özel bir anlamı yoktur. Farklı verilerle büyüktür koşulu dar aralığı kapsayabilir. PostgreSQL, tablo istatistiklerini kullanarak seçicilik olarak bilinen eşleşen satır oranını tahmin eder. Sınır değerinin sayısal büyüklüğü tek başına bu oranı belirlemez. PostgreSQL: Planlayıcının Kullandığı İstatistikler

Mac üzerinde karşılaştırmayı nasıl yeniden oluşturabilirsiniz?

Mevcut bir PostgreSQL bağlantısına ve Mac’inizin Terminal uygulamasında kullanılabilen psql komutuna ihtiyacınız var. Kurulum bu çalışmanın kapsamı dışındadır. Her zamanki veritabanı bağlantınızı psql ile açın; bu komut bağlantı ayarlarını kabul eder ve etkileşimli bir SQL istemi sağlar. Aşağıdaki blokları aynı yeni oturumda çalıştırın. PostgreSQL: psql

Kurulum, yalnızca sentetik makale metinleri içeren geçici bir tablo oluşturur. Oturum sona erdiğinde geçici tablolar silinir. Sıradan bir tam sayı birincil anahtarı otomatik olarak benzersiz bir B-tree indeksi oluşturur; bu nedenle id üzerinde ikinci bir indeks gereksizdir. PostgreSQL: CREATE TABLE

SHOW server_version;

CREATE TEMP TABLE article_scan_demo (
    id integer PRIMARY KEY,
    title text NOT NULL,
    body text NOT NULL
);

INSERT INTO article_scan_demo (id, title, body)
SELECT
    g,
    'Article ' || g,
    repeat('Sample article text. ', 20)
FROM generate_series(1, 100000) AS s(g);

ANALYZE article_scan_demo;

SELECT
    count(*) AS total_rows,
    count(*) FILTER (WHERE id < 11) AS below_11,
    count(*) FILTER (WHERE id > 11) AS above_11
FROM article_scan_demo;

PostgreSQL’in tamsayı generate_series işlevi, varsayılan bir artış değeri olan birle birlikte her iki uç noktayı da kapsar. Bu nedenle sayım sorgusu toplam 100.000 satır, 11’den küçük 10 satır ve 11’den büyük 99.989 satır döndürmelidir. PostgreSQL: Küme Döndüren İşlevler

Satırları yükledikten sonra planlayıcının doldurulmuş tabloya ilişkin istatistiklere sahip olması için ANALYZE komutunu çalıştırın. Bu komut istatistikleri toplar; EXPLAIN ANALYZE içindeki yürütme ölçümü seçeneğinden farklı bir amaca hizmet eder. PostgreSQL: ANALYZE

Ardından iki sorgunun tüm satırlarını karşılaştırın:

EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT * FROM article_scan_demo WHERE id < 11;

EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT * FROM article_scan_demo WHERE id > 11;

Bu komutlar her sorguyu yürütür ve seçilen tüm makale satırlarını döndürmeden planı ve gerçek satır sayılarını bildirir. BUFFERS blok etkinliğini ekler; TIMING OFF tek tek düğümlerin zamanlamasını devre dışı bırakır. Bu, yürütmeyi incelemek için yararlıdır; ancak eşleşen tüm makale verilerini bir masaüstü uygulamasına göndermek için gereken süreyi ölçmez. PostgreSQL: EXPLAIN

Olağan ayarlarda dar aralık için indeks tabanlı bir plan, geniş aralık içinse sıralı tarama bekleyin. Bunları kesin sonuçlar değil, beklentiler olarak değerlendirin: PostgreSQL tahminlerine göre bitmap planı veya geçerli başka bir plan seçebilir. Bu örneğin SQL’i incelendi, ancak hazırlık sırasında çalıştırılabilecek bir PostgreSQL sunucusu yoktu; satır sayısı hesabı ayrıca doğrulandı.

Planın hangi bölümleri bu tercihi açıklar?

Tarama düğümüne, tahmini rows değerine ve gerçek rows değerine bakarak başlayın. İlk sorguda Index Cond içeren Index Scan ya da Bitmap Heap Scan altında bir Bitmap Index Scan arayın. İkinci sorguda Filter içeren Seq Scan arayın. Bitmap planı da indeks kullanır; heap sayfalarını getirmeden önce eşleşen satır konumlarını düzenler. PostgreSQL: EXPLAIN Kullanımı

Ardından şu gözlemleri kontrol edin:

Bu örnekteki gözlem İncelenecek konu
Tahminler, gerçek 10 ve 99.989 satır sayılarıyla kabaca eşleşiyor Eşleşen satır sayılarındaki fark, farklı tercihleri açıklar.
Tahmini satır sayıları gerçeklerden önemli ölçüde farklı Planlayıcı ayarlarını değiştirmeden önce istatistikleri yenileyin.
Çok daha küçük bir örnekte her iki koşul da sıralı tarama kullanıyor Tablo boyutu doğrudan taramayı daha ucuz hâle getirebilir.
Bitmap planı görünüyor Başka bir indeks tabanlı seçenek olarak satır tahminlerini ve heap işlemlerini karşılaştırın.

Planlayıcı maliyetleri milisaniye cinsinden değil, keyfî birimlerle ifade edilen tahminlerdir. 100 maliyet değerini süre olarak yorumlamak yerine bunları PostgreSQL’in modelinin bir parçası olarak karşılaştırın. Örnek geçici tablolar kullandığından, arabellek etkinliği yerel bloklar altında görünür; normal tablo ve indeks arabellekleri ise paylaşılan bloklar altında görünür. PostgreSQL: EXPLAIN

Tahminler şaşırtıcı görünüyorsa aynı oturumda bu tanılama sorgusuyla devam edin:

SELECT
    schemaname,
    n_distinct,
    histogram_bounds,
    correlation
FROM pg_stats
WHERE schemaname = (
    SELECT nspname
    FROM pg_namespace
    WHERE oid = pg_my_temp_schema()
)
  AND tablename = 'article_scan_demo'
  AND attname = 'id';

Histogram, aralık tahmini için kullanılan değer dağılımını açıklar. Korelasyon, sütunun mantıksal sırasının fiziksel satır sırasını ne kadar yakından izlediğini gösterir; her iki uca yakın değerler, heap’e indeks erişimini daha az maliyetli gösterebilir. Bu alanların hiçbiri PostgreSQL’in taramaları değiştirmesi gereken evrensel bir yüzde belirlemez. PostgreSQL: pg_stats

node-postgres üzerinden sorguları nasıl karşılaştırabilirsiniz?

node-postgres, sorgu metnini ve parametre değerlerini planlamanın yapıldığı PostgreSQL’e gönderir. Sorgu API’si SQL’i ve ardından bir değerler dizisini kabul eder; bu nedenle her iki karşılaştırmada da 11 sınır değeri kullanılabilir. node-postgres: Sorgular

Bağımsız bir Node.js yeniden üretimi için çalışan bir Node.js/npm kurulumuna ve mevcut PostgreSQL bağlantı ayarlarınıza ihtiyacınız var. Yeni bir proje dizininde npm init -y komutunu, ardından npm install pg komutunu çalıştırın. Aşağıdaki programın tamamını compare-scans.mjs olarak kaydedip node compare-scans.mjs komutuyla çalıştırın. İstemci, PGHOST, PGPORT ve PGDATABASE gibi PostgreSQL ortam ayarlarını kullanır; mevcut yerel veritabanınızın ayarlarını kullanın. node-postgres: Bağlanma

import pg from 'pg';

const client = new pg.Client();

try {
  await client.connect();

  await client.query(`
    CREATE TEMP TABLE article_scan_demo (
      id integer PRIMARY KEY,
      title text NOT NULL,
      body text NOT NULL
    )
  `);

  await client.query(`
    INSERT INTO article_scan_demo (id, title, body)
    SELECT
      g,
      'Article ' || g,
      repeat('Sample article text. ', 20)
    FROM generate_series(1, 100000) AS s(g)
  `);

  await client.query('ANALYZE article_scan_demo');

  const queries = [
    'EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT * FROM article_scan_demo WHERE id < $1',
    'EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT * FROM article_scan_demo WHERE id > $1'
  ];

  for (const text of queries) {
    const result = await client.query(text, [11]);
    console.log(text);
    console.log(
      result.rows.map(row => row['QUERY PLAN']).join('\n')
    );
  }
} finally {
  await client.end();
}

Program kendi örnek tablosunu oluşturur ve tüm işlemleri tek bir bağlantıda yürütür. Her iki planı yazdırır ve ardından bağlantıyı kapatır. JavaScript söz dizimi Node.js ile kontrol edildi; hazırlık sırasında veritabanında yürütme yapılmadı. Bağlantı kurulumu başarısız olursa tarama davranışını yorumlamadan önce bu sorunu çözün.

Adlandırılmış prepared statement’lar karşılaştırmayı değiştirebilir mi?

node-postgres sorgu yapılandırmasına name eklemek, her bağlantıda adlandırılmış bir prepared statement oluşturur. Bu, başka bir ayrım doğurur: PostgreSQL parametre değerlerine göre özel bir plan ya da yürütmeler arasında paylaşılan genel bir plan kullanabilir. node-postgres: Sorgular, PostgreSQL: PREPARE

PostgreSQL 18’in varsayılan otomatik politikasında ilk beş yürütme özel plan kullanır. PostgreSQL daha sonra genel planın tahmini maliyetini özel planların ortalama maliyetiyle karşılaştırır; altıncı yürütmede otomatik olarak genel plana geçmez. PostgreSQL: PREPARE

Bir uygulamadaki adlandırılmış statement, doğrudan SQL testinden farklı davranıyorsa aynı bağlantıda EXPLAIN EXECUTE kullanarak bu statement’ı inceleyin. Genel plan parametre sembollerini gösterirken özel plan, sağlanan değerleri kullanır. SQL metinleri farklı olduğundan iki karşılaştırma operatörünü ayrı adlandırılmış statement’larda tutun. PostgreSQL: PREPARE

Sorguyu veya planlayıcı ayarlarını ne zaman değiştirmelisiniz?

Önce sorgunun gerçek amacını koruyun. SELECT * ifadesini SELECT id ile değiştirmek, PostgreSQL’in getirmesi gereken veriyi değiştirir. Gerekli tüm sütunlar uygun bir indekste mevcutsa yalnızca indeks taraması mümkün hâle gelir. Heap erişiminden kaçınmak görünürlük haritasına da bağlıdır; dolayısıyla yalnızca indeks taraması planı, heap’e hiç erişilmeyeceğini garanti etmez. PostgreSQL: Yalnızca İndeks Taramaları ve Kapsayan İndeksler

Görev eşleşen tüm makaleleri gerektiriyorsa planları karşılaştırırken bu gereksinimi koruyun. Yalnızca sıralanmış küçük bir alt küme gerekiyorsa bunun yerine gerçek ORDER BY ve LIMIT sorgusunu test edin: sıralama ve erken sonlandırma, tercih edilen planı değiştirebilir. PostgreSQL: EXPLAIN Kullanımı

İstatistikleri kontrol ettikten sonra mevcut SQL oturumunda alternatif bir planı karşılaştırabilirsiniz:

BEGIN;
SET LOCAL enable_seqscan = off;

EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
SELECT * FROM article_scan_demo WHERE id > 11;

ROLLBACK;

Bu ayar sıralı taramaları caydırır; ancak onları tamamen engelleyemez. Sonucu tanısal bir karşılaştırma olarak kullanın. PostgreSQL ayrıca sıralı ve sıralı olmayan sayfa erişimini seq_page_cost ve random_page_cost üzerinden modeller. Göreli değerlerini değiştirmek plan tercihlerini değiştirebilir; ancak belgeler maliyet ayarlarını birkaç deneyle değil, sorgu iş yükünün tamamında değerlendirmeyi önerir. PostgreSQL: Sorgu Planlaması

Bu sentetik tablo için belirleyici karşılaştırma, eşleşen 10 satıra karşı 99.989 satırdır. Tam sorguyu kontrol edin, istatistikleri yenileyin, tahmini ve gerçek satır sayılarını karşılaştırın; gerektiğinde prepared-plan davranışını inceleyin. Bu adımlar, sıralı taramanın istenen işi karşılayıp karşılamadığını belirler ve daha ileri değişiklikler için kanıt sağlar.

Kaynaklar