← blog · 3 Kasım 2026

PostgreSQL'de Tablo Bölümleme: Ne Zaman Gerekir, Parça Eleme, Eski Veriyi Atmak ve Kilit Tuzakları

Bölümleme büyük tabloyu hızlandırmak için değil, zamanla biriken veriyi ucuza atmak için vardır. Aralık bölümlemesini kurmak, parçaları kilitsiz açıp ayırmak, benzersiz kısıt ve varsayılan parça tuzakları ve bölümlemenin zarar verdiği durumlar.

Bir tablo yüz milyonlarca satıra ulaştığında sorun çoğu zaman sorgu hızı değil, tablonun bakımıdır. Eski veriyi silmek saatler süren bir DELETE olur, ardından autovacuum o kadar ölü satırı temizlemekle uğraşır, indeksler şişer, tek bir VACUUM ya da indeks yeniden oluşturma bütün tabloyu kapsadığı için her bakım işi büyür. PostgreSQL'in bildirimsel bölümlemesi (declarative partitioning) bu tabloyu tek bir mantıksal tablo olarak bırakıp fiziksel olarak parçalara ayırır. Doğru kurulduğunda eski veriyi silmek bir DROP TABLE kadar ucuzlar. Yanlış kurulduğunda ise sorgular yavaşlar, ekleme işlemleri hata verir ve kilitler beklenmedik anlarda trafiği durdurur.

Bölümleme ne zaman gerçekten işe yarar

Bölümlemeyi seçmenin en sağlam gerekçesi veri yaşam döngüsüdür: olay kayıtları, denetim izleri, metrikler, sipariş geçmişi gibi zamana göre biriken ve belli bir süre sonra silinen ya da arşivlenen veri. Burada kazanç nettir; ayı dolmuş bir parçayı ayırıp silmek ne ölü satır üretir ne de vacuum yükü.

İkinci gerekçe, sorguların neredeyse hep bölüm anahtarına göre filtrelenmesidir. "Son yedi günün kayıtları" gibi sorgularda planlayıcı ilgisiz parçaları hiç okumaz (partition pruning).

Bu iki koşul yoksa önce başka araçlara bakın:

  • Doğru indeks ve toplu silme. Yalnızca sorgu hızı derdiniz varsa iyi seçilmiş bir bileşik indeks çoğu zaman bölümlemeden daha az karmaşıklıkla aynı sonucu verir. Silmeyi küçük partilerle yapmak da çoğu orta büyüklükteki tablo için yeter.
  • BRIN indeksi. Satırları zaman sırasına göre eklenen, neredeyse hiç güncellenmeyen tablolarda BRIN indeksi çok küçük yer kaplar ve aralık sorgularında ciddi kazanç sağlar.

Taraf tutmak gerekirse: tablo zamanla büyüyor ve eski veriyi düzenli olarak atıyorsanız aralık (range) bölümlemesi kurun. Sadece "tablo büyük, belki hızlanır" diye bölümleme yapmayın; bölüm anahtarını kullanmayan sorgular bölümlemeden sonra daha yavaş çalışır, çünkü planlayıcı her parçaya ayrı ayrı bakmak zorunda kalır.

Kiracıya göre hash bölümlemesi de sık önerilir ama nadiren karşılığını verir. Kiracı silmek yine satır satır silmektir, büyük kiracılar parçalar arasında dengesizlik yaratır. Kiracı izolasyonu gerekiyorsa bu bir bölümleme kararı değil, mimari karardır.

Temel kurulum

Aralık bölümlemesinde ana tablo veri tutmaz, yalnızca yapıyı ve kuralı tanımlar:

CREATE TABLE olaylar (
    id          bigserial,
    olusturma   timestamptz NOT NULL,
    kiraci_id   bigint      NOT NULL,
    veri        jsonb,
    PRIMARY KEY (id, olusturma)
) PARTITION BY RANGE (olusturma);

CREATE TABLE olaylar_2026_11 PARTITION OF olaylar
    FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');

CREATE TABLE olaylar_2026_12 PARTITION OF olaylar
    FOR VALUES FROM ('2026-12-01') TO ('2027-01-01');

Aralığın alt sınırı dahil, üst sınırı hariçtir; bu yüzden ardışık parçalar arasında boşluk ya da çakışma kalmaz.

Birincil anahtarın (id, olusturma) olduğuna dikkat edin. Bu bir tercih değil, zorunluluktur; aşağıda tuzaklar bölümünde nedenini anlatıyoruz.

Planlayıcının parçaları gerçekten elediğini doğrulamak için:

EXPLAIN SELECT count(*) FROM olaylar
WHERE olusturma >= '2026-11-10' AND olusturma < '2026-11-17';

Planda yalnızca olaylar_2026_11 görünmelidir. Parametreli sorgularda eleme çalışma anında yapılır; EXPLAIN ANALYZE çıktısında bunu "Subplans Removed" satırı olarak görürsünüz.

Eski veriyi atmak

Bölümlemenin asıl ödülü burada:

ALTER TABLE olaylar DETACH PARTITION olaylar_2025_10 CONCURRENTLY;
DROP TABLE olaylar_2025_10;

CONCURRENTLY seçeneği PostgreSQL 14 ile geldi ve ana tabloda ağır kilit tutmadan parçayı ayırır. İki kısıtı var: bir işlem bloğu içinde çalışmaz ve ana tabloda varsayılan (default) parça varken kullanılamaz. Ayrılan parçayı silmeden önce bir arşive dökmek isterseniz, ayırdıktan sonra sıradan bir tablo gibi pg_dump ile alabilirsiniz.

Yeni parçaları önceden ve güvenli açmak

CREATE TABLE ... PARTITION OF ana tabloda ATTACH PARTITION'dan daha ağır bir kilit alır. Yoğun trafik altında bu komut, ana tabloya erişen uzun bir sorgunun bitmesini bekler; beklerken arkasına gelen her sorgu da kuyruğa girer. Bu yüzden iki şey yapın: parçaları ihtiyaçtan haftalar önce açın ve her DDL'in önüne bir lock_timeout koyun.

Dolu ya da trafik altındaki bir tabloda daha az kilitli yol, parçayı ayrı oluşturup sonra bağlamaktır:

SET lock_timeout = '3s';

CREATE TABLE olaylar_2027_01
    (LIKE olaylar INCLUDING DEFAULTS INCLUDING CONSTRAINTS);

ALTER TABLE olaylar_2027_01 ADD CONSTRAINT olaylar_2027_01_aralik
    CHECK (olusturma >= '2027-01-01' AND olusturma < '2027-02-01');

ALTER TABLE olaylar ATTACH PARTITION olaylar_2027_01
    FOR VALUES FROM ('2027-01-01') TO ('2027-02-01');

ALTER TABLE olaylar_2027_01 DROP CONSTRAINT olaylar_2027_01_aralik;

Parça sınırıyla birebir aynı CHECK kısıtı önceden varsa PostgreSQL bağlarken tabloyu taramaz. Kısıt yoksa bağlama sırasında bütün parça okunur.

Parça açmayı elle yapmayın. Bir zamanlanmış iş, örneğin pg_partman eklentisi ya da kendi yazdığınız küçük bir betik, ileriye dönük birkaç parçayı hazır tutmalıdır. Hiçbir parçaya uymayan bir satır eklendiğinde PostgreSQL no partition of relation "olaylar" found for row hatası verir ve uygulama ayın ilk dakikasında yazamaz hale gelir.

Tuzaklar

Benzersiz kısıt bölüm anahtarını içermek zorunda. PostgreSQL'de bölümlenmiş tabloda küresel bir benzersiz indeks yoktur; her parçanın indeksi yalnızca kendi satırlarını bilir. Bu yüzden birincil anahtar ve her UNIQUE kısıtı bölüm anahtarı sütunlarını içermelidir. Sonuç: id tek başına tablo genelinde benzersiz garanti edilmez. Kimliği bir diziden ya da UUID'den üretiyorsanız pratikte sorun çıkmaz, ama "e-posta benzersiz olsun" gibi bir iş kuralını bölümlenmiş tabloda kısıtla uygulayamazsınız. Bu tabloya başvuran yabancı anahtarların da bölüm anahtarını taşıması gerekir, çünkü başvurulan sütunlar benzersiz olmalıdır.

Bölüm anahtarına uygulanan ifade elemeyi bozar. WHERE olusturma::date = '2026-11-10' ya da WHERE date_trunc('day', olusturma) = ... gibi koşullarda planlayıcı hangi parçaya bakacağını çıkaramaz ve hepsini tarar. Koşulu her zaman sütunun kendisi üzerinde aralık olarak yazın.

Varsayılan parça sessiz bir yük biriktirir. "Ne olur ne olmaz" diye açılan varsayılan parça, parça açma işi aksadığında satırları kabul eder ve hata yerine sessizlik üretir. Sonra o aralık için gerçek parçayı açmak istediğinizde PostgreSQL varsayılan parçayı tarayıp çakışan satır olup olmadığına bakar, çakışma varsa işlem reddedilir. Ayrıca varsayılan parça varken DETACH ... CONCURRENTLY kullanılamaz. Varsayılan parçayı yalnızca izlediğiniz bir güvenlik ağı olarak tutun ya da hiç açmayın; hata almak, verinin yanlış yere birikmesinden iyidir.

Çok fazla parça planlamayı yavaşlatır. Her sorgu, planlama ve kilitleme sırasında ilgili parçalarla uğraşır. Günlük parçalarla yıllarca veri tutmak binlerce parça demektir ve eleme olmayan her sorgu bunun bedelini öder. Parça boyutunu silme sıklığınıza göre seçin: veriyi aylık atıyorsanız aylık parça yeterlidir.

İndeksleri eşzamanlı oluşturmak dolaylı yol ister. Bölümlenmiş ana tabloda CREATE INDEX CONCURRENTLY çalışmaz. Yol şu:

CREATE INDEX olaylar_kiraci_idx ON ONLY olaylar (kiraci_id, olusturma);

CREATE INDEX CONCURRENTLY olaylar_2026_11_kiraci_idx
    ON olaylar_2026_11 (kiraci_id, olusturma);

ALTER INDEX olaylar_kiraci_idx ATTACH PARTITION olaylar_2026_11_kiraci_idx;

Her parça için son iki adımı tekrarlayın. Bütün parçalar bağlandığında ana indeks geçerli hale gelir.

Autovacuum ana tablonun istatistiğini toplamaz. Parçalar kendi istatistiklerini alır, ama bölümlenmiş ana tablonun toplu istatistiği autovacuum tarafından güncellenmez. Birden çok parçayı kapsayan sorgularda planlayıcı yanlış tahmin yapabilir. Parça açma işinin sonuna ana tablo için düzenli bir ANALYZE olaylar; ekleyin.

Mevcut bir tabloyu bölümlenmiş tabloya dönüştürmek yerinde yapılamaz. Sıradan bir tabloyu ALTER TABLE ile bölümlenmiş tabloya çeviremezsiniz. Yaygın yol, yeni bölümlenmiş tabloyu açmak, eski tabloyu tek bir büyük parça olarak bağlamak (yukarıdaki CHECK hilesiyle taramadan) ve yeni veriyi yeni parçalara akıtmaktır. Eski parça saklama süresi dolduğunda kendiliğinden silinir.

Ne zaman seçmemeli

Tablonuz birkaç on milyon satırın altındaysa, sorgularınız bölüm anahtarını kullanmıyorsa ya da veriyi hiç silmiyorsanız bölümleme size karmaşıklık dışında pek bir şey getirmez. Benzersizlik kurallarınız bölüm anahtarı dışındaki sütunlara dayanıyorsa bölümleme bu kuralları uygulamadan kaldırır. Böyle durumlarda iyi bir indeks, BRIN ve küçük partilerle silme daha doğru araçlardır. Bölümleme, veriyi zamana göre doğup ölen tablolar için bir bakım aracıdır; ilk ona öyle bakın.