Veritabanı Performansı Neden Kritiktir?
Veritabanı performansı, modern uygulamaların kullanıcı deneyimini ve iş sürekliligini doğrudan etkileyen en kritik faktörlerden biridir. Yavaş sorgular, kullanıcıların sayfaları terk etmesine, iş süreçlerinin aksamasına ve gelir kaybına yol açar. Araştırmalar, sayfa yüklenme süresindeki her bir saniyelik artışın dönüşüm oranlarını yüzde yedi oranında düşürdüğünü göstermektedir.
Veritabanı performans sorunları genellikle uygulamanın ölçeklenmesiyle birlikte ortaya çıkar. Geliştirme ortamında sorunsuz çalışan bir sorgu, üretim ortamında milyonlarca kayıt üzerinde çalıştığında dakikalar sürebilir. Bu nedenle performans optimizasyonu, uygulama geliştirme sürecinin ayrılmaz bir parçası olmalıdır.
Bu rehberde, veritabanı performansını artırmak için kullanabileceğiniz kanıtlanmış stratejileri ve teknikleri detaylı olarak ele alacağız. İndeksleme'den sorgu optimizasyonuna, önbelleklemeden partitioning'e kadar geniş bir yelpazede pratik bilgiler sunacağız.
İndeksleme Stratejileri
İndeksler, veritabanı performans optimizasyonunun temel taşıdır. Doğru uygulandığında sorgu süresini dramatik şekilde azaltırken, yanlış veya aşırı kullanımda yazma performansını olumsuz etkileyebilir.
İndeks Türleri
Farklı veritabanı motorları çeşitli indeks türlerini destekler. Her birinin kendine özgü kullanım senaryoları vardır:
- B-Tree İndeks: En yaygın indeks türüdür. Eşitlik ve aralık sorgularında etkilidir. Varsayılan indeks türü olarak çoğu veritabanı motorunda kullanılır
- Hash İndeks: Yalnızca eşitlik sorgularında kullanılır, aralık sorgularını desteklemez. O(1) arama performansı sunar
- GIN (Generalized Inverted Index): Full-text search, JSONB ve dizi sorgularında etkilidir. PostgreSQL'de yaygın olarak kullanılır
- GiST (Generalized Search Tree): Geometrik veriler, tam metin arama ve özel veri tipleri için kullanılır
- Bitmap İndeks: Düşük kardinaliteli sütunlar (cinsiyet, durum gibi az sayıda farklı değer içeren) için optimize edilmiştir
- Covering İndeks (Include): Sorgunun ihtiyaç duyduğu tüm sütunları kapsayarak tabloya erişim gereksinimini ortadan kaldırır
İndeksleme En İyi Uygulamaları
İndeksleme stratejisi oluştururken aşağıdaki prensipleri göz önünde bulundurmak gerekir:
- WHERE koşullarını analiz edin: En sık kullanılan filtre sütunlarına indeks ekleyin
- Bileşik indekslerde sütun sırasını optimize edin: Seçiciliği yüksek sütunları öne alın ve sorgulardaki sütun sırasıyla eşleştirin
- Gereksiz indekslerden kaçının: Kullanılmayan indeksler yazma performansını düşürür ve depolama alanı tüketir
- Covering index kullanın: Sık çalışan sorgular için INCLUDE ile ek sütunlar ekleyerek index-only scan sağlayın
- Kısmi (partial) indeksler değerlendirin: Yalnızca belirli koşulları sağlayan satırları indeksleyerek boyutu küçültün
İndeks Kullanım Analizi
-- PostgreSQL: Kullanılmayan indeksleri tespit etme
SELECT
schemaname || '.' || relname AS tablo,
indexrelname AS indeks,
idx_scan AS kullanim_sayisi,
pg_size_pretty(pg_relation_size(indexrelid)) AS boyut
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
-- SQL Server: Eksik indeks önerileri
SELECT
migs.avg_total_user_cost * migs.avg_user_impact *
(migs.user_seeks + migs.user_scans) AS iyilestirme_degeri,
mid.statement AS tablo,
mid.equality_columns,
mid.inequality_columns,
mid.included_columns
FROM sys.dm_db_missing_index_groups mig
JOIN sys.dm_db_missing_index_group_stats migs
ON mig.index_group_handle = migs.group_handle
JOIN sys.dm_db_missing_index_details mid
ON mig.index_handle = mid.index_handle
ORDER BY iyilestirme_degeri DESC;
Sorgu Optimizasyonu Teknikleri
Sorgu optimizasyonu, veritabanı performansını artırmanın en etkili yollarından biridir. Doğru yazılmış bir sorgu, aynı sonucu veren kötü yazılmış bir sorgudan yüzlerce kat daha hızlı çalışabilir.
Temel Sorgu Optimizasyon İlkeleri
- SELECT * kullanmaktan kaçının: Yalnızca ihtiyacınız olan sütunları seçin. Bu hem ağ trafiğini hem de bellek kullanımını azaltır
- WHERE koşullarında fonksiyon kullanmayın:
WHERE YEAR(tarih) = 2024yerineWHERE tarih >= '2024-01-01' AND tarih < '2025-01-01'kullanın. Fonksiyonlar indeks kullanımını engeller - Subquery yerine JOIN tercih edin: Çoğu durumda JOIN, alt sorgulardan daha verimlidir. Özellikle correlated subquery'lerden kaçının
- EXISTS ile IN arasındaki farkı bilin: Büyük veri kümelerinde EXISTS genellikle IN'den daha performanslıdır çünkü ilk eşleşmeyi bulduğunda durur
- UNION ALL tercih edin: Duplike kontrolüne ihtiyacınız yoksa UNION yerine UNION ALL kullanın. UNION, sonuçları sıralamak ve duplikeleri elemek için ek işlem yapar
Pagination Optimizasyonu
Büyük veri kümelerinde sayfalama (pagination) yaygın bir gereksinimdir ancak yanlış uygulandığında ciddi performans sorunlarına yol açar:
-- Kötü: OFFSET büyüdükçe performans düşer
SELECT * FROM siparisler
ORDER BY tarih DESC
OFFSET 100000 LIMIT 20;
-- İyi: Keyset pagination kullanın
SELECT * FROM siparisler
WHERE tarih < '2024-06-15 10:30:00'
ORDER BY tarih DESC
LIMIT 20;
-- Alternatif: Deferred join tekniği
SELECT s.* FROM siparisler s
INNER JOIN (
SELECT id FROM siparisler
ORDER BY tarih DESC
OFFSET 100000 LIMIT 20
) AS sub ON s.id = sub.id
ORDER BY s.tarih DESC;
JOIN Optimizasyonu
JOIN işlemleri veritabanı sorgularının en yoğun kaynak tüketen bölümlerinden biridir. Veritabanı motorları üç temel JOIN algoritması kullanır:
- Nested Loop Join: Küçük tablolar için idealdir. Dış tablonun her satırı için iç tabloda arama yapar
- Hash Join: Büyük tablolar arasındaki eşitlik birleştirmelerinde etkilidir. Küçük tablodan hash tablosu oluşturur
- Merge Join: Her iki tablo da sıralı olduğunda en verimli yöntemdir. Paralel tarama yapar
JOIN performansını artırmak için birleştirme sütunlarında indeks bulunduğundan emin olun, küçük tabloyu sürücü tablo olarak kullanın ve gereksiz JOIN'lerden kaçının.
Execution Plan (Yürütme Planı) Analizi
Execution plan, veritabanı motorunun bir sorguyu nasıl yürüteceğini gösteren detaylı bir yol haritasıdır. Sorgu optimizasyonunun en güçlü aracı olan execution plan'ları okumak ve yorumlamak, her veritabanı geliştiricisinin sahip olması gereken temel bir beceridir.
Execution Plan Okuma
-- PostgreSQL
EXPLAIN ANALYZE
SELECT u.ad, COUNT(s.id) as siparis_sayisi
FROM kullanicilar u
JOIN siparisler s ON u.id = s.kullanici_id
WHERE s.tarih >= '2024-01-01'
GROUP BY u.ad
ORDER BY siparis_sayisi DESC;
-- SQL Server
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
-- Sorgunuzu çalıştırın
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
Dikkat Edilmesi Gereken Operatörler
- Seq Scan / Table Scan: Tüm tablonun taranması. Büyük tablolarda performans sorununa işaret eder
- Index Scan: İndeks kullanılarak satırların bulunması. İdeal durumdur
- Index Only Scan: Yalnızca indeksten veri okunması, tabloya erişim yok. En iyi performansı sunar
- Sort: Bellekte sıralama işlemi. Büyük veri kümelerinde disk tabanlı sıralamaya düşebilir
- Hash Join / Nested Loop / Merge Join: Birleştirme stratejisi. Veri boyutuna göre uygun olanı seçilmelidir
Normalizasyon ve Denormalizasyon
Veritabanı tasarımında normalizasyon ve denormalizasyon kararları, performansı doğrudan etkiler. Bu iki yaklaşım arasındaki denge, uygulamanın gereksinimlerine göre belirlenmelidir.
Normalizasyon
Normalizasyon, veri tekrarını azaltarak tutarlılığı artırır. İlk Normal Form'dan (1NF) Beşinci Normal Form'a (5NF) kadar çeşitli seviyeleri vardır. Pratikte çoğu uygulama Üçüncü Normal Form (3NF) seviyesinde tasarlanır. Normalizasyonun avantajları arasında veri bütünlüğü, daha küçük tablo boyutları ve daha kolay bakım yer alır. Ancak karmaşık sorgularda çok sayıda JOIN gerektirebilir.
Denormalizasyon
Denormalizasyon, okuma performansını artırmak için bilinçli olarak veri tekrarı yapmaktır. Raporlama tabloları, materialized view'lar ve özet tablolar denormalizasyonun yaygın uygulamalarıdır. Yazma performansı ve veri tutarlılığı pahasına okuma hızını dramatik şekilde artırabilir.
Altın kural: OLTP sistemleri için normalizasyon, OLAP ve raporlama sistemleri için denormalizasyon tercih edin. Hibrit yaklaşımda, normalize edilmiş operasyonel veritabanını kaynak olarak kullanırken, analitik sorgular için denormalize edilmiş materialized view'lar oluşturun.
Connection Pooling
Veritabanı bağlantılarının oluşturulması ve kapatılması kaynak yoğun bir işlemdir. Her yeni bağlantı TCP handshake, kimlik doğrulama ve bellek tahsisi gerektirir. Connection pooling, bir bağlantı havuzu oluşturarak bu maliyeti minimize eder.
Connection Pooling Nasıl Çalışır?
Uygulama başlatıldığında belirli sayıda veritabanı bağlantısı oluşturulur ve havuzda tutulur. Bir istek geldiğinde havuzdan mevcut bir bağlantı atanır, işlem tamamlandığında bağlantı havuza geri döner. Bu sayede bağlantı oluşturma maliyeti yalnızca bir kez ödenir.
Popüler Connection Pooling Çözümleri
- PgBouncer: PostgreSQL için hafif ve yüksek performanslı bağlantı havuzlayıcı. Transaction, session ve statement modlarını destekler
- HikariCP: Java ekosistemi için en hızlı bağlantı havuzu kütüphanesi. Spring Boot'un varsayılan havuzlayıcısıdır
- ProxySQL: MySQL için gelişmiş proxy ve bağlantı havuzlayıcı. Sorgu yönlendirme ve caching özellikleri de sunar
Connection Pool Yapılandırma İpuçları
// HikariCP yapılandırma örneği
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(20); // Maksimum bağlantı sayısı
config.setMinimumIdle(5); // Minimum boşta bağlantı
config.setConnectionTimeout(30000); // Bağlantı zaman aşımı (ms)
config.setIdleTimeout(600000); // Boşta kalma zaman aşımı
config.setMaxLifetime(1800000); // Maksimum bağlantı ömrü
config.setLeakDetectionThreshold(60000); // Sızıntı tespit eşiği
Önbellekleme Stratejileri
Önbellekleme, sık erişilen verileri hızlı erişimli bir katmanda saklayarak veritabanı yükünü azaltan güçlü bir tekniktir. Doğru uygulandığında yanıt sürelerini dramatik şekilde iyileştirir.
Redis ile Önbellekleme
Redis, bellek içi veri yapısı deposu olarak önbellekleme senaryolarında en yaygın kullanılan çözümdür. String, hash, list, set ve sorted set gibi çeşitli veri yapılarını destekler. Kalıcılık seçenekleri (RDB ve AOF) sayesinde veriler disk üzerinde de saklanabilir.
// C# ile Redis önbellekleme örneği
public async Task<Product> GetProductAsync(int productId)
{
string cacheKey = $"product:{productId}";
// Önce cache'e bak
var cached = await _redis.StringGetAsync(cacheKey);
if (cached.HasValue)
return JsonSerializer.Deserialize<Product>(cached);
// Cache'de yoksa veritabanından al
var product = await _dbContext.Products.FindAsync(productId);
// Cache'e kaydet (30 dakika TTL)
await _redis.StringSetAsync(
cacheKey,
JsonSerializer.Serialize(product),
TimeSpan.FromMinutes(30)
);
return product;
}
Önbellekleme Desenleri
- Cache-Aside (Lazy Loading): Veri önce cache'den okunur, yoksa veritabanından alınıp cache'e yazılır. En yaygın desendir
- Write-Through: Veri hem cache'e hem veritabanına eş zamanlı yazılır. Tutarlılık garantisi sağlar
- Write-Behind (Write-Back): Veri önce cache'e yazılır, belirli aralıklarla veritabanına aktarılır. Yazma performansı yüksektir ancak veri kaybı riski taşır
- Read-Through: Cache, veritabanı erişimini kendisi yönetir. Uygulama yalnızca cache ile iletişim kurar
Cache Invalidation Stratejileri
Cache invalidation, bilgisayar biliminin en zor sorunlarından biri olarak kabul edilir. Yaygın yaklaşımlar arasında TTL (Time-To-Live) tabanlı otomatik süre sonu, olay tabanlı (event-driven) invalidasyon ve sürüm tabanlı (versioned) cache anahtarları yer alır. Kritik verilerde TTL'i kısa tutmak, düşük önem dereceli verilerde daha uzun TTL kullanmak dengeli bir yaklaşımdır.
Partitioning ve Sharding
Veritabanı büyüdükçe tek bir sunucu veya tablonun sınırlarına ulaşılabilir. Partitioning ve sharding, bu ölçekleme sorunlarını çözmek için kullanılan iki farklı stratejidir.
Tablo Partitioning
Partitioning, büyük bir tabloyu fiziksel olarak daha küçük parçalara bölerken mantıksal olarak tek bir tablo gibi davranmasını sağlar. Sorgu motoru, WHERE koşuluna göre yalnızca ilgili bölümleri tarar (partition pruning).
-- PostgreSQL ile tarih bazlı partitioning
CREATE TABLE siparisler (
id SERIAL,
musteri_id INT,
tutar DECIMAL(10,2),
tarih TIMESTAMP
) PARTITION BY RANGE (tarih);
CREATE TABLE siparisler_2024_q1 PARTITION OF siparisler
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE siparisler_2024_q2 PARTITION OF siparisler
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
CREATE TABLE siparisler_2024_q3 PARTITION OF siparisler
FOR VALUES FROM ('2024-07-01') TO ('2024-10-01');
CREATE TABLE siparisler_2024_q4 PARTITION OF siparisler
FOR VALUES FROM ('2024-10-01') TO ('2025-01-01');
Partitioning Türleri
- Range Partitioning: Tarih, sayı gibi sürekli değer aralıklarına göre bölümleme
- List Partitioning: Belirli değer listelerine göre bölümleme (ülke, bölge gibi)
- Hash Partitioning: Hash fonksiyonu ile eşit dağılım sağlayan bölümleme
Database Sharding
Sharding, verilerin birden fazla veritabanı sunucusuna yatay olarak dağıtılmasıdır. Partitioning'den farkı, verilerin farklı fiziksel sunucularda tutulmasıdır. Yatay ölçeklenme sağlar ancak uygulama karmaşıklığını önemli ölçüde artırır. Shard key seçimi kritik önemdedir; kötü bir shard key veri dengesizliğine ve hot spot'lara yol açabilir.
İzleme Araçları ve Yavaş Sorgu Analizi
Proaktif performans izleme, sorunları kullanıcılar fark etmeden tespit etmenin ve çözmenin anahtarıdır.
Yavaş Sorgu Günlükleri
-- PostgreSQL: Yavaş sorgu logunu etkinleştirme
ALTER SYSTEM SET log_min_duration_statement = 1000; -- 1 saniye
ALTER SYSTEM SET log_statement = 'none';
SELECT pg_reload_conf();
-- MySQL: Yavaş sorgu logu
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
İzleme Araçları
- pg_stat_statements: PostgreSQL için sorgu istatistiklerini toplayan güçlü bir uzantı
- Percona Monitoring and Management (PMM): MySQL ve PostgreSQL için kapsamlı izleme ve diagnostik platformu
- SolarWinds Database Performance Analyzer: Çoklu veritabanı desteği sunan ticari izleme aracı
- Datadog Database Monitoring: Bulut tabanlı uçtan uca veritabanı izleme
- Grafana + Prometheus: Açık kaynak izleme yığını, özel dashboard'lar ile veritabanı metriklerini görselleştirme
İzlenecek Temel Metrikler
| Metrik | Açıklama | Hedef Değer |
|---|---|---|
| Sorgu yanıt süresi (p95/p99) | Sorguların yüzde 95/99'unun tamamlanma süresi | < 100ms |
| Aktif bağlantı sayısı | Eş zamanlı veritabanı bağlantıları | Havuz boyutunun %70'i altında |
| Cache hit oranı | Buffer cache'den okunan sayfa oranı | > %99 |
| Disk I/O bekleme | I/O işlemleri için harcanan süre | Toplam sürenin %10'u altında |
| Kilit bekleme süresi | Lock wait süreleri | Minimum düzeyde |
| Tablo bloat oranı | Ölü satırların oranı (PostgreSQL) | < %20 |
ORM Optimizasyonu
Object-Relational Mapping (ORM) araçları geliştirme hızını artırırken, yanlış kullanıldığında ciddi performans sorunlarına neden olabilir. N+1 sorgu problemi, aşırı veri çekme ve gereksiz tracking bunların başında gelir.
N+1 Sorgu Problemi
N+1 problemi, bir ana sorgu çalıştırıldıktan sonra her sonuç satırı için ayrı bir sorgu daha çalıştırılmasıdır. Örneğin 100 sipariş listelendikten sonra her siparişin müşteri bilgisini ayrı ayrı sorgulamak 101 sorgu üretir.
// Entity Framework Core - N+1 problemi
// KÖTÜ: Her sipariş için ayrı müşteri sorgusu çalışır
var siparisler = await _context.Siparisler.ToListAsync();
foreach (var siparis in siparisler)
{
Console.WriteLine(siparis.Musteri.Ad); // Lazy loading ile N+1
}
// İYİ: Eager loading ile tek sorguda çözüm
var siparisler = await _context.Siparisler
.Include(s => s.Musteri)
.ToListAsync();
// DAHA İYİ: Projection ile yalnızca gerekli alanları çekin
var sonuc = await _context.Siparisler
.Select(s => new { s.Id, s.Tutar, MusteriAd = s.Musteri.Ad })
.ToListAsync();
AsNoTracking ve Projection
Salt okunur sorgularda AsNoTracking() kullanarak change tracking maliyetini ortadan kaldırın. Projection (Select ile DTO'ya dönüştürme) ile yalnızca ihtiyaç duyulan sütunları çekin. Bu iki teknik birlikte kullanıldığında bellek kullanımını ve sorgu süresini önemli ölçüde azaltır.
Veritabanı Bakımı
Düzenli veritabanı bakımı, uzun vadeli performansın sürdürülmesi için zorunludur. İhmal edilen bakım işlemleri zamanla performans degradasyonuna yol açar.
Temel Bakım Görevleri
- VACUUM (PostgreSQL): Ölü satırları temizler ve disk alanını geri kazanır. VACUUM ANALYZE aynı zamanda istatistikleri günceller
- İstatistik güncelleme: Sorgu optimizasyonunun doğru çalışması için tablo istatistiklerinin güncel olması gerekir
- İndeks yeniden oluşturma: Fragmentasyon biriken indeksler REINDEX veya ALTER INDEX REBUILD ile yeniden oluşturulmalıdır
- Log temizleme: Transaction logları ve sorgu logları düzenli olarak arşivlenmeli veya temizlenmelidir
- Yedekleme doğrulama: Yedeklemelerin düzenli olarak geri yüklenerek test edilmesi hayati önemdedir
Otomatik Bakım Planı
-- PostgreSQL otomatik vacuum yapılandırması
ALTER TABLE buyuk_tablo SET (
autovacuum_vacuum_threshold = 1000,
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_analyze_threshold = 500,
autovacuum_analyze_scale_factor = 0.02
);
Sonuç
Veritabanı performans optimizasyonu, tek seferlik bir görev değil sürekli bir süreçtir. İndeksleme stratejileri, sorgu optimizasyonu, önbellekleme, partitioning ve düzenli bakım bir bütün olarak ele alınmalıdır. En önemli ilke, ölçmeden optimize etmemektir; her değişikliğin öncesi ve sonrası mutlaka benchmark edilmelidir.
Performans sorunlarına proaktif yaklaşmak, reaktif yaklaşımdan her zaman daha az maliyetlidir. İzleme araçlarını kurun, uyarı eşiklerini belirleyin ve düzenli performans denetimleri yapın. Unutmayın ki en hızlı sorgu, hiç çalıştırılmayan sorgudur; önbellekleme ve uygulama düzeyinde optimizasyonlarla veritabanı yükünü en baştan azaltmak her zaman öncelikli strateji olmalıdır.