Tallinn'deki ofisimizden selamlar. Ben Ahmet Bulut, POI369'un kurucu ortağı ve teknoloji mimarıyım. Yıllardır dijital ürünler geliştirirken, parlak bir fikrin veya harika bir arayüzün, yavaş bir veritabanı yüzünden nasıl duvara tosladığını defalarca gördüm. Özellikle bizim gibi Supabase, Vercel gibi modern ve ölçeklenebilir teknolojileri kullanan ekipler için, temelde yatan PostgreSQL veritabanının sağlığı her şey demek.
Bu yazıda, size akademik teorilerden çok, production'da karşılaştığımız, kan ter ve gözyaşıyla öğrendiğimiz PostgreSQL veritabanı optimizasyonu stratejilerini anlatacağım. Amacım, size sadece 'ne' yapmanız gerektiğini değil, 'neden' ve 'nasıl' yapmanız gerektiğini de pratik örneklerle göstermek. Hazırsanız, kahvenizi alın ve başlayalım.
Temel Taşlar: Doğru Indexing Stratejileri
Veritabanı indexing'ini, devasa bir kütüphanedeki kart kataloğuna benzetebiliriz. Kataloğunuz yoksa, bir kitabı bulmak için tüm rafları tek tek gezmeniz gerekir (buna Full Table Scan diyoruz). Katalog (yani index) ise size kitabın tam olarak hangi rafta olduğunu söyler. İşte bu kadar basit ama bir o kadar da güçlü bir konsept.
POI369'da geliştirdiğimiz projelerde, doğru index'in bir sorguyu 5 saniyeden 50 milisaniyeye düşürdüğüne şahit olduk. Bu, kullanıcı deneyimi ile terk edilme arasındaki fark demek.
En Yakın Dostunuz: B-Tree Index
PostgreSQL'in varsayılan ve en sık kullanılan index türüdür. Eşitlik (=), büyüktür/küçüktür (>, <), aralık (BETWEEN), ve liste (IN) sorguları için mükemmeldir. PRIMARY KEY ve UNIQUE kısıtlamaları otomatik olarak arkada bir B-Tree index oluşturur.
Pratik Örnek: Bir users tablonuz var ve kullanıcıları sık sık e-posta adreslerine göre arıyorsunuz.
-- Bu sorgu, index olmadan yavaş çalışacaktır. SELECT * FROM users WHERE email = '[email protected]';
-- Çözüm: email kolonu için bir index oluşturmak. CREATE INDEX idx_users_email ON users(email);
Takeaway: WHERE, JOIN (ON koşulunda) ve ORDER BY ifadelerinde sıkça kullandığınız kolonlar, B-Tree index için ilk adaylarınızdır.
Gelişmiş Senaryolar: GIN ve Partial Index
Her durum B-Tree için uygun değildir. PostgreSQL'in gücü, farklı senaryolar için sunduğu zengin index türlerinden gelir.
- GIN (Generalized Inverted Index): Dizi (
array), JSONB veya tam metin arama (full-text search) gibi kompozit veri tipleri üzerinde arama yaparken kullanılır. Örneğin, bir ürüne ait etiketler (tags) bir array kolonunda tutuluyorsa, GIN index hayat kurtarır. - Partial Index (Kısmi Index): Bu benim favorilerimden. Sadece belirli bir koşulu sağlayan satırlar için index oluşturmanızı sağlar. Bu, index boyutunu ciddi şekilde küçültür ve performansı artırır.
Pratik Örnek (Partial Index): Siparişler tablonuzda milyonlarca satır var ama siz genellikle sadece 'aktif' veya 'gönderilmeyi bekleyen' siparişleri sorguluyorsunuz.
-- Sadece tamamlanmamış siparişler için bir index oluşturalım.
CREATE INDEX idx_orders_pending ON orders(order_date)
WHERE status = 'pending' OR status = 'processing';Bu sayede, status = 'completed' olan milyonlarca satır index'e dahil edilmez. Akıllıca, değil mi?
Sır Perdesini Aralamak: EXPLAIN ANALYZE ile Sorgu Optimizasyonu
Tahminlere dayalı optimizasyon, en tehlikeli alışkanlıklardan biridir. PostgreSQL'in bize sunduğu EXPLAIN ANALYZE komutu, sorgu planlayıcısının (query planner) arka planda neler çevirdiğini bize tüm çıplaklığıyla gösteren sihirli bir araçtır.
Bir sorgunun başına EXPLAIN ANALYZE yazdığınızda, PostgreSQL o sorguyu çalıştırır ve size adım adım nasıl yürüttüğünü, hangi index'leri kullandığını (veya kullanmadığını), ne kadar sürdüğünü ve tahmini maliyetini (cost) raporlar.
Nasıl Okunur?
Seq Scan(Sequential Scan): Kırmızı alarm! Bu, PostgreSQL'in index kullanmadan tablonun tamamını taradığını gösterir. Küçük tablolarda sorun değil, ama büyük tablolarda felakettir.Index Scan/Bitmap Index Scan: Yeşil ışık! Bu, sorgunuzun bir index kullandığını gösterir.cost: Planlayıcının sorgu maliyeti tahminidir. İlk sayı başlangıç maliyeti, ikinci sayı toplam maliyettir. Genellikle daha düşük olan daha iyidir.actual time: Sorgunun gerçek çalışma süresidir (milisaniye cinsinden). En önemli metrik budur.
Pratik Örnek:
EXPLAIN ANALYZE SELECT * FROM products WHERE category_id = 123 AND in_stock = true;Bu komutun çıktısı, (category_id, in_stock) şeklinde bir birleşik index'e (composite index) ihtiyacınız olup olmadığını size net bir şekilde söyleyecektir.
POI369'dan Bir Not: Ekibimizde yeni bir özellik geliştirildiğinde, karmaşık sorgular için
EXPLAIN ANALYZEçıktısını Pull Request'e eklemek bir standarttır. Bu, kodun kendisi kadar veritabanı etkisini de gözden geçirmemizi sağlar.
Sorgu Optimizasyonu Checklist'i (Copy-Paste Ready)
- [ ] Yavaş olduğunu düşündüğün sorgunun başına
EXPLAIN ANALYZEekle ve çalıştır. - [ ] Çıktıda
Seq Scanvar mı? Varsa,WHEREveyaJOINkoşullarındaki kolonlar için uygun bir index oluştur. - [ ]
SELECT *kullanıyor musun? Sadece ihtiyacın olan kolonları (SELECT id, name, email) iste. Bu, I/O ve ağ trafiğini azaltır. - [ ]
WHEREkoşulunda bir kolon üzerinde fonksiyon kullanıyor musun? (örn:WHERE LOWER(email) = '...'). Bu, genellikle index kullanımını engeller. Ya uygulama tarafında veriyi küçük harfe çevir ya da bir fonksiyonel index (CREATE INDEX ... ON users (LOWER(email))) oluştur. - [ ] Çok sayıda
JOINmi var?JOINsırasının ve türünün performansı etkileyebileceğini unutma. - [ ]
LIMITveOFFSETile sayfalama (pagination) mı yapıyorsun? Çok büyükOFFSETdeğerleri performansı düşürür. Keyset pagination (cursor-based) yöntemini araştırmayı düşün.
Takeaway: Ölçmeden optimize etme. EXPLAIN ANALYZE, veritabanı optimizasyonunun pusulasıdır.
Büyümenin Kaçınılmaz Gerçeği: Scaling Stratejileri
Uygulamanız popüler hale geldiğinde, tek bir veritabanı sunucusu artık yeterli olmayabilir. Scaling (ölçeklendirme), bu büyüme sancılarını yönetme sanatıdır.
Dikey Scaling (Vertical Scaling): Kolay ama Sınırlı Yol
Bu, sunucunuza daha fazla RAM, CPU veya daha hızlı diskler eklemektir. Başlangıç için en kolay yoldur ancak iki temel sorunu vardır: birincisi, maliyeti katlanarak artar; ikincisi, ulaşabileceğiniz bir tavan vardır. Bir noktadan sonra daha güçlü bir sunucu bulamazsınız.
Yatay Scaling (Horizontal Scaling): Gerçek Dünya Çözümü
Bu, yükü birden fazla sunucuya dağıtmaktır. PostgreSQL dünyasında bunun en yaygın iki yolu vardır:
Read Replicas (Okuma Replikaları): Ana veritabanınızın (Primary) bir veya daha fazla salt okunur kopyasını oluşturursunuz. Yazma işlemleri (
INSERT,UPDATE,DELETE) ana veritabanına giderken, yoğun okuma işlemleri (SELECT) replikalara yönlendirilir. Bloglar, e-ticaret siteleri gibi okuma yoğun uygulamalar için harikadır.Sharding (Parçalama): Bu daha karmaşık bir yöntemdir. Veritabanınızı mantıksal parçalara (shard'lara) böler ve bu parçaları farklı sunuculara dağıtırsınız. Örneğin, kullanıcıları ID'lerine veya coğrafi konumlarına göre farklı sunucularda tutabilirsiniz. Bu, hem okuma hem de yazma yükünü dağıtır ama uygulama mantığını karmaşıklaştırır. Citus gibi PostgreSQL eklentileri bu süreci kolaylaştırabilir.
Gözden Kaçan Kahraman: Connection Pooling
Özellikle Vercel gibi serverless ortamlarda çalışanlar için bu bölüm hayati. PostgreSQL'e her bağlantı kurmak maliyetli bir işlemdir. Yüzlerce anlık serverless fonksiyonun her birinin veritabanına yeni bir bağlantı açmaya çalıştığını düşünün. Veritabanınız kısa sürede bağlantı limitlerine ulaşır ve çöker.
Connection Pooling, bu sorunu çözer. PgBouncer gibi bir araç, veritabanı ile uygulamanız arasında durarak hazırda bir bağlantı havuzu tutar. Uygulamanız bir bağlantı istediğinde, havuzdan hazır bir tanesini verir. İşlem bitince bağlantıyı havuza geri koyar. Bu, sürekli yeni bağlantı açma/kapama maliyetini ortadan kaldırır.
Supabase kullanıyorsanız şanslısınız! Supabase, platformunda dahili olarak bir connection pooler ile gelir. Bu, bizim POI369'da Supabase'i tercih etmemizin önemli nedenlerinden biridir.
Takeaway: Modern bir uygulama geliştiriyorsanız, özellikle serverless bir mimari kullanıyorsanız, connection pooling bir seçenek değil, zorunluluktur.
Kaçınılması Gereken Yaygın Hatalar
- Aşırı Indexleme (Over-indexing): Her kolon için index oluşturmak iyi bir fikir değildir. Unutmayın, her
INSERT,UPDATE,DELETEişleminde index'lerin de güncellenmesi gerekir. Bu, yazma performansını düşürür. Sadece gerçekten ihtiyaç duyulan yerlerde index kullanın. - Yanlış Kolonları Indexleme: Düşük kardinaliteye sahip (yani çok az sayıda farklı değer içeren, örneğin
cinsiyetgibi) kolonları tek başlarına indexlemek genellikle verimsizdir. EXPLAIN'i Görmezden Gelmek: Sorgunun yavaş olduğunu hissedip, körlemesine index eklemek. Bu, samanlıkta iğne aramaktır. Önce teşhis, sonra tedavi.- Veritabanını Sadece Veri Deposu Olarak Görmek: PostgreSQL son derece güçlü bir sistemdir.
Window Functions,Common Table Expressions (CTEs),JSONBfonksiyonları gibi özelliklerini öğrenmek, karmaşık işlemleri uygulama katmanından veritabanı katmanına taşıyarak inanılmaz performans artışları sağlayabilir. - Bakımı İhmal Etmek:
VACUUMveANALYZEkomutlarını düzenli olarak çalıştırmak (Auto-vacuum genellikle bunu yapar ama yine de bilmekte fayda var) tabloları sağlıklı ve istatistikleri güncel tutar. Bu, sorgu planlayıcısının daha iyi kararlar vermesini sağlar.
Sonuç ve POI369 Yaklaşımı
Gördüğünüz gibi, PostgreSQL veritabanı optimizasyonu, sadece birkaç komut ezberlemekten çok daha fazlasıdır. Bu, sisteminizin kalbini anlamak, onunla empati kurmak ve büyümesini proaktif olarak yönetmekle ilgili bir zihniyettir.
POI369 olarak biz, geliştirdiğimiz her üründe bu prensipleri temel alıyoruz. Ölçeklenebilir bir veritabanı mimarisi kurmak, projenin en başında, daha ilk satır kod yazılmadan başlar. Doğru veri modellemesi, akıllı indexleme stratejileri ve sürekli performans izleme, bizim için projenin olmazsa olmazlarıdır.
Bu yolculuk karmaşık görünebilir, ancak doğru araçlar ve doğru yaklaşımla, veritabanınızı bir performans canavarına dönüştürebilirsiniz. Unutmayın, hızlı bir uygulama, mutlu kullanıcılar ve başarılı bir iş demektir.
Eğer kendi projenizde benzer zorluklarla karşılaşıyorsanız veya dijital ürününüzü bir sonraki seviyeye taşıyacak teknoloji ortağı arıyorsanız, bizimle iletişime geçmekten çekinmeyin. Deneyimlerimizi paylaşmaktan ve size yardımcı olmaktan mutluluk duyarız.