Veritabanı Yönetimi

    PostgreSQL Tablo Partitioning

    Büyük PostgreSQL tablolarını range, list ve hash partition ile bölmenin pratik rehberi.

    11 dk okuma Güncellendi: 25 Ağustos 2026

    Bir tabloya yüz milyonlarca satır yığıldıktan sonra DELETE FROM olaylar WHERE olusturuldu < '2025-01-01' komutunu çalıştırıp yarım saat bekleyen, sonra da tablonun diskte hiç küçülmediğini fark eden biriyseniz, PostgreSQL partitioning tam olarak bu problemi çözmek için var. Tablo bölümleme, tek bir mantıksal tabloyu fiziksel olarak birden çok küçük tabloya dağıtmaktır. Uygulamanız hâlâ tek bir olaylar tablosuna yazar ve okur; PostgreSQL arka planda her satırı doğru parçaya yönlendirir, sorgu geldiğinde de yalnızca ilgili parçalara dokunur.

    Bu rehberde declarative partitioning'in üç stratejisini (range, list, hash), gerçek bir zaman serisi tablosunun sıfırdan bölümlenmesini, partition pruning'in sorgu planında nasıl doğrulanacağını, üretimde çalışan devasa bir tabloyu kesintiye uğratmadan bölümlemeye geçirmeyi ve aylık partition açıp eskileri düşüren bakım rutinini anlatacağım. Ayrıca birincil anahtar kısıtı, indeks davranışı ve max_locks_per_transaction gibi ilk denemede herkesi ısıran tuzaklara ayrı bir bölüm ayırdım.

    Partitioning Ne Zaman Gerçekten Gerekir#

    Partitioning bir performans sihri değildir; belirli erişim desenlerine karşı çok etkili, geri kalan her şeyde ise ekstra karmaşıklık getiren bir tasarım kararıdır. Tablonuz 5 milyon satırsa ve sorgularınız zaten indeks kullanıyorsa, bölümleme size hemen hemen hiçbir şey kazandırmaz — hatta planlama süresini uzatarak zarar verebilir. Kazancın gerçekten geldiği durum, tablonun boyutunun bellekte tutulamayacak kadar büyümesi ve sorguların doğal olarak belirli bir aralığa yoğunlaşmasıdır.

    Pratikte partitioning'i şu üç sinyalden en az ikisi varsa düşünürüm: tablo tek başına birkaç yüz gigabaytı geçmiştir, sorgular ezici çoğunlukla tek bir sütuna (genelde tarih) göre filtre uygular ve düzenli olarak eski veri toplu halde silinir veya arşivlenir. Özellikle son madde belirleyicidir: bir aylık partition'ı DROP TABLE ile saniyeler içinde yok edersiniz, aynı işi DELETE ile yapmak saatler sürer, WAL üretir, tabloyu şişirir ve ardından VACUUM FULL gerektirir.

    BelirtiPartitioning yardımcı olur muAlternatif
    Tablo > 200 GB, sorgular tarih aralığındaEvet, güçlü kazanç
    Eski veri toplu siliniyor / arşivleniyorEvet, en büyük kazançBölümlenmemiş tabloda DELETE + VACUUM
    Tek satır aramaları yavaşHayırDoğru indeks, EXPLAIN ANALYZE ile analiz
    Yazma throughput'u yetersizKısmenDaha hızlı disk, synchronous_commit, batch insert
    Tablo 10 milyon satır, düzgün indeksliHayırDokunmayın

    Range, List ve Hash Stratejileri#

    PostgreSQL 10 ile gelen declarative partitioning üç bölümleme yöntemi sunar ve hangisini seçeceğiniz tamamen verinizin dağılımına bağlıdır. Range partitioning, bölümleme anahtarını sıralı aralıklara böler ve zaman serisi verisinin standart cevabıdır: her ay ya da her hafta bir partition. List partitioning, anahtarın alabileceği sonlu değerlere göre böler; ülke kodu, kiracı (tenant) kimliği veya kayıt tipi gibi kategorik alanlarda uygundur. Hash partitioning ise anahtarın hash değerini modülo alarak satırları kabaca eşit sayıda parçaya yayar; doğal bir aralık ya da kategori yoksa ama yazma yükünü dağıtmak istiyorsanız işe yarar.

    Bu üç yöntemi iç içe de kullanabilirsiniz. Örneğin dış katmanda aya göre range, her ayın içinde müşteri kimliğine göre hash bölümleme yapmak mümkündür. Ancak alt bölümleme (subpartitioning) partition sayısını çarpar ve planlama maliyetini hızla yükseltir; gerçekten ihtiyacınız olduğundan emin olmadan girmeyin.

    YöntemAnahtar tipiTipik kullanımYeni partition gerekli mi
    RANGESıralanabilir (tarih, sayı)Log, olay, fatura tablolarıEvet, periyodik
    LISTSonlu kategoriBölge, tenant, durum koduYeni kategori gelince
    HASHHerhangi (hash'lenebilir)Yük dağıtımı, doğal aralık yokHayır, sabit modulus

    Range Partition ile Adım Adım Kurulum#

    Sıfırdan bölümlenmiş bir tablo kurmanın mantığı şudur: önce boş bir "ebeveyn" tablo tanımlarsınız, sonra o ebeveynin altına aralıkları belirtilmiş çocuk tablolar bağlarsınız. Ebeveyn tabloda veri fiziksel olarak durmaz; sadece şema ve yönlendirme kuralı bulunur.

    -- 1) Ebeveyn tablo: PARTITION BY ile bölümleme anahtarını ilan ediyoruz
    CREATE TABLE olaylar (
        id          bigint       GENERATED ALWAYS AS IDENTITY,
        musteri_id  bigint       NOT NULL,
        tip         text         NOT NULL,
        payload     jsonb,
        olusturuldu timestamptz  NOT NULL DEFAULT now(),
        -- DİKKAT: birincil anahtar bölümleme sütununu İÇERMEK ZORUNDA
        PRIMARY KEY (id, olusturuldu)
    ) PARTITION BY RANGE (olusturuldu);
    
    -- 2) Aylık partition'lar. Üst sınır DAİMA hariçtir (exclusive).
    CREATE TABLE olaylar_2026_07 PARTITION OF olaylar
        FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
    
    CREATE TABLE olaylar_2026_08 PARTITION OF olaylar
        FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');
    
    -- 3) Hiçbir aralığa uymayan satırlar için güvenlik ağı
    CREATE TABLE olaylar_default PARTITION OF olaylar DEFAULT;
    
    -- 4) İndeks ebeveyne kurulur; PostgreSQL tüm çocuklara otomatik yayar
    CREATE INDEX ON olaylar (musteri_id, olusturuldu DESC);
    

    Üst sınırın hariç olması en sık gözden kaçan detaydır: FROM ('2026-07-01') TO ('2026-08-01') aralığı 31 Temmuz 23:59:59.999999'u kapsar ama 1 Ağustos 00:00:00'ı kapsamaz. Ardışık partition'ları tanımlarken bir ayın üst sınırı ile sonraki ayın alt sınırı aynı değer olmalıdır; arada boşluk bırakırsanız o aralığa düşen satırlar DEFAULT partition'a gider ve fark etmeniz aylar sürebilir.

    DEFAULT partition bir güvenlik ağıdır ama sessiz bir tuzaktır da: içine satır düştüğü anda o aralığı kapsayan yeni bir partition eklemeniz engellenir, çünkü PostgreSQL default'taki satırların yeni partition'la çakışıp çakışmadığını kontrol etmek için tabloyu tarar ve çakışma varsa hata verir. Bu yüzden olaylar_default tablosunu boş olup olmadığı yönünden düzenli izleyin:

    -- Default partition'a satır düştü mü? Sıfırdan farklıysa bir yerde partition eksik demektir.
    SELECT count(*) FROM olaylar_default;
    

    Partition Pruning'i Sorgu Planında Doğrulama#

    Partitioning'in performans kazancı tek bir mekanizmadan gelir: partition pruning. Planlayıcı, WHERE koşuluna bakarak hangi partition'ların sonuca katkı veremeyeceğini anlar ve onlara hiç dokunmaz. Bu davranış varsayılan olarak açıktır (enable_partition_pruning = on) ama sorgunuzu doğru yazmazsanız devreye girmez. Kazancın gerçekleştiğini varsaymak yerine her zaman plana bakın.

    EXPLAIN (ANALYZE, BUFFERS)
    SELECT count(*) FROM olaylar
    WHERE olusturuldu >= '2026-08-01' AND olusturuldu < '2026-09-01';
    

    Çıktının başında şuna benzer bir satır görmelisiniz — dikkat edilecek yer Partitions removed ifadesi ve altta yalnızca tek bir partition'ın taranmasıdır:

    Append  (cost=... rows=...) (actual time=... rows=1 loops=1)
      Subplans Removed: 2
      ->  Parallel Seq Scan on olaylar_2026_08 olaylar_1
    

    Pruning'i sessizce öldüren en yaygın hata, bölümleme sütununu bir fonksiyonla sarmalamaktır. WHERE date_trunc('month', olusturuldu) = '2026-08-01' yazdığınızda planlayıcı ifadeyi partition sınırlarıyla eşleştiremez ve tüm partition'ları tarar. Aynı şekilde WHERE olusturuldu::date = '2026-08-15' de pruning'i bozar. Doğru biçim daima çıplak sütun üzerinde bir aralık karşılaştırmasıdır. Plan okumaya alışkın değilseniz EXPLAIN ANALYZE çıktısını okuma rehberi bu çıktıdaki her satırın ne anlama geldiğini ayrıntısıyla açıklıyor.

    İkinci bir incelik, pruning'in iki farklı zamanda çalışabilmesidir: sabit değerlerle yazılmış sorgularda planlama anında, $1 gibi parametrelerle veya now() ile yazılanlarda ise çalıştırma anında. İkinci duruma runtime pruning denir ve planda Subplans Removed yerine (never executed) işaretleriyle görünür. İkisi de geçerlidir; yeter ki tüm partition'lar gerçekten okunmuyor olsun.

    Üretimdeki Bir Tabloyu Bölümlemeye Geçirmek#

    En zor senaryo, hâlihazırda yüz milyonlarca satır taşıyan ve 7/24 yazılan bir tabloyu bölümlemeye çevirmektir. ALTER TABLE ... PARTITION BY diye bir komut yoktur; mevcut tabloyu yerinde dönüştüremezsiniz. Uygulanabilir yol, yeni bir bölümlenmiş tablo kurup eski tabloyu ona bir partition olarak bağlamak ya da veriyi parça parça taşımaktır.

    Kesintiyi en aza indiren pratik sıra şudur:

    1. Yeni ebeveyn tabloyu olaylar_yeni adıyla, eski tabloyla birebir aynı sütun tipleriyle oluşturun.
    2. Eski tabloya, bağlayacağınız aralığı garanti eden bir CHECK kısıtı ekleyin ve NOT VALID ile başlayıp sonra VALIDATE CONSTRAINT çalıştırın — böylece uzun süreli ACCESS EXCLUSIVE kilidi almazsınız.
    3. Gelecek aylar için boş partition'ları önceden açın.
    4. Kısa bir bakım penceresinde eski tabloyu yeniden adlandırıp ATTACH PARTITION ile ebeveyne bağlayın, sonra olaylar_yeni tablosunu olaylar olarak adlandırın.
    -- Adım 2: kısıtı önce doğrulamadan ekle, sonra ayrı işlemde doğrula
    ALTER TABLE olaylar_eski
        ADD CONSTRAINT olaylar_eski_aralik
        CHECK (olusturuldu >= '2020-01-01' AND olusturuldu < '2026-07-01') NOT VALID;
    
    ALTER TABLE olaylar_eski VALIDATE CONSTRAINT olaylar_eski_aralik;
    
    -- Adım 4: kısıt geçerli olduğu için ATTACH tam tarama YAPMAZ, anlık biter
    ALTER TABLE olaylar ATTACH PARTITION olaylar_eski
        FOR VALUES FROM ('2020-01-01') TO ('2026-07-01');
    

    Buradaki kritik nokta üçüncü satırdır: geçerli ve partition sınırlarını ima eden bir CHECK kısıtı varsa PostgreSQL ATTACH sırasında tabloyu baştan sona taramaz. Kısıtı eklemeyi atlarsanız ATTACH PARTITION yüz milyonlarca satırı okur ve bu süre boyunca tabloyu kilitler — küçük bir hazırlık adımının atlanması, on dakikalık işi iki saatlik kesintiye çevirir. Bu tip geçişleri denerken önce mutlaka bir kopya üzerinde prova yapın; yedekleme hizmetiyle alınan güncel bir yedeğiniz yoksa hiç başlamayın.

    Bakım: Yeni Partition Açma ve Eskiyi Düşürme#

    Range partitioning'in gizli maliyeti bakımdır. Gelecek ayın partition'ı önceden açılmamışsa, ayın ilk saniyesinde gelen INSERT ya DEFAULT partition'a düşer ya da default yoksa doğrudan hata verir. Bu yüzden partition açma işini asla elle yapmayın; bir cron işine ya da pg_partman eklentisine devredin.

    Elle yönetiyorsanız, her ayın başında çalışacak basit bir yaklaşım şudur:

    #!/usr/bin/env bash
    # Gelecek 3 ay için partition'ları oluşturur; zaten varsa sessizce geçer.
    set -euo pipefail
    for i in 1 2 3; do
      BAS=$(date -u -d "+${i} month" +%Y-%m-01)
      SON=$(date -u -d "+$((i+1)) month" +%Y-%m-01)
      AD="olaylar_$(date -u -d "$BAS" +%Y_%m)"
      psql -X -q -d uygulama -c \
        "CREATE TABLE IF NOT EXISTS ${AD} PARTITION OF olaylar
         FOR VALUES FROM ('${BAS}') TO ('${SON}');"
    done
    

    Eski veriyi temizlemek ise partitioning'in en tatlı kısmıdır. 12 aydan eski bir ayı silmek için tek bir komut yeterlidir ve saniyeler sürer:

    -- Arşivlemek istiyorsanız önce ayırın: tablo diskte kalır, ebeveynden kopar
    ALTER TABLE olaylar DETACH PARTITION olaylar_2025_08 CONCURRENTLY;
    
    -- Gerçekten gerekmiyorsa doğrudan düşürün
    DROP TABLE olaylar_2025_08;
    

    DETACH PARTITION CONCURRENTLY, PostgreSQL 14 ile geldi ve ebeveyn tabloyu uzun süre kilitlemeden ayırma imkânı verir; daha eski sürümlerde CONCURRENTLY olmadan çalışır ve kısa bir ağır kilit alır. Ayırdıktan sonra tabloyu pg_dump ile arşivleyip sonra düşürmek, soğuk veriyi ucuz depolamaya taşımanın en temiz yoludur.

    Sık Yapılan Hatalar ve Tuzaklar#

    Birincil anahtar hatası. Bölümlenmiş bir tabloda birincil anahtar veya benzersizlik kısıtı, bölümleme sütununu içermek zorundadır. Yani PRIMARY KEY (id) yazamazsınız, PRIMARY KEY (id, olusturuldu) yazarsınız. Bunun doğal sonucu şudur: id artık global olarak benzersiz garanti edilmez, yalnızca partition içinde benzersizdir. Uygulamanız id üzerinden tekillik varsayıyorsa, bigint GENERATED ALWAYS AS IDENTITY yerine UUID'ye geçmeyi ya da tasarımı yeniden düşünmeyi değerlendirin.

    Çok fazla partition. Her partition ayrı bir tablodur ve sorgu planlaması sırasında kilit alır. Binlerce partition'lı bir tabloda basit bir sorgunun planlanması bile milisaniyeler yerine yüz milisaniyelere çıkabilir ve max_locks_per_transaction varsayılan değerini aşarak "out of shared memory" hatası alırsınız. Pratik kural: partition sayısını birkaç yüzle sınırlayın. Günlük partition ile üç yıllık veri saklamak 1000'den fazla parça demektir; aylık bölümlemeye geçin ya da eski ayları birleştirin.

    Kaçırılan indeksler. PostgreSQL 11 ve sonrasında ebeveyne kurduğunuz indeks tüm çocuklara yayılır, ama bu yayılma yeni partition eklendiğinde de otomatik olur — yeter ki indeksi ebeveyne kurmuş olun. ONLY anahtar kelimesiyle sadece bir çocuğa indeks kurarsanız, o indeks diğer aylarda yoktur ve sorgu bazı aylarda hızlı, bazı aylarda korkunç yavaş çalışır. Böyle "bazen hızlı" davranışlarda ilk bakacağınız yer \d+ olaylar çıktısındaki indeks listesidir.

    Cross-partition güncelleme maliyeti. Bölümleme sütununu UPDATE ile değiştirirseniz PostgreSQL satırı bir partition'dan silip diğerine ekler. Bu desteklenir ama pahalıdır; bölümleme anahtarı olarak değişmeyen bir sütun seçin. Zaman serisi verisinde olusturuldu genelde hiç değişmediği için ideal adaydır.

    Sıkça Sorulan Sorular#

    PostgreSQL partitioning hangi sürümden itibaren kullanılabilir#

    Declarative partitioning PostgreSQL 10 ile geldi, ancak ilk sürümü oldukça sınırlıydı. Hash partitioning ve default partition 11 ile, ciddi pruning iyileştirmeleri ve bölümlenmiş tabloya yabancı anahtar verebilme 12 ile, DETACH PARTITION CONCURRENTLY ise 14 ile eklendi. Pratik tavsiyem yeni bir kurulumda 14 veya üstünü kullanmanızdır; daha eski sürümlerde bakım işlemleri sırasında alacağınız kilitler üretimde canınızı yakar.

    Partitioning sorguları her zaman hızlandırır mı#

    Hayır, ve bu en yaygın yanılgıdır. Partitioning yalnızca sorgu bölümleme anahtarını filtreliyorsa hızlandırır. Anahtarı içermeyen bir sorgu tüm partition'ları taramak zorunda kalır ve ek Append katmanı yüzünden bölümlenmemiş halinden biraz daha yavaş çalışır. Küçük tablolarda ise planlama maliyeti kazancı tamamen yer; 10 milyon satırlık düzgün indekslenmiş bir tabloyu bölmeyin.

    Kaç tane partition oluşturmalıyım#

    Sağlıklı aralık genelde onlarca ile birkaç yüz arasındadır. Her partition planlama sırasında kilit tükettiği için binlerce parça hem planlama süresini uzatır hem max_locks_per_transaction sınırını zorlar. Saklama süreniz ile bölümleme periyodunu çarpıp sonucun 200-300'ü geçmemesini hedefleyin: 5 yıl saklanacak veri için aylık (60 partition) uygundur, günlük (1825 partition) değildir.

    pg_partman kullanmalı mıyım yoksa elle mi yönetmeliyim#

    İki üç partition'lık basit bir yapıda cron ile çalışan on satırlık bir betik fazlasıyla yeterlidir ve bağımlılık eklemez. Ancak partition sayısı arttıkça, saklama politikası (retention) devreye girdikçe ve alt bölümleme kullanmaya başladıkça pg_partman çok iş görür: yeni partition açma, eskiyi düşürme veya arşivleme, şablon tablo üzerinden indeks yayma gibi işleri hazır sunar. Barındırma sağlayıcınızın eklentiye izin verip vermediğini önceden kontrol edin.

    Bölümlenmiş tabloda birincil anahtar neden çalışmıyor#

    Çalışıyor, ama bir koşulu var: birincil anahtar ve benzersizlik kısıtları bölümleme sütununu içermek zorunda. Bunun nedeni PostgreSQL'in global bir indeks tutmaması; her partition'ın kendi yerel indeksi var ve tekilliği ancak partition içinde garanti edebiliyor. Bu yüzden PRIMARY KEY (id) reddedilir, PRIMARY KEY (id, olusturuldu) kabul edilir. Global tekillik gerekiyorsa UUID gibi çakışma olasılığı ihmal edilebilir bir anahtar kullanın.

    Partitioning ile sharding arasındaki fark nedir#

    Partitioning tek bir PostgreSQL sunucusu içinde tabloyu bölmektir; tüm parçalar aynı makinede, aynı işlem (transaction) bağlamında durur. Sharding ise veriyi birden fazla sunucuya dağıtmaktır ve dağıtık işlem, dağıtık sorgu gibi tamamen yeni sorunlar getirir. Önce partitioning'i doğru uygulayın; tek makinenin disk ve CPU'su gerçekten yetmiyorsa Citus gibi bir uzantıyla sharding'i o zaman değerlendirin.

    Kapanış#

    PostgreSQL partitioning'i doğru kullanmanın özeti birkaç alışkanlıkta toplanıyor: bölümleme anahtarını sorgularınızın gerçekten filtrelediği ve hiç değişmeyen bir sütundan seçin, üst sınırın hariç olduğunu unutmayın, gelecek partition'ları otomatik açan bir bakım işi kurun ve kazandığınızı varsaymak yerine her seferinde EXPLAIN çıktısında pruning'i gözünüzle doğrulayın. Bir de partition sayısını disiplinli tutun — bölümleme, sınırsız bir ölçekleme aracı değil, sınırlı ve dengeli bir tasarım tercihidir.

    Bu ölçekte bir veritabanı işletiyorsanız asıl darboğaz çoğu zaman disk ve bellektir. NVMe destekli VDS ya da kaynağı esnek büyütülebilen bulut sunucu paketlerimiz, bölümlenmiş büyük tabloların altında rahat çalışacak IO ve RAM alanı verir. Partition geçişi, bakım penceresi planlaması ve otomatik yedek düzeni gibi işleri kendiniz üstlenmek istemiyorsanız sunucu yönetimi hizmetimiz kurulumdan izlemeye kadar süreci devralır; düzenli veritabanı yedekleri için de yedekleme çözümümüze göz atabilirsiniz.

    PostgreSQLPartitioningPerformans

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.