PostgreSQL Performans İpuçları: Bahis Veri Tabanı Optimizasyonu
Bahis platformlarında yoğun yazma ve okuma yükünü kaldıracak PostgreSQL optimizasyonları: sorgu analizi, indeks stratejileri, tarih bazlı bölümleme, PgBouncer, okuma replikası ve autovacuum.
LLCBullet Ekibi4 dk okuma

PostgreSQL; ACID garantileri, JSONB desteği, zengin indeks türleri ve olgun replikasyon özellikleriyle bahis platformlarında sık tercih edilen bir veritabanıdır. Ancak varsayılan ayarlarla kurulan bir sunucu, büyük bir maç akşamında kupon yazma, bakiye güncelleme ve sonuçlandırma işlemlerinin aynı anda geldiği yükte hızla zorlanır. Bu rehberde ölçmekten başlayıp indeks, bölümleme, bağlantı havuzu, replika ve bakım ayarlarına kadar pratik adımları ele alıyoruz.
Önce ölç: EXPLAIN ANALYZE
Optimizasyona tahminle değil, ölçümle başla. pg_stat_statements eklentisi toplam süreye göre en pahalı sorguları gösterir; tek tek sorguları incelemek için de EXPLAIN (ANALYZE, BUFFERS) kullanılır.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, stake, odds, status, created_at
FROM bets
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT 20;- Büyük bir tabloda Seq Scan görüyorsan eksik ya da kullanılmayan bir indeks olabilir.
- Tahmini satır sayısı ile gerçek satır sayısı arasında büyük fark varsa istatistikler eskidir; ANALYZE çalıştır.
- "shared read" değeri yüksekse veri bellekten değil diskten okunuyor demektir.
- Aynı sorgunun planı yük altında değişiyorsa parametre değerlerine göre farklı planlar seçiliyor olabilir; en kötü durumu da ölç.
İndeks stratejileri
Bileşik indeks
Oyuncunun bahis geçmişi gibi "belirli kullanıcının son kayıtları" sorguları için tek sütunlu iki indeks yerine, sütun sırası sorguya uyan bir bileşik indeks kur:
CREATE INDEX CONCURRENTLY idx_bets_user_created
ON bets (user_id, created_at DESC);Kısmi indeks
Sorgularının çoğu yalnızca açık bahislerle ilgiliyse tüm tabloyu değil, yalnızca o satırları indeksle. İndeks küçülür ve bellekte kalma ihtimali artar:
CREATE INDEX CONCURRENTLY idx_bets_open_event
ON bets (event_id)
WHERE status = 'open';BRIN ve GIN
- BRIN: Zaman sırasıyla eklenen büyük tablolarda (işlem geçmişi, loglar) tarih aralığı sorguları için çok küçük bir indeks sağlar. Veri fiziksel olarak zaman sırasında değilse etkisi azalır.
- GIN: JSONB sütunlarında içerik sorguluyorsan gerekir; aksi halde tam tablo taraması yapılır.
Üretimde indeksi CONCURRENTLY ile oluştur; aksi halde oluşturma süresince tabloya yazma engellenir. Kullanılmayan indeksleri de pg_stat_user_indexes görünümüyle bul ve kaldır; her indeks yazma işlemlerine ek maliyet getirir.
Tarih bazlı bölümleme
Bahis ve işlem geçmişi tabloları zamanla çok büyür ama sorguların çoğu yakın döneme bakar. Bildirimsel bölümleme (declarative partitioning) ile tabloyu aylık parçalara ayırmak, sorguların yalnızca ilgili parçaları taramasını sağlar ve eski verinin arşivlenmesini kolaylaştırır: eski bir ayı DETACH ile ayırıp ayrı depolamaya taşıyabilirsin.
CREATE TABLE bet_history (
id bigint GENERATED ALWAYS AS IDENTITY,
user_id bigint NOT NULL,
stake numeric(18,2) NOT NULL,
created_at timestamptz NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE bet_history_2026_09 PARTITION OF bet_history
FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');Yeni ayların bölümlerini önceden oluşturan küçük bir zamanlanmış iş kurmayı unutma; aksi halde ay başında gelen kayıtlar yazılacak bölüm bulamaz.
PgBouncer ile bağlantı havuzu
PostgreSQL her bağlantı için ayrı bir süreç çalıştırır; yüzlerce eşzamanlı bağlantı bellek ve bağlam değiştirme maliyetini hızla artırır. PgBouncer, uygulama ile veritabanı arasında bağlantıları havuzlayan hafif bir ara katmandır. Transaction modu, çok sayıda kısa işlem yapan bahis uygulamaları için genellikle en verimli seçenektir. Aşağıdaki değerler yalnızca örnektir; kendi yükünle test ederek belirlemelisin:
[databases]
betting = host=10.0.0.10 port=5432 dbname=betting
[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 40
server_idle_timeout = 60- Transaction modunda oturum düzeyindeki özellikler (SET ile değiştirilen ayarlar, LISTEN/NOTIFY, oturum kapsamlı advisory lock) beklendiği gibi çalışmaz; uygulamanı buna göre kontrol et.
- Hazırlanmış ifadeler (prepared statements) için PgBouncer sürümünün ve sürücünün desteğini kontrol et; yeni PgBouncer sürümleri bunu max_prepared_statements ayarıyla destekler.
- Havuz boyutunu veritabanı sunucusunun çekirdek sayısı ve disk kapasitesiyle birlikte düşün; daha büyük havuz her zaman daha hızlı değildir.
Sorgu tarafında dikkat edilecekler
- SELECT * yerine yalnızca gereken sütunları seç; geniş satırlar belleği ve ağı gereksiz yere doldurur.
- Derin sayfalamada OFFSET yerine son görülen kayda göre ilerleyen (keyset) sayfalama kullan.
- N+1 sorgu desenlerini tek bir birleştirme ya da toplu sorguyla değiştir.
- Her bağlantıya statement_timeout ile üst süre koy; kontrolden çıkan tek bir sorgu bütün havuzu meşgul edebilir.
- Bakiye düşümünü tek bir koşullu UPDATE ile yap: bakiyeyi okuyup uygulamada hesaplamak ve geri yazmak yarış durumuna açıktır.
UPDATE wallets
SET balance = balance - $2
WHERE user_id = $1 AND balance >= $2
RETURNING balance;Bu sorgu hiç satır döndürmezse bakiye yetersizdir ve işlem reddedilir; kontrol ile düşüm aynı ifadede yapıldığı için iki eşzamanlı istek aynı bakiyeyi iki kez harcayamaz.
Okuma replikası
Bahis geçmişi raporları, mutabakat sorguları ve arka ofis ekranları gibi ağır okumaları birincil sunucudan uzak tut. Streaming replication ile kurulan bir okuma replikası bu yükü üstlenir. Replikanın birincil sunucudan bir miktar geride kalabileceğini unutma: bakiye ve açık kupon gibi yazıldıktan hemen sonra okunması gereken veriler birincil sunucudan okunmalı.
Autovacuum ayarları
PostgreSQL, güncellenen ve silinen satırların eski sürümlerini hemen silmez; bu ölü satırları autovacuum temizler. Bahis tablolarında güncelleme hacmi yüksek olduğunda varsayılan eşikler geç kalır ve tablo şişer. Yoğun tablolar için eşikleri tablo bazında düşür:
ALTER TABLE bets SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.01
);pg_stat_user_tables görünümündeki n_dead_tup ve last_autovacuum sütunlarını düzenli izle. Uzun süre açık kalan işlemler de autovacuum'un ölü satırları temizlemesini engeller; idle_in_transaction_session_timeout ayarıyla bu tür işlemleri sınırla.
Kontrol listesi
- pg_stat_statements'ı etkinleştir ve en pahalı sorguları haftalık gözden geçir.
- Sık çalışan sorguların planlarını EXPLAIN (ANALYZE, BUFFERS) ile kontrol et.
- Büyüyen geçmiş tablolarını tarihe göre bölümle.
- Uygulama ile veritabanı arasına PgBouncer koy.
- Raporlama yükünü okuma replikasına taşı.
- Yoğun tablolarda autovacuum eşiklerini düşür.
- Büyük etkinliklerden önce yük testi yap.
Veritabanının önüne önbellek katmanı eklemek için Redis ile önbellekleme yazımıza, yedekleme ve geri dönüş planı için de veri yedekleme rehberimize göz atabilirsin.
- postgresql
- veritabanı performansı
- indeks stratejisi
- pgbouncer
- autovacuum
- bölümleme
Bu yazıyı paylaş