LIMIT 10 sonucu sınırlar, ancak on farklı değer bulmak için binlerce girdi satırının incelenmesi gerekebilir. Bu kılavuz, Mac’inizin Terminal uygulamasında sentetik makale konularını kullanarak bu işlemi nasıl teşhis edeceğinizi ve iki yaklaşımı nasıl karşılaştıracağınızı gösterir.
On sonuç neden binlerce satır gerektirebilir?
Bir makale tablosunda 100 konuya dağılmış 100.000 satır ve konu başına 1.000 makale olduğunu varsayalım. Göreviniz alfabetik sıradaki ilk on farklı konu adını almaktır. Sorgunun yinelenen adları elemesi, konu sırasını koruması ve on sonuç döndürmesi gerekir.
PostgreSQL, DISTINCT ifadesini yinelenen çıktı satırlarını kaldırmak, LIMIT ifadesini ise döndürülen sonucu sınırlamak için tanımlar. Bu anlamlar, temeldeki tarama için on satırlık bir bütçe koymaz. Planlayıcı plan seçerken limiti dikkate alır; dolayısıyla fiziksel iş, seçilen yürütme stratejisine bağlıdır. SELECT
topic-001 değerinin 1.000 kez, ardından topic-002 değerinin 1.000 kez yer aldığı sıralı bir akışı ele alalım. Akış tabanlı bir yinelenenleri kaldırma işlemi, dokuz önceki grubun tamamını ve onuncu grubun ilk satırını tükettikten sonra onuncu farklı konuyu görür:
Gerekli girdi satırı = (9 × 1.000) + 1 = 9.001
Döndürülen farklı sonuç = 10
Bu, belirli bir akış stratejisine yönelik açıklayıcı bir hesaplamadır; ölçülmüş bir PostgreSQL planı değildir. Grupların eşit büyüklükte olmaması hesabı değiştirir: İlk konu grubunun çok büyük olması, sonraki gruplar küçük olsa bile iş yükünün çoğunu oluşturabilir.
Uygun bir B-tree indeksi, açık bir sıralama olmadan sıralı girdi sağlayabilir. PostgreSQL belgeleri, indeksli ORDER BY ile LIMIT kullanıldığında erken veri alımından söz eder; ancak yinelenenleri elemek ek bir gereklilik getirir: limit düğümüne yeterli sayıda farklı değer ulaşmalıdır. Bir indeks sıralama işini ortadan kaldırırken yinelenen değerleri işleme yükünü yüksek bırakabilir. Indexes and ORDER BY
Satırları hangi işlemin tükettiğini belirlemek için planı okuyun
Gerçek sorgu ve parametre değerleriyle EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) kullanın. ANALYZE deyimi çalıştırır ve gerçek satır sayılarını bildirir; BUFFERS blok etkinliğini bildirir. Aşağıdaki eksiksiz örnek, sentetik veriler üzerinde bu teşhisi çalıştırır. EXPLAIN
İki yararlı plan örüntüsü aşağıda gösterilmiştir. Bunlar şematik biçimlerdir; yakalanmış bir çıktı veya kurulumunuzun seçeceği plana ilişkin bir garanti değildir.
Sıralı akış yolu:
Limit
Unique
Index Only Scan veya Index Scan
Toplama yolu:
Limit
Sort
HashAggregate
Seq Scan
Sıralı yolda taramanın çıktısını Unique ve Limit çıktılarıyla karşılaştırın. Aradaki büyük fark, yinelenen değerlerin çok olduğunu gösterir. Toplama yolunda toplama düğümü, son sıralama ve limit işlemleri tamamlanmadan önce grupları belirlemek için girdisini tüketir. PostgreSQL’in plan belgeleri, taramaların sıralama ve toplama düğümlerine nasıl girdi sağladığını açıklar. Using EXPLAIN
Gerçek satır sayılarını loops ile birlikte okuyun: Bir düğüm tekrar tekrar çalıştığında satır sayıları, her çalıştırma başına ortalamadır. Düğümün toplam çıktısını tahmin ederken bunları döngü sayısıyla çarpın. Ayrıca sıralamanın altındaki girdiyi inceleyin; sıralama tarafından az sayıda satır üretilmesi, sıralamanın girdisinin de küçük olduğunu göstermez. Using EXPLAIN
Üç ölçümü ayrı tutun: bir düğümün ürettiği satırlar, blok etkinliği ve geçen süre. Tampon sayıları mantıksal satırları değil blokları açıklar; üst düğümlerin tampon toplamları alt düğümleri de içerir. Geçici tablolar yerel tamponları kullanır; sıralama gibi işlemlerde kullanılan geçici çalışma dosyaları temp blokları olarak görünür. EXPLAIN
Karşılaştırmayı Mac’inizde yeniden oluşturun
Güncel bir Node.js LTS sürümü, npm ve mevcut bir yerel PostgreSQL veritabanı kullanın. Bu anlatım, belge tabanı olarak PostgreSQL 18’i kullanır. Program, sonuçları o sürümün belgelerine göre yorumlayabilmeniz için sunucu sürümünüzü yazdırır. node-postgres, npm install pg ile kurulumu ve güncel Node.js ile LTS sürümlerini desteklediğini belgeler. node-postgres
Terminal uygulamasında bir çalışma dizini oluşturup sürücüyü kurun:
mkdir distinct-row-demo
cd distinct-row-demo
npm init -y
npm install pg
Aşağıdaki eksiksiz programı distinct-demo.mjs olarak kaydedin. Geçici bir tabloda sentetik makale satırları oluşturur, sıradan sorguyu indeks eklemeden önce ve sonra ölçer ve açıkça ardıl değer arayan bir sorguyla karşılaştırır. Her sorgunun aynı veritabanı oturumunda çalışması için program boyunca tek bir Client kullanır.
Bağlantı ayarları olağan PostgreSQL ortam değişkenlerinden alınır. Programı çalıştırmadan önce bunları mevcut yerel veritabanınız için ayarlayın. node-postgres hem ortam değişkenleriyle bağlantı kurmayı hem de sorgu değerlerini SQL metninden ayrı geçirmeyi belgeler. Connecting, Queries
import pg from 'pg';
import assert from 'node:assert/strict';
const { Client } = pg;
const client = new Client({ connectionTimeoutMillis: 5000 });
const limit = 10;
const distinctSql = `
SELECT DISTINCT topic FROM demo_articles
ORDER BY topic LIMIT $1::integer
`;
const seekSql = `
WITH RECURSIVE topics(topic, step) AS (
(SELECT topic, 1 FROM demo_articles ORDER BY topic LIMIT 1)
UNION ALL
SELECT n.topic, t.step + 1
FROM topics AS t
CROSS JOIN LATERAL (
SELECT a.topic FROM demo_articles AS a
WHERE a.topic > t.topic ORDER BY a.topic LIMIT 1
) AS n
WHERE t.step < $1::integer
)
SELECT topic FROM topics ORDER BY topic
`;
async function showPlan(label, sql) {
const plan = await client.query(
`EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) ${sql}`, [limit]
);
console.log(`\n${label}`);
console.log(plan.rows.map(row => row['QUERY PLAN']).join('\n'));
}
try {
await client.connect();
const version = await client.query('SHOW server_version');
console.log('PostgreSQL', version.rows[0].server_version);
await client.query(`
CREATE TEMP TABLE demo_articles (
article_id integer NOT NULL,
topic text COLLATE "C" NOT NULL,
title text NOT NULL
)
`);
await client.query(`
INSERT INTO demo_articles (article_id, topic, title)
SELECT n,
'topic-' || lpad((((n - 1) / 1000) + 1)::text, 3, '0'),
'Sample article ' || n::text
FROM generate_series(1, 100000) AS s(n)
`);
await client.query('ANALYZE demo_articles');
await showPlan('Before index', distinctSql);
await client.query(
'CREATE INDEX demo_articles_topic_idx ON demo_articles (topic)'
);
await client.query('ANALYZE demo_articles');
await showPlan('After index', distinctSql);
await showPlan('Successor seeks', seekSql);
const ordinary = await client.query(distinctSql, [limit]);
const seeking = await client.query(seekSql, [limit]);
assert.deepEqual(seeking.rows, ordinary.rows);
assert.equal(ordinary.rows.length, limit);
console.log(ordinary.rows.map(row => row.topic));
} catch (error) {
console.error(error.message);
process.exitCode = 1;
} finally {
await client.end();
}
Veri oluşturucu, PostgreSQL’in belgelenmiş generate_series işlevini kullanarak 1 ile 100.000 arasında tamsayılar üretir. Aritmetik işlem, art arda gelen her 1.000 makalelik gruba aynı konuyu atar. ANALYZE, planlayıcının plan seçerken kullandığı tablo istatistiklerini sağlar. Set Returning Functions, ANALYZE
Mevcut yerel veritabanınızın adını kullanarak programı çalıştırın. Örneğin, veritabanının adı postgres ise:
PGHOST=localhost PGPORT=5432 PGDATABASE=postgres node distinct-demo.mjs
Beklenen konu adları topic-001 ile topic-010 arasındadır; doğrulamalar sonuç sayısını ve iki sorgunun aynı sonuçları vermesini denetler. JavaScript sözdizimi ve akış hesabındaki 9.001 girdi satırı bu makale için kontrol edildi. Araştırma ortamında veritabanında çalıştırma olanağı bulunmadığından, zaman ölçümleri ve yakalanmış PostgreSQL planları bilerek eklenmedi.
Yalnızca indeks taraması yine de neden ciddi iş gerektirebilir?
Index Only Scan, sorgunun istediği sütun değerlerini indeksten alabileceği anlamına gelir. Her farklı sonuç için yalnızca bir indeks girdisi gerektiği anlamına gelmez. Tablonun kendisine nadiren erişilse bile gerçek satır sayısını inceleyin.
Yığın erişimi ayrı bir konudur. PostgreSQL görünürlük haritasını kontrol eder ve ilgili sayfalar tümüyle görünür olarak işaretlenmemişse yine de yığın tuple’larına erişebilir. Bu nedenle yeni eklenmiş demo satırları, indeks-only planında bile yığın getirmelerine yol açabilir. Tarama satırlarının yanında Heap Fetches değerini de kontrol edin. Index-Only Scans and Covering Indexes
INCLUDE ile ek yük sütunları eklemek, indeks-only taramasında daha fazla değerin kullanılabilmesini sağlayabilir; ancak istenen farklı konu sayısını değiştirmez. İndeks kapsamını, gösterilmiş yığın erişimi işini gidermek için seçin; yinelenen değerlerin tüketimini ayrıca ölçün. Index-Only Scans and Covering Indexes
PostgreSQL 18’in skip scan özelliği bunu otomatik olarak çözer mi?
PostgreSQL 18, B-tree skip scan desteğini kullanıma sundu. Belgelenen amacı, sonraki sütunlardaki kısıtların önceki sütun değerleri arasında yinelenen aramalara yön verebildiği bazı çok sütunlu indeks aramalarını iyileştirmektir. Bu özellik 18. sürümde sunuldu; PostgreSQL 17 veya önceki sürümlerde bulunduğunu varsaymayın. Release 18, Multicolumn Indexes
Önde gelen anahtardaki bir farklı değerden diğerine otomatik olarak atlama işlemine genellikle gevşek indeks taraması denir. PostgreSQL projesinin wiki’si bu işlemi koşul odaklı indeks skip scan’den ayırır ve özyinelemeli SQL benzetimini belgeler. Dolayısıyla PostgreSQL 18’in skip scan duyurusu, bu tek sütunlu DISTINCT sorgusu için otomatik gevşek tarama olduğunu göstermez. Loose indexscan
Programdaki ardıl arama sorgusu bu benzetim yöntemini uyarlar: Önce en küçük konuyu bulur, sonra önceki konudan büyük en küçük konuyu tekrar tekrar arar. Özyinelemeli adım, on sonuca ulaşıldığında veya ardıl kalmadığında durur. Uygun bir indeksle her arama, önceki yinelenen değer grubunun ötesine geçebilir. Hızlanma varsaymak yerine sorgunun gerçek planını ve yürütme süresini sıradan sorguyla karşılaştırın. Loose indexscan
Bu örnek, konuların null olmamasını, artan sırayı, belirtilen C karşılaştırma düzenini ve pozitif bir limiti özellikle şart koşar. Sıfır limiti yine de özyinelemeli başlangıç satırını döndürür; dolayısıyla genelleştirilmiş bir uygulamada sıfır ayrı ele alınmalıdır. Null konular özel olarak işlenmelidir; çünkü büyük olma karşılaştırması null değerleri sıralamaz. Makale filtreleri eklerseniz, ilk aramaya ve her ardıl aramasına aynı koşulları uygulayın; aksi hâlde yeniden yazılan sorgu farklı bir küme döndürebilir.
Sonraki değişikliği kanıtlara göre seçin
Planları topladıktan sonra bu karar kılavuzunu kullanın. Sıralama indeksinin ortadan kaldırabileceği işi, yinelenen konu değerlerinden kaynaklanan işten ayırır.
| Gözlemlenen sonuç | Sonra incelenecek konu |
|---|---|
| Bir tarama, toplama ve son sıralama adımına girdi sağlıyor | Mevcut planı, konu sıralamasıyla eşleşen bir B-tree indeksiyle karşılaştırın. |
| Sıralı tarama, on konu için binlerce satır üretiyor | Yinelenen değer gruplarının büyüklüklerini ölçün ve açık ardıl aramalarını karşılaştırın. |
| İndeks-only taraması çok sayıda yığın getirmesi bildiriyor | Görünürlük ve yığın erişimi işini, yinelenen değer sayılarından ayrı inceleyin. |
| Tahminler gerçek satır sayılarından önemli ölçüde farklı | İstatistikleri ANALYZE ile güncelleyin, ardından yeniden ölçün. |
| İndeksli sıradan sorgu zaten düşük maliyetli | Ölçülmüş başka bir gereksinim yeniden yazmayı gerektirmedikçe daha basit sorguyu kullanın. |
LIMIT 10 ifadesini, DISTINCT uygulamasından önce on ham makale seçen bir alt sorguya taşımaktan kaçının. Örnek sıralamada bu on makalenin tümü topic-001 konusuna aittir; bu nedenle yeniden yazılan sorgu on konu yerine tek konu döndürür. Bu, görevi değiştirir.
Farklılık anahtarını da doğru tutun. Seçilen sütunlara article_id eklemek, her makaleyi farklı bir çıktı satırı yaparak konu tekilleştirmesini bozar. Görev, her konu için bir örnek makale seçmeye dönüşürse PostgreSQL’in DISTINCT ON özelliği bu işlemi destekler; bu özelliğin ifadeleri, sıralama ifadelerinin en soldakilerle eşleşmelidir. Bu özellik tek başına daha az satır taranacağını garanti etmez. SELECT
İlk on farklı makale konusunu almak için, taramanın gerçek çıktısını döndürülen on sonuçla karşılaştırarak başlayın. Ardından eşleşen indeksi ve yinelenen grupların baskın olduğu durumlarda sınırlandırılmış ardıl arama sorgusunu deneyin. Verilerinize uygun iş yüküne sahip yaklaşımı seçerken istenen konu kümesini ve sıralamayı koruyun.
Kaynaklar
- SELECT — PostgreSQL Global Development Group.
- Indexes and ORDER BY — PostgreSQL Global Development Group.
- Using EXPLAIN — PostgreSQL Global Development Group.
- EXPLAIN — PostgreSQL Global Development Group.
- node-postgres — node-postgres.
- Connecting — node-postgres.
- Queries — node-postgres.
- Set Returning Functions — PostgreSQL Global Development Group.
- ANALYZE — PostgreSQL Global Development Group.
- Index-Only Scans and Covering Indexes — PostgreSQL Global Development Group.
- Release 18 — PostgreSQL Global Development Group.
- Multicolumn Indexes — PostgreSQL Global Development Group.
- Loose indexscan — PostgreSQL Wiki.