Kapsamlı PostgreSQL Geliştirici Rehberi
Backend geliştiricileri için PostgreSQL’in en çok kullanılan özellikleri, best practice’leri ve pattern’leri hakkında derinlemesine, uygulamaya dönük bir rehber — sadece sorguyu çalıştırmak değil, Postgres’i doğru ve verimli kullanmak isteyenler için.
İçindekiler
- Temel Mimari Kavramlar
- Veri Tipleri
- Şema Tasarımı ve Kısıtlamalar
- İndeksleme
- Sorgu Yazımı ve Optimizasyon
- EXPLAIN ve Sorgu Planlayıcısı
- Transaction’lar ve İzolasyon Seviyeleri
- Kilitlenme (Locking) ve Eşzamanlılık
- JSON / JSONB
- Tam Metin Arama (Full-Text Search)
- Window Fonksiyonları ve İleri SQL
- Common Table Expressions (CTE)
- Partitioning (Bölümleme)
- Vacuum, Autovacuum ve Bloat
- Bağlantı Yönetimi ve Pooling
- Replikasyon ve Yüksek Erişilebilirlik
- Yedekleme ve Kurtarma
- Güvenlik Best Practice’leri
- Bilinmesi Gereken Extension’lar
- Yaygın Pattern’ler
- Migration’lar
- İzleme (Monitoring) ve Gözlemlenebilirlik
- Kaçınılması Gereken Anti-Pattern’ler
- Hızlı Referans Kontrol Listeleri
1. Temel Mimari Kavramlar
Postgres’in neden böyle davrandığını anlamak, rehberin geri kalanının anlaşılmasını çok kolaylaştırır.
- Process modeli: Postgres her bağlantı için bir işletim sistemi process’i kullanır (thread değil). Bu yüzden bağlantı sayısı çok önemlidir — her bağlantının gerçek bellek maliyeti vardır (en az birkaç MB,
work_memkullanımıyla daha da fazla). Connection pooling’in var olmasının temel nedeni budur. - MVCC (Multi-Version Concurrency Control): Postgres bir
UPDATEsırasında satırı yerinde asla üzerine yazmaz. Yeni bir satır versiyonu yazar ve eskisini “ölü” olarak işaretler.DELETEda alanı hemen boşaltmaz — sadece satırı ölü olarak işaretler. Bu mekanizma, bloklamayan okumaları mümkün kılar ve aynı zamanda Postgres’in nedenVACUUM‘a ihtiyaç duyduğunun sebebidir. - WAL (Write-Ahead Log): Her değişiklik, veri dosyalarına uygulanmadan önce WAL’a yazılır. WAL; crash recovery, replikasyon ve point-in-time recovery (PITR) için temel yapı taşıdır.
- Shared buffers: Postgres, veri sayfalarını bellekte önbelleğe alır (
shared_buffers). Ayrıca işletim sisteminin sayfa önbelleğine de büyük ölçüde güvenir — bu çift katmanlı önbellekleme davranışı bazı diğer veritabanlarına göre farklıdır ve bellek ayarlarını boyutlandırma şeklinizi etkiler. - Katalog (Catalog): Metadata (tablolar, kolonlar, indeksler, tipler) sistem kataloglarında (
pg_class,pg_attributevb.) tutulur ve normal tablolar gibi sorgulanabilir.
2. Veri Tipleri
Doğru tipleri baştan seçmek, ileride acı verici migration’lardan kurtarır.
Sayılar
integer(4 byte, ~±2.1 milyar) — ID/sayaç için varsayılan seçim.bigint(8 byte) — 2.1 milyarı aşabilecek her şey için kullanın (event tabloları, yüksek hacimli ID’ler). Baştanbigintkullanmak, sonradan migrate etmekten çok daha ucuzdur.numeric(p,s)— kesin hassasiyet; para birimleri ve tam ondalık aritmetik gerektiren her şey için kullanın. Para için aslafloat/double precisionkullanmayın.real/double precision— yaklaşık, hızlı; kesinliğin önemli olmadığı bilimsel/ölçüm verileri için uygundur.smallint— milyarlarca satırınız olmadıkça tasarrufu nadiren buna değer.
Metin
text— sınırsız uzunluk,varchar(n)‘e göre performans dezavantajı yoktur. Postgres’tevarchar(n)yerinetexttercih edin — bazı diğer veritabanlarının aksine,varchar(n)‘in depolama veya performans avantajı yoktur ve ileride “değer çok uzun” migration sıkıntılarından kurtulmuş olursunuz.varchar(n)‘i yalnızca veritabanı katmanında zorlanması gereken gerçek bir uzunluk sınırı iş kuralınız varsa kullanın.char(n)— neredeyse hiçbir zaman istediğiniz şey değildir; boşluklarla doldurur (pad eder). Kaçının.
Tarih/Zaman
timestamptz(zaman dilimli timestamp) — neredeyse her zaman istediğiniz budur. İçeride UTC olarak saklar ve gösterimde oturumun (session) zaman dilimi ayarına göre dönüştürür. Düztimestamp(zaman dilimsiz) kullanmak, uygulamanız veya sunucularınız birden fazla zaman dilimine yayıldığında sessiz hatalara yol açan klasik bir hatadır.date— saat bileşeni olmayan salt takvim tarihleri (doğum günleri, tatiller) için.interval— süreler için (“3 gün”, “2 saat”).
Boolean
boolean— yerleşik true/false/null; bunuintegerveyachar(1)ile taklit etmeyin.
UUID
uuid— yerleşik 16 byte’lık tip; UUID’leri metin olarak saklamak yerinegen_random_uuid()kullanın (Postgres 13+ ile çekirdekte/pgcryptoile mevcuttur). Not: rastgele (v4) UUID’ler primary key olarak kullanıldığında, yoğun insert yapılan tablolarda indeks lokalitesini bozar — alternatifler için Pattern’ler bölümündeki UUIDv7’ye bakın.
Diziler (Array)
- Yerleşik destek:
integer[],text[]vb. Küçük, denormalize listeler için (örn. etiketler) kullanışlıdır ama aşırı kullanmayın — kendi özniteliklerine veya referans bütünlüğüne ihtiyaç duyan her şey için uygun bir join tablosu genellikle daha esnektir.
JSON / JSONB
- Detaylı bilgi Bölüm 9‘da. Kısa özet: girdinin tam biçimini/anahtar sırasını korumanız gerekmedikçe her zaman
jsonyerinejsonbtercih edin.
Enum’lar
CREATE TYPE status AS ENUM ('pending','active','done');— hızlı ve kendi kendini belgeleyen bir yapıdır, ancak yeni değer eklemekALTER TYPE ... ADD VALUEgerektirir (tarihsel olarak başka DDL’lerle aynı transaction içinde çalıştırılamıyordu, ancak modern sürümlerde bu daha esnektir). Sık değişen değerler için, foreign key’li bir lookup tablosu daha esnektir.
Ağ tipleri
inet,cidr,macaddr— IP’leri metin olarak saklamak yerine bunları kullanın; doğrulanmıştır ve içerme (<<) gibi operatörleri destekler.
Range (Aralık) tipleri
int4range,tstzrange,daterangevb. — “başlangıç-bitiş” verileri (rezervasyonlar, geçerlilik dönemleri) için mükemmeldir ve çakışmaları önlemek için exclusion constraint’lerle (aşağıya bakın) iyi çalışır.
3. Şema Tasarımı ve Kısıtlamalar
Primary key’ler
- Eski
serialyerinebigint generated always as identity(SQL standardına uygun identity kolonları) tercih edin. Identity kolonları izinlerle daha iyi çalışır ve modern, standartlara uygun seçimdir:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
...
);
Foreign key’ler
- Foreign key kolonlarını her zaman indeksleyin — Postgres, referans veren (referencing) kolonda otomatik olarak indeks oluşturmaz (primary key tarafının aksine); burada eksik indeksler yavaş cascade delete’lere ve yavaş join’lere neden olur.
ON DELETEdavranışını bilinçli seçin:CASCADE,RESTRICT,SET NULLveyaSET DEFAULT. Hassas veride sessizCASCADE, yaygın bir kazara veri kaybı kaynağıdır.
Belge ve güvenlik ağı olarak kısıtlamalar
NOT NULL— cömertçe uygulayın; varsayılan olarak null’a izin vermek iyi bir varsayılan değildir.CHECKkısıtlamaları — iş kurallarını veritabanı katmanında zorlayın (CHECK (price >= 0)); bu, uygulama katmanı doğrulamasından sızabilecek hataları yakalar.UNIQUEkısıtlamaları — doğası gereği race condition’a açık olan uygulama seviyesi benzersizlik kontrolleri yerine bunu tercih edin.EXCLUDEkısıtlamaları — güçlü ama az kullanılan bir özelliktir; örneğin, aynı kaynak için çakışan tarih aralıklarını önleme:
CREATE TABLE bookings (
room_id int,
during tstzrange,
EXCLUDE USING gist (room_id WITH =, during WITH &&)
);
Bu, aynı oda için hiçbir iki rezervasyonun çakışamayacağını veritabanı seviyesinde garanti eder — uygulama kodunda hiçbir race condition mümkün değildir.
Normalizasyon vs. denormalizasyon
- Varsayılan olarak normalize edin (3NF iyi bir başlangıç noktasıdır). Sadece gerçek bir okuma performansı sorununu ölçtükten sonra bilinçli olarak denormalize edin ve denormalize edilmiş kopyanın neden var olduğunu ve nasıl senkron tutulduğunu (trigger, batch job, uygulama mantığı) belgeleyin.
İsimlendirme kuralları
- Tablo/kolon isimleri için tutarlı, lower_snake_case kullanın (Postgres, tırnaksız tanımlayıcıları küçük harfe çevirir — buna karşı her yerde
"CamelCase"isimleri tırnak içine alarak direnmek, yaygın bir kendi kendine yaratılan sorundur). - Tekil veya çoğul tablo isimleri — birini seçin ve proje genelinde tutarlı uygulayın.
4. İndeksleme
İndeksler, Postgres’teki en yüksek etkiye sahip tek performans aracıdır — ve en çok yanlış kullanılanıdır.
İndeks tipleri
- B-tree (varsayılan) — eşitlik ve aralık sorguları için iyidir (
=,<,>,BETWEEN, sıralama). Vakaların büyük çoğunluğunda kullanın. - GIN (Generalized Inverted Index) — çoklu değerli/bileşik kolonlar için:
jsonb, diziler, tam metin arama (tsvector). - GiST (Generalized Search Tree) — geometrik veri, range tipleri, exclusion constraint’leri, en yakın komşu (nearest-neighbor) aramaları için.
- BRIN (Block Range Index) — çok büyük tablolar için küçük, hızlı oluşturulan bir indeks; kolonun fiziksel satır sırasıyla ilişkili olduğu durumlarda iyidir (örn. append-only bir
created_atkolonu). B-tree’den çok daha küçüktür ama sadece bu korelasyon varsayımı altında faydalıdır. - Hash — artık nadiren gereklidir; B-tree eşitliği de aynı derecede iyi kapsar ve daha fazla operatörü destekler.
Pratik kurallar
- Foreign key’leri indeksleyin. Her zaman. Bu, en yaygın eksik-indeks hatasıdır.
- Bileşik (composite) indeks kolon sırası önemlidir.
(a, b)üzerindeki bir indeks, sadeceaveyaa AND bfiltresi olan sorguları destekler, ama sadecebüzerindeki bir filtreyi verimli desteklemez. Gerçek sorgu kalıplarınıza uygun şekilde, en seçici / tek başına en sık filtrelenen kolonu önce koyun. - Covering (kapsayıcı) indeksler — kolonları sıralama/arama anahtarının parçası yapmadan, sadece “index-only scan” için indekse eklemek üzere
INCLUDEkullanın:
CREATE INDEX idx_orders_customer ON orders (customer_id) INCLUDE (order_date, total);
- Kısmi (partial) indeksler — sadece gerçekten sorguladığınız satırları indeksleyin, indeks boyutunu ciddi şekilde küçültün:
CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'pending';
Soft-delete pattern’leri (WHERE deleted_at IS NULL) veya duruma göre filtrelenen sorgular için mükemmeldir.
- İfade (expression) indeksleri — her zaman bir fonksiyon/ifade üzerinden sorgu yapıyorsanız, bu ifadenin sonucunu indeksleyin:
CREATE INDEX idx_users_lower_email ON users (lower(email));
-- destekler: WHERE lower(email) = '[email protected]'
- Aşırı indeksleme yapmayın. Her indeks
INSERT/UPDATE/DELETEişlemlerini yavaşlatır ve depolama/önbellek tüketir.pg_stat_user_indexesile periyodik olarak kullanılmayan indeksleri denetleyin. - Üretim tablolarında yazma işlemlerini bloklamadan indeks oluşturmak için
CREATE INDEX CONCURRENTLYkullanın — oluşturması daha yavaştır ama tabloyu kilitleyenACCESS EXCLUSIVEkilidi almaz. Canlı tablolara karşı üretim migration’larında bunu her zaman kullanın. - Yinelenen/gereksiz indeksleri kontrol edin — çoğu sorgu kalıbı için
(a, b)zaten varsa,(a)genellikle gereksizdir.
5. Sorgu Yazımı ve Optimizasyon
- Sadece ihtiyacınız olan kolonları seçin.
SELECT *, index-only scan’leri engeller ve bant genişliğini boşa harcar. WHEREiçinde indekslenmiş kolonlar üzerinde fonksiyon kullanmaktan kaçının, uygun bir ifade indeksiniz olmadıkça —WHERE date_trunc('day', created_at) = ...,created_atüzerindeki düz bir indeksi kullanamaz.- Büyük alt sorgu sonuç kümeleri için
INyerineEXISTSkullanın — planlayıcı günümüzde genelde ikisini de benzer şekilde optimize eder, amaEXISTSkısa devre yapar, niyeti daha net iletir ve tarihsel olarak büyük kümelerdeIN‘den daha iyi performans göstermiştir. - Örtük (implicit) tip dönüşümlerine dikkat edin. Bir
textkolonunu birintegerliteral ile veya birintkolonunubigintile karşılaştırmak, sessizce indeks kullanımını engelleyebilir veya beklenmedik sonuçlara yol açabilir. - Yazma işlemlerini toplu yapın (batch). Çok satırlı
INSERT ... VALUES (...), (...), (...), azaltılmış round-trip’ler ve WAL yükü nedeniyle tekli satır insert’lerinden çok daha hızlıdır. Çok büyük yüklemeler içinCOPYkullanın. ORDER BYileLIMIT‘i birlikte kullanın — devasa bir sonuç kümesindeLIMITolmadanORDER BY, tam bir sıralamaya zorlar.- Sayfalama (Pagination): derin sayfalama için
OFFSET‘ten kaçının (yine de N satırı tarar ve atar). Bunun yerine keyset (anahtar tabanlı) sayfalama kullanın (bkz. Pattern’ler). - Hata yaması olarak
SELECT DISTINCTkullanmaktan kaçının — bir join’den tekrarlanan satırları kaldırmak içinDISTINCT‘e ihtiyacınız varsa, bu genellikle join’in yanlış olduğu anlamına gelir (örn. bire-çok join genişlemesi/fan-out). Bunun yerine sorgu mantığını düzeltin. RETURNINGkullanın — ayrı birSELECTyerine,INSERT/UPDATE/DELETE‘ten tek bir round-trip’te veri geri almak için.
6. EXPLAIN ve Sorgu Planlayıcısı
EXPLAIN olmadan sorgu optimizasyonu tahmin yürütmekten ibarettir.
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT ...;
EXPLAINtek başına planlanan çalıştırmayı gösterir (gerçek çalıştırma yapmaz).EXPLAIN ANALYZEsorguyu gerçekten çalıştırır ve gerçek zamanlamaları gösterir — üretimdeINSERT/UPDATE/DELETEile bunu çalıştırırken dikkatli olun (bir yazma sorgusunun planını test etmeniz gerekiyorsa transaction içine alıpROLLBACKyapın).BUFFERSgerçek sayfa hit/read’lerini gösterir — yavaş bir sorgunun CPU mu yoksa I/O mu darboğaz yaşadığını teşhis etmek için gereklidir.- Çıktıda dikkat edilmesi gereken temel şeyler:
- Beklediğiniz bir Index Scan yerine büyük bir tabloda Seq Scan — genellikle eksik indeks, sargable olmayan bir predicate, ya da planlayıcının seq scan’in daha ucuz olduğuna karar vermesi anlamına gelir (bu, küçük tablolar için veya satırların büyük bir kısmı seçiliyorsa doğru olabilir).
- Tahmini satır sayısı vs. gerçek satır sayısı — büyük bir sapma, eski/güncel olmayan istatistikleri işaret eder; tabloda
ANALYZEçalıştırın veyadefault_statistics_target‘ı kontrol edin. - Büyük, indekslenmemiş tablolara karşı Nested Loop join’leri — son derece yavaş olabilir; genellikle join kolonuna indeks eklenerek düzeltilir.
- Diske taşan (planda “external merge” olarak görünen) Sort işlemleri — o işlem için
work_mem‘in çok düşük olduğunu gösterir.
pg_stat_statementsextension’ı — tahmin yürütmek yerine, üretimde zaman içinde en yavaş/en sık çalışan sorguları gerçekten takip edin. Bu, neredeyse her ciddi kurulumda varsayılan olarak etkin olmalıdır.- Planlayıcı istatistiklerini güncel tutmak için
ANALYZEkullanın (ve autovacuum’un analyze işleminin çalıştığından emin olun) — büyük toplu yüklemelerden veya şema değişikliklerinden sonra kritik önem taşır.
7. Transaction’lar ve İzolasyon Seviyeleri
- Postgres varsayılan olarak
READ COMMITTEDizolasyon seviyesini kullanır — bir transaction içindeki her ifade, transaction’ın başladığı andaki değil, o ifadenin başladığı andaki bir snapshot’ı görür. REPEATABLE READ— tüm transaction, başlangıcından itibaren tutarlı tek bir snapshot görür; eşzamanlı bir transaction, sizin transaction’ınızın bağımlı olduğu veriyi değiştirirse ve her ikisi de çakışan değişiklikleri commit etmeye çalışırsa bir serialization hatası fırlatır.SERIALIZABLE— en katı seviyedir; transaction’lar sanki sırayla, birer birer çalışıyormuş gibi davranır. SadeceREPEATABLE READ‘in önleyemediği ince anomalileri (write skew gibi) önler. Daha fazla overhead getirir ve uygulamanızın serialization hatalarında (SQLSTATE 40001) yeniden denemesi (retry) gerekir.- Transaction’ları kısa tutun. Uzun süren transaction’lar, autovacuum’un ölü satırları temizleme yeteneğini geciktirir (çünkü Postgres, o eski transaction’a hâlâ görünür olabilecek eski satır versiyonlarını saklamak zorundadır); bu, yaygın bir tablo bloat kaynağıdır.
- Dış I/O beklerken (bir API çağrısı, kullanıcı girdisi) transaction’ı asla açık bırakmayın — bu, üretimde kilit çekişmesi (lock contention) ve bloat’ın en yaygın nedenlerinden biridir.
- Bir transaction içinde session ayarlarını (örn.
statement_timeout) sadece o transaction’a özgü kılmak içinSET LOCALkullanın.
8. Kilitlenme (Locking) ve Eşzamanlılık
- Satır seviyesi kilitler:
SELECT ... FOR UPDATE, seçilen satırları eşzamanlı değişikliklere karşı kilitler — “bakiyeyi oku, sonra güncelle” gibi pattern’lerde kayıp güncellemeleri (lost update) önlemek için gereklidir. FOR UPDATE SKIP LOCKED— job queue oluşturmak için harika bir pattern: birden fazla worker, birbirini bloklamadan farklı satırları alabilir:
SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
FOR UPDATE NOWAIT— bir satır kilitliyse beklemek yerine hemen başarısız olur; kuyruğa girmek yerine hızlıca başarısız olmak istediğinizde kullanışlıdır.- Tablo seviyesi kilitler:
ALTER TABLE ... ADD COLUMN(varsayılan değer olmadan, modern Postgres’te) gibi DDL işlemleri hızlı, sadece metadata değişiklikleridir; ancak değişken (volatile) bir varsayılan değerle kolon eklemek veya bir kolonun tipini değiştirmek tüm tabloyu yeniden yazabilir veACCESS EXCLUSIVEkilidi alarak işlem süresince tüm okuma/yazmaları bloklayabilir. Büyük bir üretim tablosunda bir işlemi çalıştırmadan önce her zaman “hızlı yol” olup olmadığını kontrol edin. - Deadlock’lar: Postgres deadlock’ları tespit eder ve bir tarafı otomatik olarak iptal eder. Bunları önlemek için kilitleri (satır kilitleri dahil,
UPDATE/SELECT FOR UPDATEyoluyla) kod tabanınız genelinde her zaman tutarlı bir sırayla edinin. - Advisory (danışma) kilitleri (
pg_advisory_lock) — herhangi bir satır/tabloya bağlı olmayan uygulama seviyesi kilitler; “bu cron job’ının aynı anda sadece bir örneğinin çalıştığından emin ol” gibi durumlar için kullanışlıdır.
9. JSON / JSONB
- Neredeyse tüm durumlarda
jsondeğil,jsonbkullanın.jsonb, ayrıştırılmış (parsed) ikili bir temsil saklar (sorgulaması daha hızlıdır, indekslenebilir);jsonise girdinin tam metnini saklar (insert biraz daha hızlıdır, anahtar sırasını/boşlukları/tekrar eden anahtarları korur — nadiren ihtiyacınız olan bir şeydir). - Kapsam/varlık sorguları için jsonb’yi GIN ile indeksleyin:
CREATE INDEX idx_products_attrs ON products USING gin (attributes);
-- destekler: WHERE attributes @> '{"color": "red"}'
- Sürekli tek bir alanı sorguluyorsanız belirli anahtarlar üzerinde ifade indeksleri:
CREATE INDEX idx_products_sku ON products ((attributes->>'sku'));
- Bilinmesi gereken operatörler:
->(JSON değerini jsonb olarak alır),->>(değeri metin olarak alır),#>/#>>(iç içe geçmiş path alır),@>(içerir),?(anahtar var mı),jsonb_set(),jsonb_build_object(),||(üst seviye anahtarları birleştirir/merge eder). jsonb‘yi gerçek bir şemanın yerine kullanmayın. Gerçekten değişken/seyrek özellikler için (örn. kategoriye göre farklılık gösteren ürün öznitelikleri) mükemmeldir, ama düzenli olarak filtrelediğiniz, join yaptığınız veya aggregate ettiğiniz kolonlar için zayıf bir alternatiftir — bunlar gerçek, tiplenmiş kolonlara ait olmalıdır.- Yapıyı uygulama katmanında veya
CHECKkısıtlamalarıyla doğrulayın (örn.CHECK (jsonb_typeof(data) = 'object')) — aksi halde Postgres, birjsonbkolonu içinde belirli bir yapıyı zorlamaz.
10. Tam Metin Arama (Full-Text Search)
Postgres, gerçekten yetenekli, yerleşik bir tam metin arama motoruna sahiptir — küçük-orta ölçekte Elasticsearch kurmaktan kaçınmak için genellikle yeterlidir.
tsvector— önceden işlenmiş, normalize edilmiş bir doküman temsilidir (kök alınmış, stop word’lerden arındırılmış).tsquery— ayrıştırılmış bir arama sorgusudur.
ALTER TABLE articles ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;
CREATE INDEX idx_articles_search ON articles USING gin (search_vector);
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english', 'postgres & performance');
- Yukarıdaki gibi bir generated (üretilmiş) kolon kullanmak,
tsvector‘ün kaynak kolonlarla otomatik olarak senkron kalmasını sağlar — trigger gerekmez ve indekslenebilir. ts_rank/ts_rank_cd— sonuçları sıralama için alaka düzeyine (relevance) göre puanlar.websearch_to_tsquery— ham kullanıcı girdisinden Google tarzı arama sözdizimini (tırnaklar,-hariç tutma,OR) ayrıştırır; elletsqueryoluşturmak yerine genellikle kullanıcıya açık arama kutuları için doğru giriş noktasıdır.- Bulanık/yazım hatasına toleranslı arama için,
pg_trgmextension’ı (trigram benzerlik) ilesimilarity()/%üzerinde bir GIN veya GiST indeksi eşleştirin.
11. Window Fonksiyonları ve İleri SQL
Window fonksiyonları, mevcut satırla ilişkili bir satır kümesi üzerinde hesaplama yapar, ancak (GROUP BY‘ın aksine) bunları gruplara çökertmez.
SELECT
employee_id,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
salary - LAG(salary) OVER (ORDER BY hire_date) AS diff_from_prev_hire
FROM employees;
ROW_NUMBER()— her partition için benzersiz, sıralı bir numara; “grup başına ilk N” sorguları ve deduplikasyon için kullanışlıdır.RANK()/DENSE_RANK()—ROW_NUMBER()‘a benzer ama beraberlikleri (tie) yönetir (RANKberaberliklerden sonra boşluk bırakır,DENSE_RANKbırakmaz).LAG()/LEAD()— self-join yapmadan önceki/sonraki bir satırın değerine erişim sağlar — dönemler arası (period-over-period) karşılaştırmalar için mükemmeldir.SUM()/AVG()/COUNT() OVER (...)— frame ifadeleri (ROWS BETWEEN ... AND ...) aracılığıyla kümülatif toplamlar ve hareketli ortalamalar.- “Grup başına ilk N” pattern’i:
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) rn
FROM employees
) t WHERE rn <= 3;
Diğer ileri seviye yapılar
LATERALjoin’ler — bir join’in sağ tarafındaki alt sorgunun, sol taraftaki satırın kolonlarına satır satır referans vermesine izin verir; yalnızca window fonksiyonlarıyla verimli ifade edilemeyen “satır başına ilişkili ilk N satır” sorguları için gereklidir.GROUPING SETS/ROLLUP/CUBE— birden fazlaGROUP BYsorgusunuUNIONile birleştirmek yerine, tek bir sorgu geçişinde birden fazla toplama seviyesi (örn. ara toplamlar ve genel toplam) hesaplar.FILTERifadesi —SUM(CASE WHEN ... THEN 1 ELSE 0 END)yerine, temiz bir şekilde koşullu toplama:COUNT(*) FILTER (WHERE status = 'active').
12. Common Table Expressions (CTE)
WITH regional_sales AS (
SELECT region, SUM(amount) AS total
FROM orders
GROUP BY region
)
SELECT * FROM regional_sales WHERE total > 100000;
- CTE’ler, karmaşık sorguları isimlendirilmiş, sıralı adımlara bölerek okunabilirliği artırır.
- Modern Postgres (12+) CTE’leri varsayılan olarak inline eder (bunları her zaman bir optimizasyon bariyeri olarak materialize eden eski sürümlerin aksine). Bu, CTE’lerin artık otomatik bir performans cezası taşımadığı anlamına gelir — ancak planlayıcının predicate’leri CTE’ye itmesini özellikle engellemek istediğinizde (örn. yan etkileri olan bir CTE için veya belirli bir planı zorlamak için)
MATERIALIZEDile materyalizasyonu açıkça zorlayabilirsiniz. - Recursive (özyinelemeli) CTE’ler (
WITH RECURSIVE) — hiyerarşik/graf verileri için standart araçtır: organizasyon şemaları, kategori ağaçları, malzeme listesi (bill-of-materials) açılımları:
WITH RECURSIVE subordinates AS (
SELECT id, manager_id, name FROM employees WHERE id = 1
UNION ALL
SELECT e.id, e.manager_id, e.name
FROM employees e
JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;
- Yazılabilir (writable) CTE’ler — bir CTE içinde
INSERT/UPDATE/DELETE ... RETURNINGzincirleyebilir ve sonucu bir sonraki adıma besleyebilirsiniz; “veriyi A’dan B’ye taşı” işlemleri için tek bir atomik ifadede kullanışlıdır:
WITH moved AS (
DELETE FROM staging_orders WHERE processed = true RETURNING *
)
INSERT INTO orders SELECT * FROM moved;
13. Partitioning (Bölümleme)
Çok büyük tablolar için (on milyonlarca+ satır), yerleşik bildirimsel (declarative) partitioning, tek bir mantıksal tabloyu fiziksel alt tablolara böler.
- Range (aralık) partitioning — en yaygın olanı; genellikle tarihe göre (
created_at); “eski veriyi sil” işlemini kolaylaştırır (yavaş birDELETEyerine bir partition’ı düşürmek) ve sorgu budamasını (query pruning) sağlar (partition anahtarına göre filtreleyen sorgular, ilgisiz partition’ları tamamen atlar).
CREATE TABLE events (
id bigint,
created_at timestamptz NOT NULL,
payload jsonb
) PARTITION BY RANGE (created_at);
CREATE TABLE events_2026_01 PARTITION OF events
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
- List partitioning — ayrık değerlere göre (örn.
region,tenant_id). - Hash partitioning — doğal bir range/list anahtarı olmadığında eşit dağılım için.
- Ne zaman bölümlenmeli: tablo çok büyük, çoğu sorgunun filtrelediği net bir partition anahtarı var ve/veya eski verinin verimli toplu silinmesine ihtiyacınız var (örn. veri saklama politikaları). Erken bölümlemeyin — gerçek bir karmaşıklık ekler (kısıtlamalar, indeksler ve foreign key’lerin her partition için ekstra dikkat gerektirmesi).
- Partition oluşturmayı otomatikleştirin (örn.
pg_partmanextension’ı veya zamanlanmış bir job aracılığıyla) — bir sonraki dönemin partition’ını oluşturmayı unutmak, klasik bir üretim olayıdır (incident). - Bölümlenmiş bir tabloya referans veren foreign key’lerin tarihsel olarak kısıtlamaları olmuştur — kendi Postgres sürümünüzdeki davranışı doğrulayın.
14. Vacuum, Autovacuum ve Bloat
Bu, Postgres operasyonlarının en yanlış anlaşılan bölümlerinden biridir.
- MVCC nedeniyle,
UPDATE/DELETEişlemleri “ölü tuple’lar” bırakır.VACUUMbu alanı yeniden kullanım için geri kazanır (ancak genellikle diskteki dosyayı küçültmez — aşağıdakiVACUUM FULL‘a bakın). - Autovacuum bunu otomatik olarak eşiklere göre çalıştırır (
autovacuum_vacuum_threshold+ tablo boyutunun bir yüzdesi olanautovacuum_vacuum_scale_factor). Varsayılan ayarlar tutucudur (conservative) ve yüksek değişim oranlı (high-churn) tablolar için çoğunlukla yetersiz sıklıktadır. - Vacuum’un geride kalmasının belirtileri: satır sayısı artmadan tablo/indeks boyutunun büyümesi (bloat), yavaşlayan sequential scan’ler,
EXPLAINtahminlerinin gerçeklikten sapması ve aşırı durumlarda transaction ID wraparound uyarıları. - Yüksek yazma trafiğine sahip tablolar için tablo başına ayar (tuning):
ALTER TABLE hot_table SET (autovacuum_vacuum_scale_factor = 0.01, autovacuum_analyze_scale_factor = 0.005);
VACUUM FULL— tüm tabloyu yeniden yazarak disk alanını gerçekten geri kazanır, ancakACCESS EXCLUSIVEkilidi alır (her şeyi bloklar). Sadece bakım pencerelerinde çalıştırın, ya da çevrimiçi bir alternatif içinpg_repackextension’ını kullanın.- Transaction ID wraparound — Postgres transaction ID’leri 32 bit ve döngüseldir; autovacuum eski satırları zamanında “dondurmayı” (freeze) başaramazsa, sonunda verinin erişilemez hale gelme riskiyle karşılaşırsınız. Üretimde
age(datfrozenxid)‘i izleyin. Buna denk gelmek nadirdir ama görmezden gelinirse felaket getirir — autovacuum’un basitçe devre dışı bırakılamamasının sebebi budur. ANALYZE(istatistik yenileme),VACUUM‘dan (alan geri kazanımı) ayrı, daha ucuz bir işlemdir — autovacuum ikisini de yapar, ancak planlayıcı istatistiklerini hemen yenilemek için toplu veri değişikliklerinden sonra tek başınaANALYZEde çalıştırabilirsiniz.
15. Bağlantı Yönetimi ve Pooling
- Her Postgres bağlantısı tam bir işletim sistemi process’i olduğundan, bağlantı sayısı gerçek, sınırlı bir kaynaktır — güçlü donanımda bile tipik olarak on binlerce değil, yüzlerce mertebesindedir.
- Bir connection pooler kullanın — PgBouncer standart seçimdir. Çoğu web uygulama iş yükü için
transactionpooling modunda çalıştırın (bir bağlantı, tüm client oturumu boyunca değil, yalnızca bir transaction süresince “kullanımda” olarak işaretlenir). transactionmodu pooling’in session seviyesi özellikleri bozduğunun farkında olun: transaction’lar arası prepared statement’lar, session seviyesiSET(bunun yerineSET LOCALkullanın), transaction’lar arası tutulan advisory kilitler veLISTEN/NOTIFY. Bu özelliklere güvenmeden önce pooling modunuzun kısıtlamalarını bilin.- Uygulama tarafı pool’lar (örn. ORM’nizde/driver’ınızda) muhafazakâr boyutlandırılmalıdır — CPU çekirdek sayınızdan fazla bağlantı, context-switching yükü nedeniyle genellikle ters etki yapar; tüm uygulama instance’ları genelindeki toplam bağlantı sayısı,
max_connections‘ın rahatlıkla altında olmalıdır. - Kaçak sorguları ve “unutulmuş” açık transaction’ların kilitleri süresiz tutmasını önlemek için makul bir
statement_timeoutveidle_in_transaction_session_timeoutayarlayın.
16. Replikasyon ve Yüksek Erişilebilirlik
- Streaming (akış) replikasyonu (yerleşik) — bir primary (birincil), WAL’ı bir veya daha fazla standby’a neredeyse gerçek zamanlı olarak gönderir. Standby’lar salt-okunur sorguları yanıtlayabilir (hot standby), okuma trafiğini ölçeklendirmek için kullanışlıdır.
- Senkron vs. asenkron replikasyon — senkron, standby’ın yazmayı onayladığından emin olduktan sonra primary’nin başarıyı bildirmesini garanti eder (daha güçlü dayanıklılık, ek yazma gecikmesi); asenkron varsayılandır (daha düşük gecikme, failover sırasında son birkaç transaction’ı kaybetme riski).
- Mantıksal (logical) replikasyon — ham WAL/byte seviyesinde değil, satır/ifade seviyesinde replike eder; belirli tabloları replike etmeyi, farklı büyük Postgres sürümleri arasında replikasyonu ve verinin harici sistemlere (örn. arama indeksleri, veri ambarları, CDC pipeline’ları) beslenmesini mümkün kılar.
- Failover — Postgres çekirdeği otomatik failover içermez; bu, Patroni, repmgr gibi araçlar veya yönetilen bulut hizmetleri tarafından ele alınır. Bunu açıkça planlayın — “replikasyonu etkinleştirmek” ile “yüksek erişilebilirlik” aynı şey değildir.
- Read replica gecikmesi (lag) — primary üzerindeki
pg_stat_replication‘dan veya WAL pozisyonlarını karşılaştırarak her zaman erişilebilirdir; uygulamanızı replika’larda eventual consistency’ye (nihai tutarlılık) tolerans gösterecek şekilde tasarlayın (örn. primary’ye yazdıktan hemen sonra, gecikmeyi hesaba katmadan bir replika’dan kendi yazdığınızı okumaya çalışmayın).
17. Yedekleme ve Kurtarma
pg_dump— tek bir veritabanının (şema + veri) mantıksal yedeğidir, sürümler/platformlar arası taşınabilirdir, ancak çok büyük veritabanlarına iyi ölçeklenmez (tek thread’li restore yavaştır; paralel dump/restore içinpg_dump --jobs=Nvepg_restore --jobs=Nyardımcı olur).pg_basebackup— tüm cluster’ın (tüm veritabanlarının) fiziksel yedeğidir (büyük veri kümeleri için daha hızlıdır), WAL tabanlı Point-in-Time Recovery (PITR)‘ın temeli olarak kullanılır.- PITR — bir base backup’ı sürekli WAL dosyası akışıyla birleştirmek, herhangi bir belirli ana geri dönmenizi sağlar (kazara veri silinmesinden veya kötü bir deploy’dan kurtulmak için hayati önem taşır). pgBackRest ve WAL-G gibi araçlar bunu üretimde iyi yönetir.
- Restore’larınızı test edin. Test-restore yapmadığınız bir yedek, bir yedek değil bir hipotezdir. Bu, en yaygın atlanan ve atlanması en maliyetli operasyonel uygulamadır.
- Büyük sürüm yükseltmelerinden veya riskli şema migration’larından önce her zaman yedek alın.
18. Güvenlik Best Practice’leri
- En az yetki ilkesi (principle of least privilege): uygulama rolleri superuser olmamalıdır. Sadece ihtiyaç duyulan şema/tablolarda, ihtiyaç duyulan belirli yetkileri (
SELECT,INSERTvb.) verin. - Her kullanıcıya doğrudan bireysel yetkiler vermek yerine, izinleri gruplamak için rolleri kullanın, sonra bu rolleri kullanıcılara atayın.
- Row-Level Security (RLS) — uygulama mantığından bağımsız olarak, satır bazlı erişim kurallarını veritabanı seviyesinde zorlar — çok kiracılı (multi-tenant) sistemler için değerlidir:
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Bu gerçek bir savunma derinliği (defense-in-depth) katmanıdır: RLS doğru yapılandırıldığında, bir SQL injection hatası veya bir uygulama hatası bile kiracılar (tenant) arası veri sızdıramaz, çünkü sınırı veritabanının kendisi zorlar.
- Her zaman parametreli sorgular kullanın — kullanıcı girdisini asla SQL’e string olarak birleştirmeyin (concatenate). Bu, ORM kullanımından bağımsız olarak geçerlidir; ham sorgular veya dinamik SQL (
EXECUTE format(...)), PL/pgSQL’de%I/%Ltırnaklama konusunda ekstra dikkat gerektirir. - Üretimde, özellikle tam olarak kontrol etmediğiniz bir ağ üzerinden,
sslmode=require(veya daha katısı:verify-full) ile bağlantıları şifreleyin. - Veritabanı seviyesi bir ihlale karşı bile koruma gerektiğinde, hassas kolonları uygulama katmanında veya
pgcryptoile şifreleyin — Postgres, varsayılan olarak tekil kolon değerlerini şifrelemez. - Uyumluluk açısından hassas sistemler için denetim günlüğü (audit logging) —
pgauditextension’ını veya Postgres’in yerleşik loglama’sını (log_statement) kullanın. - Postgres’i güncel tutun — küçük (minor) sürüm güncellemeleri genellikle güvenlik düzeltmeleri içerir ve güvenli/düşük riskli şekilde uygulanmak üzere tasarlanmıştır.
19. Bilinmesi Gereken Extension’lar
CREATE EXTENSION extension_adi; ile etkinleştirilir.
| Extension | Ne yapar |
|---|---|
pg_stat_statements | Tüm sorgular genelinde sorgu performans istatistiklerini takip eder — üretimde varsayılan olarak açık olmalıdır |
pgcrypto | Kriptografik fonksiyonlar, eski sürümlerde gen_random_uuid() |
pg_trgm | Trigram tabanlı bulanık metin eşleştirme/benzerlik arama |
postgis | Tam coğrafi/mekânsal veri tipleri ve fonksiyonları — mekânsal veri için endüstri standardı |
pg_partman | Partition oluşturma/bakımını otomatikleştirir |
pg_repack | Uzun exclusive kilit olmadan çevrimiçi tablo/indeks bloat temizliği |
pgaudit | Detaylı oturum/nesne denetim günlüğü |
hstore | Basit anahtar-değer tipi (yeni projelerde büyük ölçüde jsonb tarafından geçilmiştir) |
uuid-ossp | Eski UUID üretim fonksiyonları (artık gen_random_uuid() yerleşik olduğundan çoğunlukla gereksiz) |
citext | Büyük/küçük harf duyarsız metin tipi — karşılaştırmaları her zaman lower() içine sarmaktan daha basittir |
timescaledb | Zaman serisi optimizasyonları (varsayılan olarak paketlenmemiştir ama IoT/metrik iş yükleri için yaygın kullanılır) |
20. Yaygın Pattern’ler
Upsert (INSERT … ON CONFLICT)
INSERT INTO users (email, name)
VALUES ('[email protected]', 'Alice')
ON CONFLICT (email)
DO UPDATE SET name = EXCLUDED.name, updated_at = now();
Atomiktir, race-condition içermez — uygulama kodundan “var mı diye kontrol et, sonra insert veya update yap” yaklaşımından çok daha iyidir.
Keyset (cursor tabanlı) sayfalama
Bunun yerine:
SELECT * FROM posts ORDER BY created_at DESC OFFSET 10000 LIMIT 20; -- derinlerde yavaş
Şunu kullanın:
SELECT * FROM posts
WHERE created_at < :last_seen_created_at
ORDER BY created_at DESC
LIMIT 20;
created_at (veya onunla birlikte kullanılan benzersiz bir eşitleyici) indekslendiği sürece, sayfa derinliğinden bağımsız sabit performans sağlar.
Soft delete (yumuşak silme)
ALTER TABLE orders ADD COLUMN deleted_at timestamptz;
CREATE INDEX idx_orders_active ON orders (id) WHERE deleted_at IS NULL;
“Aktif satırlar” sorgu kalıbı için her soft-delete kolonunu bir kısmi indeksle eşleştirin, yoksa silinen satırlar birikçe sorgular yavaşlar.
Optimistic locking (DB seviyesi kilit olmadan kayıp güncellemeleri önleme)
UPDATE accounts SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = :beklenen_versiyon;
-- etkilenen satır sayısını kontrol edin == 1; 0 ise, başka biri önce güncellemiştir — yeniden deneyin veya yeniden yükleyin
SKIP LOCKED ile job queue
(bkz. Bölüm 8) — ayrı bir mesaj broker’ı olmadan basit bir job/task kuyruğu için standart, Postgres’e özgü pattern.
Primary key’ler için UUIDv7
Primary key olarak rastgele (v4) UUID’ler, yeni değerler B-tree üzerinde sona eklenmek yerine rastgele dağıldığı için, yoğun insert yapılan tablolarda indeks parçalanmasına (fragmentation) neden olur. UUIDv7 (zamana göre sıralı; native destek olmayan Postgres sürümlerinde extension’lar veya uygulama seviyesi üretim ile desteklenir), UUID’nin benzersizlik/tahmin edilemezlik avantajlarını, sıralı bir integer’ın davranışına çok daha yakın, çok daha iyi insert lokalitesiyle birlikte sunar.
Trigger’larla denetim izi (audit trail)
CREATE TABLE orders_audit (LIKE orders INCLUDING ALL, operation text, changed_at timestamptz DEFAULT now());
CREATE OR REPLACE FUNCTION audit_orders() RETURNS trigger AS $$
BEGIN
INSERT INTO orders_audit SELECT OLD.*, TG_OP, now();
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER orders_audit_trigger
AFTER UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit_orders();
Hafif pub-sub için LISTEN/NOTIFY
LISTEN new_order;
NOTIFY new_order, '{"order_id": 123}';
Polling yapmadan DB olaylarına uygulama katmanı tepkileri tetiklemek için kullanışlıdır — ancak dayanıklı (durable) bir mesaj kuyruğu değildir (kimse dinlemiyorsa bildirimler kaybolur; garantili teslimat için gerçek bir kuyruk/CDC sistemi kullanın).
21. Migration’lar
- Her zaman bir migration aracı kullanın (Flyway, Liquibase, Alembic, Sqitch veya framework’ünüzün yerleşik migration sistemi) — şema değişikliklerini asla elle üretime uygulamayın.
- Postgres sürümünüzde hangi DDL işlemlerinin “hızlı” (sadece metadata) vs. “yavaş” (tablo yeniden yazımı) olduğunu anlayın:
- Hızlı: varsayılan değer olmadan
ADD COLUMN(veya modern Postgres 11+‘da sabit bir varsayılan değerle),DROP COLUMN, nullable bir kolon ekleme. - Yavaş / tabloyu yeniden yazar: değişken (volatile) bir varsayılan değerle kolon ekleme, bir kolonun tipini değiştirme (çoğu durumda), önceden doğrulanmış bir check olmadan
NOT NULLkısıtlaması ekleme.
- Hızlı: varsayılan değer olmadan
- Uzun kilitleri önlemek için büyük tablolarda kısıtlamaları iki adımda ekleyin: önce
NOT VALIDolarak ekleyin (hızlıdır, doğrulama için tarama/kilit yapmaz), sonra ayrı olarakVALIDATE CONSTRAINTçalıştırın (tarar ama daha hafif bir kilit alır, tüm süre boyunca okuma/yazmayı bloklamaz):
ALTER TABLE orders ADD CONSTRAINT chk_positive CHECK (total >= 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT chk_positive;
- Üretim tablolarında her zaman
CREATE INDEX CONCURRENTLYkullanın (bkz. Bölüm 4). - Sıfır kesinti (zero-downtime) deploy’lar için geriye dönük uyumlu migration’lar: bir kolonu yeniden adlandırırken/kaldırırken, aşamalı deploy yapın — (1) yeni kolon ekleyin, uygulamadan çift-yazma (dual-write) yapın, geriye doldurun (backfill), (2) okumaları yeni kolona çevirin, (3) eski kolona yazmayı durdurun, (4) eski kolonu daha sonraki bir deploy’da kaldırın. Deploy sırasında çalışan uygulama sürümünü kıracak bir yeniden adlandırma asla yapmayın.
- Üretimde çalıştırmadan önce migration’ları üretim büyüklüğünde bir veri kümesine karşı test edin — 100 satırlı bir geliştirme veritabanında anında olan bir migration, 100 milyon satırlı bir üretim tablosunu dakikalarca kilitleyebilir.
22. İzleme (Monitoring) ve Gözlemlenebilirlik
Üretimde takip edilmesi gereken temel şeyler:
pg_stat_statements— en yavaş ve en sık çalışan sorgular.pg_stat_activity— o an çalışan sorgular, bloke olmuş sorgular, uzun süren transaction’lar.pg_stat_user_tables/pg_stat_user_indexes— sequential scan vs. index scan sayıları (indeksi olması gereken ama olmayan tabloları tespit edin), kullanılmayan indeksler (uzun süredir var olan bir indeksteidx_scan = 0, kaldırma adayıdır).pg_stat_activityile birleştirilmişpg_locks— kilit çekişmesini/bloke zincirlerini gerçek zamanlı teşhis etme.- Replikasyon gecikmesi — primary üzerindeki
pg_stat_replication. - Önbellek isabet oranı (cache hit ratio) — düşük bir
shared_buffersisabet oranı (pg_statio_user_tablesüzerinden) bellek baskısını veyashared_buffers‘ın küçük boyutlandırıldığını gösterir. - Tablo/indeks bloat tahminleri —
pg_stat_user_tables/pgstattupleextension’ına karşı topluluk sorguları aracılığıyla. - Standart araçlar: pgAdmin, pganalyze,
postgres_exporterile Datadog/Grafana ve çoğu yönetilen bulut sağlayıcısının yerleşik dashboard’ları (RDS Performance Insights, Cloud SQL Insights vb.).
23. Kaçınılması Gereken Anti-Pattern’ler
- ❌ Büyümesi beklenen tablolarda
SERIAL/intprimary key kullanmak — ilk gündenbigint‘e geçin. - ❌ Kullanıcıya açık veya bölgeler arası herhangi bir şey için zaman dilimsiz
timestampkullanmak. - ❌ Foreign key kolonlarında eksik indeksler.
- ❌ Uygulama kodunda ve hot path’lerde kullanılan view’larda
SELECT *. - ❌ Büyük tablolarda derin
OFFSETtabanlı sayfalama. - ❌ Parayı
float/double precisionolarak saklamak. - ❌ Şema tasarımından tamamen kaçınmak için
jsonbkullanmak (gerçek kolonlara sahip olması gereken “şemasız” tablolar). - ❌ Satır kilitleri tutan veya vacuum’u bloke eden uzun süren transaction’lar.
- ❌ Kilit etkilerini anlamadan, yoğun trafik saatlerinde büyük bir tabloda
VACUUM FULLveya ağır DDL çalıştırmak. - ❌ DB seviyesi
UNIQUEkısıtlaması +ON CONFLICTyerine uygulama seviyesi benzersizlik kontrolleri (“var mı diye kontrol et, sonra insert et”). - ❌
EXPLAIN ANALYZEçıktısını görmezden gelip performans düzeltmelerini tahmin etmek. - ❌ Autovacuum’u ayarlamak yerine devre dışı bırakmak veya görmezden gelmek.
- ❌ Yedek restore’larını hiç test etmemek.
- ❌ Uygulama veritabanı rollerine superuser/geniş yetkiler vermek.
- ❌ Kullanıcı girdisini SQL sorgularına string olarak birleştirmek.
24. Hızlı Referans Kontrol Listeleri
Yeni tablo kontrol listesi
-
bigint generated always as identityprimary key (serialdeğil, gerçekten küçük/sınırlı değilse düzintde değil) - Tüm timestamp kolonları için
timestamptz - Asla null olmaması gereken kolonlarda
NOT NULL - İş kuralları için
CHECKkısıtlamaları - Foreign key’ler indekslenmiş
- İsimlendirme kuralına uyulmuş (snake_case)
- Yaygın filtrelenen sorgular için kısmi indeks düşünülmüş (örn. soft-delete pattern’i)
Üretime bir migration deploy etmeden önce
- Üretim büyüklüğünde veriye karşı test edilmiş
- Yeni indeksler için
CREATE INDEX CONCURRENTLYkullanılmış - Büyük tablolarda yeni kısıtlamalar için
NOT VALID+VALIDATE CONSTRAINTpattern’i kullanılmış - DDL’in
ACCESS EXCLUSIVEkilidi alıp almadığı doğrulanmış ve buna göre zamanlanmış - Şu anda deploy edilmiş uygulama sürümüyle geriye dönük uyumlu
Sorgu performansı kontrol listesi
- Gerçek sorguda
EXPLAIN (ANALYZE, BUFFERS)çalıştırıldı - Index Scan beklenen yerde Seq Scan olup olmadığı kontrol edildi
- Planlayıcı satır tahminlerinin gerçeklikle yaklaşık eşleştiği doğrulandı (istatistikler güncel)
- İlgili indekslerin var olduğu ve kolon sırasının filtre kalıplarıyla eşleştiği teyit edildi
- Bu sorgunun gerçek dünyadaki sıklığı/maliyeti için
pg_stat_statementskontrol edildi
Üretime hazırlık kontrol listesi
- Veritabanının önünde bir connection pooler (PgBouncer)
-
pg_stat_statementsetkin - Test edilmiş restore prosedürüyle otomatik yedekler
- Replikasyon / HA stratejisi tanımlanmış (sadece “replikasyon açık” değil)
- Kilitler, replikasyon gecikmesi, önbellek isabet oranı, bloat için izleme dashboard’ları
-
statement_timeoutveidle_in_transaction_session_timeoutyapılandırılmış - Autovacuum, özellikle yüksek değişim oranlı tablolar için ayarlanmış
Bu rehber, çalışan bir backend/uygulama geliştiricisinin günlük olarak ihtiyaç duyduğu şeylerin büyük çoğunluğunu kapsar. Derin iç yapı detayları (depolama format detayları, WAL iç yapısı, planlayıcı maliyet modeli iç yapısı) için, gerçekten mükemmel olan ve otoriter detay için doğrudan okumaya değer resmi PostgreSQL dokümantasyonuna başvurun.