Büyük Tablolarda Otomatik Partition: pg_partman Kullanımı

Milyonlarca satırdan oluşan bir PostgreSQL tablosunu yönetmeye çalıştıysanız, zamanla işlerin nasıl kötüleştiğini bilirsiniz. Sorgular yavaşlar, VACUUM işlemleri saatlerce sürer, indeksler şişer ve bir gün gelir bakarsınız ki basit bir SELECT COUNT(*) bile dakikalar alıyor. İşte tam bu noktada tablo partitioning devreye giriyor. Ama partitioning’i manuel yönetmek de başlı başına bir iş yükü. Yeni ay geldiğinde partition oluşturmayı unuttunuz mu? Veri taşma başlar. pg_partman bu sorunun çözümü: PostgreSQL için otomatik partition yönetim aracı.

pg_partman Nedir ve Neden Kullanmalısınız?

pg_partman (PostgreSQL Partition Manager), zaman bazlı veya sıra numarası bazlı otomatik partition yönetimi sağlayan bir PostgreSQL extension’ıdır. 2014’ten bu yana aktif geliştirilen bu araç, özellikle büyük log tabloları, time-series verileri ve IoT veri akışları gibi sürekli büyüyen tablolar için hayat kurtarıcıdır.

Manuel partition yönetiminin sorunlarını düşünelim:

  • Her ay yeni partition oluşturmayı hatırlamanız gerekir
  • Eski partition’ları arşivleme veya silme işlemlerini takip etmek zorunda kalırsınız
  • Birden fazla partitioned tablo varsa her birini ayrı ayrı yönetmeniz gerekir
  • Bir şeyleri unuttuğunuzda veriler ana tabloya dolmaya başlar

pg_partman tüm bu süreçleri otomatikleştirir. Yapılandırdıktan sonra maintenance işlemlerini bir cron job veya pg_cron ile scheduler’a bırakıp içiniz rahat edebilirsiniz.

Kurulum

Önkoşullar

pg_partman, PostgreSQL 11 ve üzeri sürümleri desteklemektedir. PostgreSQL 10’da native partitioning desteği sınırlı olduğundan 11+ kullanmanızı öneririm. Örneklerimizde PostgreSQL 15 kullanacağız.

Debian/Ubuntu Üzerinde Kurulum

# PostgreSQL contrib paketleri kurulu değilse önce bunu yapın
sudo apt-get install postgresql-15-partman

# Extension'ı veritabanınıza ekleyin
psql -U postgres -d mydb -c "CREATE SCHEMA partman;"
psql -U postgres -d mydb -c "CREATE EXTENSION pg_partman SCHEMA partman;"

RHEL/CentOS/Rocky Linux Üzerinde Kurulum

# PGDG repo kurulu olduğunu varsayıyoruz
sudo dnf install pg_partman_15

# postgresql.conf'a shared_preload_libraries ekleyin (pg_partman_bgw için)
echo "shared_preload_libraries = 'pg_partman_bgw'" >> /etc/postgresql/15/main/postgresql.conf

# Servis yeniden başlatma
sudo systemctl restart postgresql-15

# Extension kurulumu
psql -U postgres -d mydb << 'EOF'
CREATE SCHEMA partman;
CREATE EXTENSION pg_partman SCHEMA partman;
EOF

postgresql.conf Yapılandırması

Eğer pg_partman’ın kendi background worker’ını kullanmak istiyorsanız, aşağıdaki parametreleri postgresql.conf‘a eklemeniz gerekiyor:

# /etc/postgresql/15/main/postgresql.conf

shared_preload_libraries = 'pg_partman_bgw'

# pg_partman background worker ayarları
pg_partman_bgw.interval = 3600          # Her saat çalışır
pg_partman_bgw.role = 'postgres'        # Hangi kullanıcıyla çalışacak
pg_partman_bgw.dbname = 'mydb'          # Hangi veritabanında çalışacak

Gerçek Dünya Senaryosu: Log Tablosu

Diyelim ki bir e-ticaret uygulamanız var ve kullanıcı aktivite loglarını tutuyorsunuz. Tablo günde 500.000 kayıt alıyor ve 2 yıl sonra 365 milyon satıra ulaşmış durumda. Sorgu performansı çok kötüleşti.

Ana Tabloyu Oluşturmak

Önce partitioned ana tabloyu oluşturalım:

-- Ana (parent) tabloyu oluştur
CREATE TABLE user_activity_log (
    id          BIGSERIAL,
    user_id     BIGINT NOT NULL,
    action      VARCHAR(100) NOT NULL,
    details     JSONB,
    ip_address  INET,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- Ana tabloya indeks ekle
CREATE INDEX ON user_activity_log (user_id, created_at);
CREATE INDEX ON user_activity_log (action, created_at);

Burada dikkat etmeniz gereken bir nokta var: PostgreSQL native partitioning’de primary key, partition key’i de içermek zorundadır. Bu yüzden (id, created_at) şeklinde composite primary key kullandık.

pg_partman ile Partition Yönetimini Başlatmak

-- pg_partman'a bu tabloyu yönetmesini söyle
SELECT partman.create_parent(
    p_parent_table   => 'public.user_activity_log',
    p_control        => 'created_at',
    p_type           => 'native',
    p_interval       => 'monthly',
    p_premake        => 3
);

Parametreleri açıklayalım:

  • p_parent_table: Yönetilecek ana tablonun tam adı (schema.tablename formatında)
  • p_control: Partition key olarak kullanılacak kolon
  • p_type: native (PostgreSQL native partitioning) veya partman (trigger-based, eski yöntem)
  • p_interval: Partition aralığı. daily, weekly, monthly, yearly veya 1 hour gibi interval değerleri alabilir
  • p_premake: Kaç adet önceden partition oluşturulsun. 3 dersek, mevcut ay dahil 3 ay ilerisi için partition hazırlanır

Bu komutu çalıştırdıktan sonra pg_partman otomatik olarak birkaç aylık partition oluşturacak ve partman.part_config tablosuna yapılandırmayı kaydedecektir.

Yapılandırmayı Doğrulamak

-- Oluşturulan partition'ları kontrol et
SELECT
    nmsp_parent.nspname AS parent_schema,
    parent.relname      AS parent_table,
    nmsp_child.nspname  AS child_schema,
    child.relname       AS child_table
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child  ON pg_inherits.inhrelid   = child.oid
JOIN pg_namespace nmsp_parent ON nmsp_parent.oid = parent.relnamespace
JOIN pg_namespace nmsp_child  ON nmsp_child.oid  = child.relnamespace
WHERE parent.relname = 'user_activity_log'
ORDER BY child.relname;

-- pg_partman konfigürasyonunu görüntüle
SELECT * FROM partman.part_config WHERE parent_table = 'public.user_activity_log';

Maintenance İşlemleri

Manuel Maintenance Çalıştırmak

pg_partman’ı yapılandırdıktan sonra yeni partition’ların oluşturulması ve eski olanların temizlenmesi için run_maintenance fonksiyonunu çağırmanız gerekir:

-- Tüm partitioned tablolar için maintenance çalıştır
SELECT partman.run_maintenance();

-- Sadece belirli bir tablo için
SELECT partman.run_maintenance(p_parent_table => 'public.user_activity_log');

-- Detaylı çıktı ile
SELECT partman.run_maintenance(
    p_parent_table  => 'public.user_activity_log',
    p_analyze       => true,
    p_jobmon        => false
);

pg_cron ile Otomatik Maintenance

Background worker yerine pg_cron kullanmayı tercih ediyorum. Daha fazla kontrol sağlıyor ve log takibi daha kolay oluyor:

# Önce pg_cron extension'ını kur (Debian/Ubuntu)
sudo apt-get install postgresql-15-cron

# postgresql.conf'a ekle
echo "shared_preload_libraries = 'pg_cron'" >> /etc/postgresql/15/main/postgresql.conf
echo "cron.database_name = 'mydb'" >> /etc/postgresql/15/main/postgresql.conf

sudo systemctl restart postgresql-15
-- pg_cron extension'ını yükle
CREATE EXTENSION pg_cron;

-- Her gece 02:00'de maintenance çalıştır
SELECT cron.schedule(
    'partman-maintenance',
    '0 2 * * *',
    $$SELECT partman.run_maintenance(p_analyze := false)$$
);

-- Zamanlanan işleri kontrol et
SELECT * FROM cron.job;

Cron Job ile Maintenance (pg_cron Yoksa)

# /etc/cron.d/pg_partman dosyası oluştur
cat > /etc/cron.d/pg_partman << 'EOF'
# pg_partman maintenance - Her gece 02:00
0 2 * * * postgres psql -d mydb -c "SELECT partman.run_maintenance(p_analyze := false);" >> /var/log/pg_partman.log 2>&1
EOF

chmod 644 /etc/cron.d/pg_partman

Retention (Eski Partition Temizliği)

Bu özellik pg_partman’ın en değerli özelliklerinden biri. 12 aydan eski logları otomatik silmek veya arşivlemek isteyebilirsiniz.

-- Retention politikası belirle (12 aydan eski partition'ları sil)
UPDATE partman.part_config
SET
    retention          = '12 months',
    retention_keep_table = false,    -- Tabloyu fiziksel olarak sil
    retention_keep_index = false     -- İndekslerle birlikte sil
WHERE parent_table = 'public.user_activity_log';

-- Eğer tabloyu silmek yerine detach etmek istiyorsanız:
UPDATE partman.part_config
SET
    retention              = '12 months',
    retention_keep_table   = true    -- Tabloyu detach et ama silme
WHERE parent_table = 'public.user_activity_log';

Retention politikası belirlendikten sonra bir sonraki run_maintenance() çağrısında eski partition’lar otomatik olarak işlenecektir.

Mevcut Büyük Tabloyu Partition’a Taşımak

En zorlu senaryo bu: 500 milyon satırlık mevcut tabloyu downtime vermeden partition’a geçirmek.

-- 1. Adım: Yeni partitioned tabloyu oluştur
CREATE TABLE user_activity_log_new (
    id          BIGINT NOT NULL,
    user_id     BIGINT NOT NULL,
    action      VARCHAR(100) NOT NULL,
    details     JSONB,
    ip_address  INET,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);

-- 2. Adım: pg_partman ile yönetimi başlat
SELECT partman.create_parent(
    p_parent_table  => 'public.user_activity_log_new',
    p_control       => 'created_at',
    p_type          => 'native',
    p_interval      => 'monthly',
    p_premake       => 3,
    p_start_partition => '2022-01-01'   -- Verilerinizin başlangıç tarihi
);

Büyük veri taşıma işlemi için pg_partman’ın partition_data_proc fonksiyonunu kullanabilirsiniz. Bu, veriyi küçük batch’ler halinde taşıyarak lock sorunlarını minimize eder:

-- Eski tablodan yeni partitioned tabloya veri kopyala (batch'ler halinde)
-- Bu işlemi background'da veya screen/tmux içinde çalıştırın
DO $$
DECLARE
    v_rows_moved BIGINT := 0;
    v_total      BIGINT := 0;
BEGIN
    LOOP
        INSERT INTO user_activity_log_new
        SELECT * FROM user_activity_log
        WHERE created_at < NOW() - INTERVAL '0 days'
        ORDER BY created_at
        LIMIT 10000
        ON CONFLICT DO NOTHING;

        GET DIAGNOSTICS v_rows_moved = ROW_COUNT;
        v_total := v_total + v_rows_moved;

        RAISE NOTICE 'Toplam taşınan: % satır', v_total;

        EXIT WHEN v_rows_moved < 10000;
        PERFORM pg_sleep(0.1);  -- Sunucuya nefes aldır
    END LOOP;
END;
$$;

Daha pratik bir yol olarak pg_partman’ın kendi partition_data_time fonksiyonunu kullanmak:

-- pg_partman'ın kendi taşıma fonksiyonu
SELECT partman.partition_data_time(
    p_parent_table   => 'public.user_activity_log',
    p_batch_count    => 10,         -- Kaç batch çalıştır
    p_batch_interval => '1 month',  -- Her batch ne kadar veriyi kapsar
    p_lock_wait      => 0           -- Lock için beklenecek saniye
);

Performans İzleme ve Doğrulama

Partition kullanımını ve sorgu planlarını izlemek önemli:

-- Her partition'daki satır sayısını kontrol et
SELECT
    schemaname,
    tablename,
    pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size,
    pg_total_relation_size(schemaname||'.'||tablename) AS size_bytes
FROM pg_tables
WHERE tablename LIKE 'user_activity_log_%'
ORDER BY tablename;

-- Sorgunun partition pruning yapıp yapmadığını kontrol et
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM user_activity_log
WHERE created_at >= '2024-01-01'
  AND created_at <  '2024-02-01'
  AND user_id = 12345;

EXPLAIN çıktısında Partitions pruned: X veya sadece ilgili partition’ın tarandığını görmeniz gerekiyor. Eğer tüm partition’lar taranıyorsa enable_partition_pruning = on ayarını kontrol edin.

-- Partition pruning aktif mi?
SHOW enable_partition_pruning;

-- Kapalıysa aç
SET enable_partition_pruning = on;
-- Veya postgresql.conf'a ekle: enable_partition_pruning = on

Sık Karşılaşılan Sorunlar ve Çözümleri

Default Partition Sorunu

Veriler mevcut partition aralığının dışına düşerse PostgreSQL bunları “default” partition’a atar. pg_partman default partition yönetimini de destekler:

-- Default partition'ı yönetimine al
UPDATE partman.part_config
SET default_table = true
WHERE parent_table = 'public.user_activity_log';

Partition’lar Oluşturulmuyorsa

-- part_config tablosunu kontrol et
SELECT
    parent_table,
    control,
    partition_interval,
    premake,
    automatic_maintenance,
    last_run
FROM partman.part_config
WHERE parent_table = 'public.user_activity_log';

-- automatic_maintenance 'off' ise açık
UPDATE partman.part_config
SET automatic_maintenance = 'on'
WHERE parent_table = 'public.user_activity_log';

-- Hata loglarını kontrol et
SELECT * FROM partman.part_config_sub
WHERE sub_parent = 'public.user_activity_log';

Yabancı Anahtar (Foreign Key) Sorunları

Native partitioning’de foreign key’ler partition’dan parent’a değil parent’tan başka tablolara tanımlanabilir. Eğer başka bir tablo user_activity_log‘a FK ile bağlıysa sorun yaşarsınız. Bu durumda ya mimarıyı yeniden düşünün ya da trigger-based yaklaşıma geçin.

İleri Seviye: Sub-Partitioning

Çok yoğun tablolarda aylık partition’lar bile çok büyük olabilir. Bu durumda sub-partitioning yapabilirsiniz; örneğin önce aya göre, sonra user_id hash’ine göre:

-- Sub-partition yapılandırması
INSERT INTO partman.part_config_sub (
    sub_parent,
    sub_control,
    sub_partition_type,
    sub_partition_interval,
    sub_premake
) VALUES (
    'public.user_activity_log',
    'user_id',
    'native',
    '10',      -- Her 10 user_id'de bir partition (hash için)
    0
);

Ancak sub-partitioning karmaşıklığı ciddi ölçüde artırır. Gerçekten ihtiyaç duymadan önce aylık partitioning’in yetip yetmediğini test edin.

Monitoring Dashboard Sorgusu

Sysadmin olarak sisteminizi takip etmek için düzenli çalıştırabileceğiniz özet sorgu:

-- Partition sağlık raporu
WITH partition_info AS (
    SELECT
        pc.parent_table,
        pc.partition_interval,
        pc.premake,
        pc.retention,
        pc.automatic_maintenance,
        COUNT(pi.inhrelid) AS partition_count
    FROM partman.part_config pc
    LEFT JOIN pg_class p ON p.oid = (
        SELECT oid FROM pg_class
        WHERE relname = split_part(pc.parent_table, '.', 2)
    )
    LEFT JOIN pg_inherits pi ON pi.inhparent = p.oid
    GROUP BY 1,2,3,4,5
)
SELECT
    parent_table         AS "Ana Tablo",
    partition_interval   AS "Aralık",
    premake              AS "Ön Oluşturma",
    retention            AS "Saklama Süresi",
    automatic_maintenance AS "Otomatik",
    partition_count      AS "Toplam Partition"
FROM partition_info
ORDER BY parent_table;

Sonuç

pg_partman, büyüyen PostgreSQL tablolarını yönetmek için olmazsa olmaz bir araç. Manuel partition yönetiminin getirdiği operasyonel yükü neredeyse sıfıra indiriyor ve eski verilerin temizlenmesini otomatikleştiriyor. Özellikle şu senaryolarda mutlaka değerlendirin:

  • Log, event veya audit tabloları 100 milyon satırı geçtiğinde
  • Zaman bazlı sorgu performansı kötüleşmeye başladığında
  • Eski verileri düzenli olarak arşivlemeniz veya silmeniz gerektiğinde
  • VACUUM ve ANALYZE işlemleri çok uzun sürdüğünde

Kurulumu ve ilk yapılandırması biraz zaman alsa da bir kez doğru kurulduktan sonra pg_partman arka planda sessizce çalışır ve sizi büyük baş ağrılarından kurtarır. Benim tavsiyem: önce test ortamında deneyin, bir veya iki ay production’ı izleyin, sonra kritik tablolarınıza uygulayın. Partition boyutlarını ve sorgu planlarını düzenli olarak kontrol edin, run_maintenance() loglarını bir yere yazın ve her şeyin sorunsuz gittiğini teyit edin. Veritabanı yönetiminde otomasyona güvenmek güzel, ama kör güvenmemek daha güzel.

Yorum yapın