PostgreSQL pgvector HNSW: Production RAG Rehberi
pgvector HNSW'yi production'da güvenle kurun: maintenance_work_mem, WAL, Patroni replikasyonu, kilitler, hybrid search ve teşhis sorguları.

📌 PostgreSQL 17 + pgvector HNSW + Patroni Production RAG Referans Mimari Şeması (Raft Quorum, DCS, TOAST, Sync/Async WAL Stream)
1. Giriş ve Mimari Genel Bakış
pgvector, embedding'leri ayrı bir vektör veritabanına (Pinecone, Qdrant, Milvus) kopyalamak yerine operasyonel verinin yanında, aynı transaction sınırları içinde tutmanızı sağlar. Çift yazma, iki sistem arasında tutarlılık reconciliation'ı ve dağıtık transaction maliyeti ortadan kalkar. Bedeli ise şudur: vektör indeksi artık PostgreSQL'in buffer manager'ı, WAL hattı, checkpointer'ı, vacuum'u ve fiziksel replikasyonu ile aynı kaynakları paylaşır. Vektör araması "ayrı bir sistem" olmaktan çıkar ve veritabanınızın I/O ve bellek bütçesinin en ağır tüketicilerinden biri hâline gelir.
Bu makalede HNSW indeksinin yaşam döngüsünü dört eksende ele alıyoruz:
- •İnşa (build): Graf
maintenance_work_memiçinde kurulur. Sığmazsa pgvector doğrudan disk üzerindeki indekse ekleme moduna geçer; inşa süresi ve üretilen WAL hacmi dramatik biçimde artar. - •Sorgu (search): Graf gezintisi entry-point'ten başlar ve her adımda komşu listeleri okunur. Erişim deseni random I/O'dur; indeks
shared_buffersve OS page cache'e sığmıyorsa latency NVMe random read IOPS'una bağlanır. - •Yazma (DML): HOT olmayan her
UPDATEve herINSERT, HNSW grafına tam bir ekleme (ef_constructionderinliğinde arama + komşu listesi güncellemesi) anlamına gelir. Bu işlemler buffer content LWLock'ları üzerinde yarışır. - •Replikasyon: İndeksin her baytı WAL olarak standby'lara akar. Senkron replikasyonda bu, primary'deki tüm commit'lerin latency'sine doğrudan yansır.
2. Senaryo ve Production Ortamı
Ortam:
| Bileşen | Değer |
|---|---|
| Donanım | 32 vCPU, 256 GB RAM, NVMe RAID10 |
| Yazılım | PostgreSQL 17, pgvector 0.8.x, Patroni (1 primary, 1 sync standby, 1 async DR standby) |
| Veri | Çok kiracılı (multi-tenant) RAG platformu, ~450 tenant, 12 milyon chunk |
| Embedding | text-embedding-3-small, 1536 boyut, halfvec(1536) olarak saklanıyor |
| Dağılım | En büyük 5 tenant verinin %60'ını tutuyor; yüzlerce tenant'ın birkaç bin chunk'ı var |
Olay: Ekip, yeni HNSW indeksini gece bakım penceresinde düz CREATE INDEX ile ve varsayılan maintenance_work_mem = 64MB ile başlattı. Sonuç:
- 1Graf belleğe sığmadığı için pgvector ilk birkaç on bin tuple'dan sonra on-disk inşa moduna düştü. İnşa 9 saat sürdü.
- 2
CREATE INDEXtablo üzerindeShareLocktuttuğu için ingest pipeline'ın tümINSERT'leri bloke oldu; kuyruk sabaha kadar birikti. - 3On-disk modda her ekleme ayrı WAL kaydı ürettiği için yaklaşık 70 GB WAL oluştu. Sync standby yüzünden primary'deki tüm commit'lerin p99 latency'si 4 ms'den 180 ms'e çıktı. Async DR standby 40 dakika geride kaldı.
- 4İndeks hazır olduktan sonra küçük tenant'lar
LIMIT 5istediği hâlde 0–2 sonuç almaya başladı. - 5Retriever her erişimde
hit_countvelast_accessed_atalanlarını aynı tabloda güncelliyordu. Bir hafta içinde indeks %35 şişti, autovacuum HNSW indeksinde saatlerce çalıştı.
Aşağıdaki adımlar bu tablonun her maddesini sistematik olarak ortadan kaldırmak için tasarlanmıştır.
3. Adım Adım Uygulama ve Konfigürasyon
▸3.1. postgresql.conf Parametreleri
256 GB RAM'li senaryo sunucusu için başlangıç değerleri:
# --- Bellek ---
shared_buffers = 64GB # RAM'in %25'i; huge_pages ile birlikte
huge_pages = on # 64GB shared_buffers için TLB baskısını azaltır
effective_cache_size = 180GB # shared_buffers + beklenen OS page cache
work_mem = 32MB # hybrid search sıralamaları için; bağlantı sayısıyla çarpılır
maintenance_work_mem = 2GB # global varsayılan; HNSW build'de OTURUM bazında yükseltilir
# --- Paralellik ---
max_worker_processes = 32
max_parallel_workers = 24
max_parallel_maintenance_workers = 8 # HNSW parallel build (pgvector 0.6.0+)
max_parallel_workers_per_gather = 4 # metadata filtreli exact taramalar için
# --- I/O ---
random_page_cost = 1.1 # NVMe; graf gezintisi random I/O'dur
effective_io_concurrency = 200
# --- WAL ve Checkpoint ---
wal_compression = zstd # PG15+; full-page image hacmini ciddi düşürür
max_wal_size = 64GB # build sırasında WAL kaynaklı checkpoint fırtınasını önler
min_wal_size = 8GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9
# --- Standby tarafı ---
hot_standby_feedback = on # standby'daki uzun okuma sorgularının iptalini azaltır
max_standby_streaming_delay = 30s
# --- Eklentiler ---
shared_preload_libraries = 'pg_stat_statements,pg_prewarm'
pg_prewarm.autoprewarm = on # restart/failover sonrası HNSW sayfalarını geri yükler| Parametre | Varsayılan | Production Önerisi | Boyutlandırma Mantığı |
|---|---|---|---|
shared_buffers | 128MB | RAM'in %25–%40'ı | Sıcak HNSW indeksi + aktif heap, shared_buffers ile OS page cache toplamına sığmalıdır. |
maintenance_work_mem | 64MB | Global 1–2 GB, build oturumunda tahmini graf boyutu kadar | Sabit bir tavan formülü yerine örneklemden ölçülen indeks boyutuna göre ayarlanır (bkz. 3.3). |
hnsw.ef_search | 40 | 40–200, iş yüküne göre | Recall/latency ödünleşimi. LIMIT değeri ef_search'ü aşarsa indeks en fazla ef_search kadar aday döndürür. |
max_parallel_maintenance_workers | 2 | çekirdek sayısı − 2, en fazla 8 | Leader process de çalıştığı için toplam süreç sayısı bir fazladır. max_parallel_workers ile sınırlanır. |
max_parallel_workers_per_gather | 2 | 4 | HNSW taraması paralel değildir; bu parametre filtreli exact scan ve hybrid sorgulardaki seq/bitmap taramayı hızlandırır. |
effective_cache_size | 4GB | RAM'in %50–%75'i | Planner'ın indeks yolunu doğru maliyetlemesi için. Bellek ayırmaz, yalnızca maliyet modelini etkiler. |
wal_compression | off | zstd veya lz4 | HNSW sayfalarında full-page image oranı yüksektir; sıkıştırma replikasyon ağını rahatlatır. |
Patroni altında parametreler postgresql.conf dosyasına elle değil, DCS üzerinden uygulanır:
patronictl -c /etc/patroni/patroni.yml edit-config \
-p 'shared_buffers=64GB' \
-p 'huge_pages=on' \
-p 'effective_cache_size=180GB' \
-p 'maintenance_work_mem=2GB' \
-p 'max_parallel_maintenance_workers=8' \
-p 'wal_compression=zstd' \
-p 'max_wal_size=64GB' \
-p "shared_preload_libraries='pg_stat_statements,pg_prewarm'" \
--force
# shared_buffers, huge_pages ve shared_preload_libraries restart gerektirir
patronictl -c /etc/patroni/patroni.yml list
patronictl -c /etc/patroni/patroni.yml restart rag-cluster --pending --forcehnsw.ef_search gibi eklenti parametreleri veritabanı veya rol seviyesinde tanımlanmalıdır:
ALTER DATABASE ragdb SET hnsw.ef_search = 100;
ALTER ROLE rag_batch_reader SET hnsw.ef_search = 200; -- offline değerlendirme işleri▸3.2. Şema Tasarımı: Sık Güncellenen Alanları Ayırın
pgvector paketini tüm Patroni node'larına kurduktan sonra eklentiyi primary üzerinde oluşturun. Standby'da paket eksikse WAL replay devam eder, ancak failover sonrası tüm vektör sorguları kütüphane bulunamadığı için hata verir.
# Her node üzerinde (örnek: PGDG paketleri, Debian/Ubuntu)
sudo apt-get install -y postgresql-17-pgvectorCREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
SELECT extname, extversion FROM pg_extension WHERE extname IN ('vector','pg_buffercache','pg_prewarm');-- Ana tablo: embedding ve nadiren değişen içerik
CREATE TABLE documents_rag (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id integer NOT NULL,
document_id uuid NOT NULL,
chunk_no integer NOT NULL,
chunk_text text NOT NULL,
metadata jsonb,
embedding halfvec(1536) NOT NULL, -- 2 x 1536 + 8 = 3080 bayt/değer
embedding_model text NOT NULL DEFAULT 'text-embedding-3-small',
created_at timestamptz NOT NULL DEFAULT now()
) WITH (fillfactor = 90);
-- Sık güncellenen sayaçlar AYRI tabloda: HNSW indeksine asla dokunmaz
CREATE TABLE documents_rag_stats (
chunk_id bigint PRIMARY KEY REFERENCES documents_rag(id) ON DELETE CASCADE,
hit_count bigint NOT NULL DEFAULT 0,
last_accessed_at timestamptz
) WITH (fillfactor = 70); -- HOT update için sayfa içi boşluk
CREATE INDEX CONCURRENTLY idx_documents_rag_tenant_doc
ON documents_rag (tenant_id, document_id);
-- Agresif autovacuum: dead tuple'lar HNSW taramasında aday slotlarını boşa harcar
ALTER TABLE documents_rag SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_limit = 2000
);
ALTER TABLE documents_rag_stats SET (autovacuum_vacuum_scale_factor = 0.01);halfvec(1536) değeri yaklaşık 3 KB'tır ve TOAST eşiğini (yaklaşık 2 KB) aşar; yani satırın embedding kısmı genellikle out-of-line TOAST'ta yaşar, heap tuple'ı küçük kalır. Bu, indekslenmeyen sütun güncellemelerinin HOT olabilmesi için iyi bir zemindir. Storage stratejisini doğrulayın; float verisi zayıf sıkıştığı için e (EXTERNAL, sıkıştırmasız) beklenir:
SELECT attname, atttypid::regtype, attstorage
FROM pg_attribute
WHERE attrelid = 'documents_rag'::regclass AND attname = 'embedding';vector tipi için en fazla 2.000, halfvec için en fazla 4.000 boyutu indeksleyebilir. 3072 boyutlu modeller (text-embedding-3-large gibi) için halfvec bir optimizasyon değil, zorunluluktur.▸3.3. İndeks Boyutunu Örneklem Üzerinden Tahmin Edin
maintenance_work_mem için sabit bir formül (ör. "RAM'in %25'i, en fazla 16 GB") HNSW'de yanıltıcıdır. Belirleyici olan, grafın bellekteki boyutudur ve bu yaklaşık olarak indeksin disk boyutuna eşittir. Kaba hesap:
satır sayısı × (2 × boyut + 8 + 12 × m + ~100 bayt sayfa/tuple overhead)
12 M satır, 1536 boyut, m = 24 için bu yaklaşık 12 M × 3.5 KB ≈ 42 GB eder; sayfa doluluk kaybıyla pratikte 45–50 GB görülür. Tahmini ölçümle doğrulayın. Örneklemi UNLOGGED oluşturmak WAL üretimini önler:
CREATE UNLOGGED TABLE rag_sample AS
SELECT * FROM documents_rag TABLESAMPLE SYSTEM (1);
SET maintenance_work_mem = '4GB';
SET max_parallel_maintenance_workers = 4;
CREATE INDEX rag_sample_hnsw
ON rag_sample USING hnsw (embedding halfvec_cosine_ops)
WITH (m = 24, ef_construction = 128);
SELECT
(SELECT count(*) FROM rag_sample) AS sample_rows,
pg_size_pretty(pg_relation_size('rag_sample_hnsw')) AS sample_index_size,
pg_size_pretty(
pg_relation_size('rag_sample_hnsw')
* (SELECT reltuples FROM pg_class WHERE relname = 'documents_rag')
/ (SELECT count(*) FROM rag_sample)
) AS projected_full_size;
DROP TABLE rag_sample;m ve ef_construction değerleri hakkında: m = 24 (varsayılan 16) yüksek boyutlu embedding'lerde recall'u iyileştirir ve komşu listesi başına ek ~100 bayt maliyet getirir; 3 KB'lık vektörün yanında bu ihmal edilebilir. ef_construction = 128 (varsayılan 64) graf kalitesini artırır, inşa süresini yaklaşık lineer olarak uzatır.
▸3.4. Pre-flight Kontroller
PGDATA=$(psql -Atc "SHOW data_directory")
# pg_wal için en az tahmini indeks boyutunun 1.5 katı boş alan
df -h "$PGDATA/pg_wal"
du -sh "$PGDATA/pg_wal"-- Replikaların mevcut durumu: build'e lag ile başlamayın
SELECT application_name, state, sync_state,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_gap,
replay_lag
FROM pg_stat_replication;
-- CREATE INDEX CONCURRENTLY, eski snapshot tutan TÜM transaction'ları bekler
SELECT pid, usename, application_name, state,
now() - xact_start AS xact_age,
backend_xmin,
left(query, 80) AS query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
AND pid <> pg_backend_pid()
ORDER BY xact_start NULLS LAST
LIMIT 10;
-- Unutulmuş replication slot'lar WAL'ı tutar ve disk dolmasına yol açar
SELECT slot_name, active, wal_status,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;▸3.5. İndeksi CONCURRENTLY ile İnşa Edin
CREATE INDEX CONCURRENTLY transaction bloğu içinde çalışamaz ve bağlantı koparsa geride INVALID bir indeks bırakır. Bu yüzden tmux / screen içinde, ON_ERROR_STOP ile ve izlenebilir bir application_name vererek çalıştırın. Komutu Patroni lideri üzerinde çalıştırdığınızdan emin olun:
patronictl -c /etc/patroni/patroni.yml list # Leader node'u doğrula
tmux new -s hnsw_build
PGAPPNAME=hnsw_build psql -d ragdb -v ON_ERROR_STOP=1 <<'SQL'
\timing on
SET maintenance_work_mem = '48GB'; -- örneklemden projekte edilen boyut + pay
SET max_parallel_maintenance_workers = 7; -- 7 worker + 1 leader
SET statement_timeout = 0;
SELECT pg_current_wal_lsn() AS wal_start \gset
CREATE INDEX CONCURRENTLY idx_documents_rag_embedding_hnsw
ON documents_rag
USING hnsw (embedding halfvec_cosine_ops)
WITH (m = 24, ef_construction = 128);
SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), :'wal_start')) AS wal_generated,
pg_size_pretty(pg_relation_size('idx_documents_rag_embedding_hnsw')) AS index_size;
SQLGraf bellek bütçesini aşarsa pgvector aşağıdaki gibi bir NOTICE üretir. Bu mesajı görürseniz build'i iptal etmeyi ve daha yüksek bellekle yeniden başlatmayı ciddi biçimde değerlendirin; on-disk moddan sonraki ilerleme hem çok daha yavaştır hem de çok daha fazla WAL üretir:
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 4183519 tuples
DETAIL: Building will take significantly more time.
HINT: Increase maintenance_work_mem to speed up builds.temp_bytes metriği bu durumu göstermez, NOTICE ve ilerleme hızındaki çöküş gösterir./dev/shm, büyük build'lerde *"could not resize shared memory segment"* hatasına yol açar. --shm-size veya Kubernetes'te emptyDir.medium: Memory ile yeterli alan tanımlayın.▸3.6. Başarısız Build Temizliği
SELECT indexrelid::regclass AS index_name, indisvalid, indisready, indislive
FROM pg_index
WHERE indrelid = 'documents_rag'::regclass
AND NOT indisvalid;
-- INVALID indeks DML'de yine güncellenir: yazma maliyeti öder, sorguda kullanılmaz
DROP INDEX CONCURRENTLY IF EXISTS idx_documents_rag_embedding_hnsw;▸3.7. Cache Warm-up
-- Primary üzerinde
SELECT pg_prewarm('idx_documents_rag_embedding_hnsw');# Okuma trafiği alan her standby'da (pg_prewarm read-only çalışır)
for host in pg-standby-1 pg-standby-2; do
psql -h "$host" -d ragdb -Atc "SELECT pg_prewarm('idx_documents_rag_embedding_hnsw');"
done▸3.8. Sorgu Tuning: ef_search ve Iterative Index Scan
Temel RAG sorgusu; ef_search transaction seviyesinde belirlenir:
BEGIN;
SET LOCAL hnsw.ef_search = 100;
EXPLAIN (ANALYZE, BUFFERS)
SELECT id,
chunk_text,
1 - (embedding <=> '[0.012,-0.023,...]'::halfvec(1536)) AS similarity
FROM documents_rag
ORDER BY embedding <=> '[0.012,-0.023,...]'::halfvec(1536)
LIMIT 5;
COMMIT;Metadata filtresi eklendiğinde HNSW önce ef_search kadar aday bulur, filtre sonra uygulanır. Verinin %1'ini tutan bir tenant için ef_search = 40 ile beklenen sonuç sayısı 0.4'tür. Senaryodaki *"küçük tenant 0–2 sonuç alıyor"* şikayetinin kaynağı budur. pgvector 0.8.0 ile gelen iterative index scan, yeterli sonuç bulunana kadar taramayı genişletir:
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SET LOCAL hnsw.iterative_scan = strict_order; -- sonuçlar mesafeye göre kesin sıralı
SET LOCAL hnsw.max_scan_tuples = 50000; -- varsayılan 20000; tarama üst sınırı
SELECT id, chunk_text
FROM documents_rag
WHERE tenant_id = 42
ORDER BY embedding <=> '[0.012,-0.023,...]'::halfvec(1536)
LIMIT 5;
COMMIT;relaxed_order daha az tuple tarar ama sıralamayı hafifçe bozabilir; sonuçları materialized CTE ile yeniden sıralayın:
BEGIN;
SET LOCAL hnsw.iterative_scan = relaxed_order;
WITH ann AS MATERIALIZED (
SELECT id, chunk_text,
embedding <=> '[0.012,-0.023,...]'::halfvec(1536) AS distance
FROM documents_rag
WHERE tenant_id = 42
ORDER BY distance
LIMIT 20
)
SELECT id, chunk_text, distance
FROM ann
ORDER BY distance
LIMIT 5;
COMMIT;Çarpık tenant dağılımında kalıcı çözüm; büyük tenant'lar için partial HNSW indeksi, küçük tenant'lar için B-tree ile exact taramadır. Birkaç bin satırlık bir tenant'ta exact scan hem %100 recall verir hem de milisaniyeler içinde biter:
-- Verinin büyük kısmını tutan tenant için ayrı graf
CREATE INDEX CONCURRENTLY idx_rag_hnsw_tenant_42
ON documents_rag USING hnsw (embedding halfvec_cosine_ops)
WITH (m = 24, ef_construction = 128)
WHERE tenant_id = 42;WHERE tenant_id = 42 koşulunu plan zamanında kanıtlaması gerekir. Driver'ınız generic plan'a geçtiğinde (tenant_id = $1) partial indeks devre dışı kalır. Bu sorgular için oturumda plan_cache_mode = force_custom_plan ayarlayın.4. İzleme ve Teşhis (Diagnostic Queries)
▸4.1. İndeks İnşa İlerlemesi
SELECT p.pid,
a.application_name,
p.phase,
p.tuples_done,
p.tuples_total,
round(100.0 * p.tuples_done / nullif(p.tuples_total, 0), 1) AS pct_done,
now() - a.query_start AS elapsed,
a.wait_event_type,
a.wait_event
FROM pg_stat_progress_create_index p
JOIN pg_stat_activity a USING (pid);phase değeri *waiting for old snapshots* gibi bir bekleme aşamasında takılı kalıyorsa, sorun pgvector değil, bir önceki pre-flight sorgusunun gösterdiği uzun transaction'dır. tuples_done artış hızı birden düşüyorsa graf maintenance_work_mem sınırını aşmıştır.
▸4.2. CIC'i Bloke Eden Oturumlar
SELECT a.pid,
pg_blocking_pids(a.pid) AS blocked_by,
a.wait_event_type,
a.wait_event,
left(a.query, 60) AS query
FROM pg_stat_activity a
WHERE a.application_name = 'hnsw_build'
OR a.pid = ANY (
SELECT unnest(pg_blocking_pids(pid))
FROM pg_stat_activity
WHERE application_name = 'hnsw_build'
);▸4.3. İndeks Boyutu ve Kullanımı
SELECT s.indexrelname AS index_name,
pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,
s.idx_scan,
s.idx_tup_read,
s.idx_tup_fetch,
s.last_idx_scan -- PG16+
FROM pg_stat_user_indexes s
WHERE s.relname = 'documents_rag'
ORDER BY pg_relation_size(s.indexrelid) DESC;▸4.4. İndeksin shared_buffers İçindeki Payı
SELECT c.relname,
count(*) AS buffers,
pg_size_pretty(count(*) * current_setting('block_size')::bigint) AS cached,
pg_size_pretty(pg_relation_size(c.oid)) AS total,
round(100.0 * count(*) * current_setting('block_size')::bigint
/ nullif(pg_relation_size(c.oid), 0), 2) AS pct_cached,
round(avg(b.usagecount), 2) AS avg_usagecount
FROM pg_buffercache b
JOIN pg_class c
ON b.relfilenode = pg_relation_filenode(c.oid)
AND b.reldatabase = (SELECT oid FROM pg_database WHERE datname = current_database())
WHERE c.relname IN ('idx_documents_rag_embedding_hnsw', 'documents_rag')
GROUP BY c.relname, c.oid;reldatabase filtresi kritiktir: aynı relfilenode değeri farklı veritabanlarında tekrar edebilir ve filtresiz join yanlış oranlar üretir. pct_cached %90'ın altındaysa ve latency dalgalıysa OS page cache'i vmtouch ile kontrol edin.
▸4.5. İndeks Seviyesinde Cache Hit Oranı
SELECT indexrelname,
idx_blks_hit,
idx_blks_read,
round(100.0 * idx_blks_hit / nullif(idx_blks_hit + idx_blks_read, 0), 2) AS hit_pct
FROM pg_statio_user_indexes
WHERE relname = 'documents_rag';▸4.6. HOT Oranı ve Dead Tuple Birikimi
SELECT relname,
n_tup_upd,
n_tup_hot_upd,
round(100.0 * n_tup_hot_upd / nullif(n_tup_upd, 0), 1) AS hot_pct,
n_tup_newpage_upd, -- PG16+
n_live_tup,
n_dead_tup,
last_autovacuum,
autovacuum_count
FROM pg_stat_user_tables
WHERE relname IN ('documents_rag', 'documents_rag_stats');documents_rag üzerinde n_tup_upd hızla artıyorsa ve hot_pct düşükse, her güncelleme HNSW grafına yeni bir düğüm ekliyor demektir. documents_rag_stats üzerinde ise hot_pct değerinin %90'ın üzerinde olması beklenir.
▸4.7. WAL Üretimi ve Replikasyon Gecikmesi
-- Primary: replika başına gecikme
SELECT application_name,
sync_state,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)) AS send_gap,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_gap,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
-- Kümülatif WAL istatistikleri: wal_fpi yüksekse full-page image baskın
SELECT wal_records, wal_fpi, pg_size_pretty(wal_bytes) AS wal_bytes, stats_reset
FROM pg_stat_wal;-- Standby üzerinde: recovery conflict kaynaklı sorgu iptalleri
SELECT datname, confl_snapshot, confl_lock, confl_bufferpin, confl_deadlock
FROM pg_stat_database_conflicts
WHERE datname = 'ragdb';▸4.8. LWLock Yarışması (Paralel Ingest)
SELECT wait_event_type, wait_event, count(*) AS sessions
FROM pg_stat_activity
WHERE state = 'active'
AND backend_type = 'client backend'
GROUP BY 1, 2
ORDER BY 3 DESC
\watch 1Yoğun paralel INSERT sırasında LWLock / BufferContent baskınsa, writer'lar grafın aynı sayfaları (özellikle entry-point çevresi) üzerinde yarışıyordur. Ingest paralelliğini düşürmek ve satırları tek tek değil COPY veya çok satırlı INSERT ile göndermek daha yüksek throughput verir.
▸4.9. Yavaş Vektör Sorguları
SELECT calls,
round(mean_exec_time::numeric, 2) AS mean_ms,
round(max_exec_time::numeric, 2) AS max_ms,
shared_blks_hit,
shared_blks_read,
round(100.0 * shared_blks_hit
/ nullif(shared_blks_hit + shared_blks_read, 0), 2) AS hit_pct,
left(query, 100) AS query
FROM pg_stat_statements
WHERE query ILIKE '%<=>%' OR query ILIKE '%<->%' OR query ILIKE '%<#>%'
ORDER BY mean_exec_time DESC
LIMIT 10;▸4.10. Recall Ölçümü
Recall'u tahmin etmek yerine ölçün. Aşağıdaki psql bloğu exact (seq scan) sonucu ile HNSW sonucunu karşılaştırır, temp table kullanmadığı için read-only standby'da da çalışır:
SELECT embedding::text AS qv
FROM documents_rag TABLESAMPLE SYSTEM (0.01)
LIMIT 1 \gset
SET enable_indexscan = off;
SELECT array_agg(id) AS exact_ids
FROM (SELECT id FROM documents_rag
ORDER BY embedding <=> :'qv'::halfvec(1536)
LIMIT 10) s \gset
RESET enable_indexscan;
SET hnsw.ef_search = 100;
SELECT cardinality(ARRAY(
SELECT unnest(:'exact_ids'::bigint[])
INTERSECT
SELECT id FROM (SELECT id FROM documents_rag
ORDER BY embedding <=> :'qv'::halfvec(1536)
LIMIT 10) ann
)) / 10.0 AS recall_at_10;▸4.11. Linux Seviyesinde Bellek ve I/O
# 1. NVMe kullanım oranı, kuyruk derinliği ve okuma latency'si (r_await)
iostat -xz 1 nvme0n1
# 2. PostgreSQL süreçlerinin major page fault oranı (majflt/s = diskten okuma)
pidstat -r -p "$(pgrep -d',' -u postgres postgres)" 2 5
# 3. HNSW indeksinin OS page cache'teki payı
PGDATA=$(psql -d ragdb -Atc "SHOW data_directory")
REL=$(psql -d ragdb -Atc "SELECT pg_relation_filepath('idx_documents_rag_embedding_hnsw')")
vmtouch -v "$PGDATA/$REL" "$PGDATA/$REL".[0-9]* 2>/dev/null | tail -n 5
# 4. Build sırasında pg_wal büyümesi
watch -n 5 "du -sh $PGDATA/pg_wal"5. Production Best Practices ve Gotchas
▸5.1. Ne Zaman Yapılmalı, Ne Zaman Kaçınılmalı
| Durum | Öneri |
|---|---|
| İlk toplu yükleme (bulk ingest) | Önce veriyi COPY ile yükleyin, indeksi en sonda kurun. Dolu tabloda tek seferlik build, satır satır graf eklemeden çok daha hızlıdır. |
| Tenant başına birkaç bin satır | HNSW gereksiz; (tenant_id) B-tree + exact scan hem daha doğru hem yeterince hızlıdır. |
| Çarpık tenant dağılımı | Büyük tenant'lar için partial HNSW, kalanlar için exact scan veya tenant grubuna göre list partitioning. |
| Embedding modeli değişimi | Yerinde UPDATE embedding = ... yapmayın. Yeni sütun + yeni indeks + dual-write + okuma geçişi + eski sütunu düşürme. Yerinde güncelleme tüm tabloyu non-HOT yeniden yazar, grafı baştan ekler ve indeks boyutu kadar ek WAL ve bloat üretir. |
| IVFFlat mi HNSW mi | IVFFlat daha hızlı kurulur ve daha az bellek ister, ancak lists merkezleri build anındaki veriye göre belirlenir; dağılım kaydıkça recall düşer ve periyodik rebuild gerekir. Sürekli büyüyen RAG korpusunda HNSW daha öngörülebilirdir. |
| Çalışma kümesi RAM'e sığmıyor | Tek node'da düşük latency hedefi gerçekçi değildir. halfvec, binary quantization ile ön eleme + yeniden sıralama veya partitioning değerlendirin. |
| Yoğun trafik saatleri | CONCURRENTLY DML'i bloke etmez, ancak CPU, I/O ve WAL yükü yine production'a yansır. Build'i düşük trafik penceresine planlayın. |
▸5.2. Kilit Mekanizması
| İşlem | Tablo Kilidi | DML'i Bloke Eder mi? | Dikkat Edilecek Nokta |
|---|---|---|---|
CREATE INDEX | ShareLock | Evet, tüm INSERT/UPDATE/DELETE | SELECT çalışır. HNSW build saatler sürebileceği için production'da kullanılmamalıdır. |
CREATE INDEX CONCURRENTLY | ShareUpdateExclusiveLock (tüm süre boyunca) | Hayır | VACUUM, ANALYZE, başka bir CIC ve çoğu ALTER TABLE ile çakışır. Fazlar arasında eski snapshot'ları bekler. |
REINDEX INDEX CONCURRENTLY | ShareUpdateExclusiveLock | Hayır | Geçici olarak iki indeks birlikte bulunur; disk ve WAL ihtiyacı iki katına çıkar. |
DROP INDEX CONCURRENTLY | ShareUpdateExclusiveLock | Hayır | Transaction bloğu içinde çalışmaz. |
VACUUM (HNSW dahil) | ShareUpdateExclusiveLock | Hayır | HNSW vacuum, silinen düğümlerin komşularını yeniden bağladığı için yavaştır; çok yüksek dead tuple oranında REINDEX CONCURRENTLY daha hızlı olabilir. |
ALTER TABLE ... ADD COLUMN embedding halfvec(1536) | AccessExclusiveLock (kısa) | Evet, kısa süreli | Default'suz nullable sütun anlıktır. Mutlaka SET lock_timeout = '3s' ile ve retry döngüsüyle çalıştırın. |
INSERT / non-HOT UPDATE | RowExclusiveLock | Hayır | Graf sayfalarında BufferContent LWLock yarışması; yüksek paralellikte CPU spin artar. |
▸5.3. Replikasyon Üzerindeki Etkiler
- •WAL hacmi ve senkron commit latency'si: Graf
maintenance_work_memiçine sığdığında pgvector indeks sayfalarını build sonunda toplu olarak WAL'a yazar; WAL hacmi kabaca indeks boyutu kadardır. Sığmadığında her düğüm eklemesi ayrı WAL kaydı üretir ve toplam hacim katlanır. Senkron standby varken bu WAL dalgası boyunca primary'deki tüm commit'ler standby'ın flush'ını bekler. - •Standby'da sorgu iptalleri: Standby sorgularını iptal eden mekanizma recovery conflict'tir ve
max_standby_streaming_delayile yönetilir.hot_standby_feedback = oniptalleri azaltır, ancak standby'daki uzun sorgular primary'de vacuum'u geciktirerek bloat'a yol açar. - •Failover sonrası soğuk cache: Standby, WAL replay sırasında yalnızca değişen sayfaları belleğe alır. Okuma trafiği almayan bir sync standby promote edildiğinde HNSW indeksi büyük ölçüde soğuktur.
pg_prewarm.autoprewarm = onrestart'ları kapsar; failover için standby'ları periyodik olarakpg_prewarmile ısıtmak gerekir. - •Eklenti sürümleri: pgvector binary'si ve sürümü tüm node'larda aynı olmalıdır.
ALTER EXTENSION vector UPDATEprimary'de çalıştırıldığında katalog değişikliği replikalara akar; yeni sürümün paketleri standby'larda kurulu değilse failover sonrası sorgular kırılır.
▸5.4. HOT İhlali, Bloat ve Recall
Bir UPDATE ancak hiçbir indekslenmiş sütunu değiştirmiyorsa ve aynı heap sayfasında yeterli boş alan varsa HOT olur. HOT olmayan her güncellemede yeni tuple'ın ctid'si tüm indekslere eklenir; HNSW için bu, ef_construction derinliğinde bir graf araması ve komşu listelerinin yeniden yazılması demektir.
Eski tuple, vacuum temizleyene kadar grafta kalır. HNSW taraması bu dead düğümleri aday olarak bulur ve heap visibility kontrolünde eler; ef_search bütçesinin bir kısmı ölü düğümlere harcanır ve sorgu LIMIT değerinden az sonuç döndürebilir. Bu nedenle:
- •Sık güncellenen alanları 3.2'deki gibi ayrı tabloya taşıyın.
- •Ana tabloda
fillfactor = 90ile HOT için sayfa içi alan bırakın. - •Dead tuple oranı yüksekken
VACUUMyerineREINDEX INDEX CONCURRENTLYile grafı sıfırdan kurmayı değerlendirin.
▸5.5. Kısa Kontrol Listesi
- pgvector paketi tüm Patroni node'larında aynı sürümde kurulu.
- Sık güncellenen alanlar embedding tablosundan ayrılmış.
- İndeks boyutu örneklemden ölçülmüş;
maintenance_work_membuna göre oturum bazında ayarlanmış. pg_walalanı, replikasyon lag'i, replication slot'lar ve uzun transaction'lar build öncesi kontrol edilmiş.- Build
CONCURRENTLY,tmuxiçinde ve izlenebilirapplication_nameile çalıştırılmış. - Build sonrası
indisvaliddoğrulanmış, indeks primary ve standby'larda prewarm edilmiş. ef_searchdeğeri ölçülmüş recall ve latency tablosuna göre seçilmiş; filtreli sorgularda iterative scan açık.- Partial indeks kullanan sorgularda generic plan riski
plan_cache_modeile kapatılmış.
-- HNSW İndeks Durumu ve Boyutu
SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size, idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes WHERE relname = 'documents_rag';
-- Buffer Cache Yerleşimi
SELECT c.relname, count(*) AS buffers, pg_size_pretty(count(*) * 8192) AS cached, round(100.0 * count(*) * 8192 / nullif(pg_relation_size(c.oid), 0), 2) AS pct_cached FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid) AND b.reldatabase = (SELECT oid FROM pg_database WHERE datname = current_database()) WHERE c.relname IN ('idx_documents_rag_embedding_hnsw', 'documents_rag') GROUP BY c.relname, c.oid;- •Ayrı vektör veritabanı (Pinecone/Milvus) maliyeti ve dual-write karmaşasını ortadan kaldırır
- •Operasyonel veriler ve vektör embeddingleri aynı transaction ve ACID sınırlarında kalır
- •pgvector 0.8.x iterative scan ve halfvec ile bellek ve CPU verimliliği sağlar
- •HNSW graf inşası yüksek maintenance_work_mem ve WAL bant genişliği tüketir
- •Senkron replikasyonda yoğun WAL akışı commit gecikmelerini (latency) artırabilir
- •HOT update ihlallerinde HNSW grafı şişebilir ve ef_search aday kalitesi düşebilir
- pgvector paketi tüm Patroni node'larında aynı sürümde kurulu olmalıdır.
- Sık güncellenen sayaç alanları (hit_count, last_accessed_at) embedding tablosundan ayrılmalıdır.
- İndeks boyutu örneklemden ölçülmeli ve maintenance_work_mem buna göre oturum bazında ayarlanmalıdır.
- pg_wal alanı, replikasyon lag'i, replication slot'lar ve uzun transaction'lar build öncesi kontrol edilmelidir.
- Build CONCURRENTLY, tmux içinde ve izlenebilir application_name ile çalıştırılmalıdır.
- Build sonrası indisvalid doğrulanmalı, indeks primary ve standby'larda prewarm edilmelidir.
- ef_search değeri ölçülmüş recall ve latency tablosuna göre seçilmeli; filtreli sorgularda iterative scan açık olmalıdır.
- Partial indeks kullanan sorgularda generic plan riski plan_cache_mode ile kapatılmalıdır.