MariaDB ve MySQL’de Partitioning ile Büyük Tabloları Bölümleme

Milyonlarca satır içeren bir tabloya sorgu attığında dakikalarca bekliyorsan, indeksler artık sana yetmiyordur. İşte tam bu noktada tablo bölümleme (partitioning) devreye giriyor. MariaDB ve MySQL’de partitioning, büyük tabloları mantıksal ya da fiziksel parçalara bölerek sorgu performansını dramatik biçimde artıran güçlü bir özelliktir. Özellikle log tabloları, sipariş geçmişleri, zaman serisi verileri gibi sürekli büyüyen yapılarda bu teknik hayat kurtarır.

Bu yazıda gerçek dünya senaryoları üzerinden partitioning kavramını derinlemesine ele alacağız. Sadece teorik bilgi değil, production ortamında kullanabileceğin hazır SQL örnekleri de bulacaksın.

Partitioning Nedir ve Neden Kullanılır?

Partitioning, tek bir tablonun verilerini belirli kurallara göre ayrı fiziksel ya da mantıksal bölümlere (partition) dağıtma işlemidir. Dışarıdan baktığında hala tek bir tablo gibi görünür; ama arka planda veri birden fazla segmentte saklanır.

Avantajları nelerdir?

  • Partition Pruning: Sorgu yalnızca ilgili partition üzerinde çalışır, gereksiz veri taranmaz
  • Bakım Kolaylığı: Eski veriyi silmek için DELETE yerine tek bir partition’ı drop edebilirsin
  • Paralel I/O: Farklı partition’lar farklı disk bölgelerine yazılabilir
  • Arşivleme: Eski partition’ları kolayca ayırabilir veya farklı tablespace’e taşıyabilirsin
  • Büyük Tablo Yönetimi: Tek seferde milyarlarca satırı yönetmek yerine parçalar halinde çalışırsın

Dezavantajları da var:

  • Partition key olmayan sütunlarda foreign key tanımlanamaz
  • Bazı yapısal değişiklikler (ALTER TABLE) daha uzun sürebilir
  • Yanlış partition stratejisi seçilirse performans kazanımı değil kaybı yaşarsın

Partitioning Türleri

MariaDB ve MySQL’de dört temel partitioning türü vardır.

RANGE Partitioning

En sık kullanılan türdür. Belirlediğin bir sütunun değer aralığına göre veri dağıtılır. Tarih bazlı tablolar için biçilmiş kaftandır.

LIST Partitioning

RANGE’e benzer ama değer aralığı yerine belirli değer listeleri kullanılır. Ülke kodu, bölge ID gibi kategorik veriler için uygundur.

HASH Partitioning

MySQL/MariaDB, belirtilen sütunun hash değerini hesaplar ve veriyi eşit dağıtmaya çalışır. Öngörülemeyen dağılımlarda kullanışlıdır.

KEY Partitioning

HASH’e benzer ama MySQL’in kendi dahili hash fonksiyonunu kullanır. Primary key veya unique key üzerinden çalışır.

Gerçek Senaryo 1: E-Ticaret Sipariş Tablosu

Diyelim ki bir e-ticaret platformu yönetiyorsun. orders tablosunda 3 yıl içinde 50 milyon satır birikmiş. Her yıl sonu muhasebe raporları çekilirken sistem yavaşlıyor, disk I/O uçuyor.

Çözüm: Yıl bazlı RANGE partitioning.

CREATE TABLE orders (
    order_id    BIGINT NOT NULL AUTO_INCREMENT,
    user_id     INT NOT NULL,
    total_amount DECIMAL(10,2) NOT NULL,
    status      VARCHAR(20) NOT NULL,
    created_at  DATETIME NOT NULL,
    PRIMARY KEY (order_id, created_at)
)
ENGINE = InnoDB
PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2021 VALUES LESS THAN (2022),
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

Dikkat etmen gereken önemli bir nokta var: Partition key, primary key’in bir parçası olmak zorunda. Bu yüzden PRIMARY KEY (order_id, created_at) şeklinde composite key kullandık.

Şimdi 2022 yılına ait siparişleri çektiğinde MySQL sadece p2022 partition’ını tarar:

-- Partition pruning devrede mi? EXPLAIN ile kontrol et
EXPLAIN SELECT * FROM orders
WHERE created_at BETWEEN '2022-01-01' AND '2022-12-31'
AND status = 'completed';

EXPLAIN çıktısında partitions kolonunda p2022 yazıyorsa pruning çalışıyor demektir. Eğer tüm partition’lar listeleniyorsa, WHERE koşulun partition key’i doğru kullanmıyor olabilir.

Gerçek Senaryo 2: Log Tablosu ve Otomatik Temizlik

Uygulama loglarını veritabanında saklayan bir sistem düşün. Aylık 5-10 milyon satır ekleniyor ve 6 aydan eski loglar silinmesi gerekiyor. Klasik DELETE WHERE created_at < ... yaklaşımı binlerce satırı teker teker siler, transaction log şişer, tabloya kilit koyar.

Partition ile bu işlem saniyeler içinde tamamlanır:

CREATE TABLE app_logs (
    log_id      BIGINT NOT NULL AUTO_INCREMENT,
    app_name    VARCHAR(100) NOT NULL,
    log_level   ENUM('DEBUG','INFO','WARN','ERROR') NOT NULL,
    message     TEXT,
    created_at  DATETIME NOT NULL,
    PRIMARY KEY (log_id, created_at),
    INDEX idx_app_level (app_name, log_level)
)
ENGINE = InnoDB
PARTITION BY RANGE (TO_DAYS(created_at)) (
    PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
    PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
    PARTITION p202403 VALUES LESS THAN (TO_DAYS('2024-04-01')),
    PARTITION p202404 VALUES LESS THAN (TO_DAYS('2024-05-01')),
    PARTITION p202405 VALUES LESS THAN (TO_DAYS('2024-06-01')),
    PARTITION p202406 VALUES LESS THAN (TO_DAYS('2024-07-01')),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

6 aylık log rotasyonu için eski partition’ı silmek ve yeni ay için partition eklemek:

-- Eski partition'ı sil (saniyeler içinde biter, tablo kilitlenmez)
ALTER TABLE app_logs DROP PARTITION p202401;

-- Yeni ay için partition ekle (p_future'dan önce eklenmeli)
ALTER TABLE app_logs REORGANIZE PARTITION p_future INTO (
    PARTITION p202407 VALUES LESS THAN (TO_DAYS('2024-08-01')),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

Bu yaklaşım production ortamında gerçekten büyük fark yaratır. DROP PARTITION işlemi metadata değişikliği gibi çalışır; milyonlarca satırı tek tek silmek yerine dosya seviyesinde temizlik yapar.

Gerçek Senaryo 3: Bölgesel Veri ile LIST Partitioning

Çok bölgeli bir SaaS uygulaması yönetiyorsun. Müşteri verileri bölgeye göre ayrılmalı; hem veri lokalizasyonu hem de sorgu performansı açısından önemli.

CREATE TABLE customers (
    customer_id  BIGINT NOT NULL AUTO_INCREMENT,
    company_name VARCHAR(200) NOT NULL,
    region_code  TINYINT NOT NULL COMMENT '1=Marmara, 2=Ege, 3=Akdeniz, 4=Ic Anadolu, 5=Karadeniz, 6=Dogu, 7=Guneydogu',
    email        VARCHAR(150) NOT NULL,
    created_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (customer_id, region_code)
)
ENGINE = InnoDB
PARTITION BY LIST (region_code) (
    PARTITION p_marmara    VALUES IN (1),
    PARTITION p_ege        VALUES IN (2),
    PARTITION p_akdeniz    VALUES IN (3),
    PARTITION p_ic_anadolu VALUES IN (4),
    PARTITION p_karadeniz  VALUES IN (5),
    PARTITION p_dogu       VALUES IN (6),
    PARTITION p_guneydogu  VALUES IN (7)
);

Marmara bölgesi satış raporu çekilirken diğer bölge verileri hiç taranmaz:

-- Sadece p_marmara partition'i taranacak
SELECT
    customer_id,
    company_name,
    email
FROM customers
WHERE region_code = 1
ORDER BY company_name;

Mevcut Tabloyu Partition’a Dönüştürmek

Production’da çalışan büyük bir tabloyu partition’a almak bazen kaçınılmaz olur. Bu işlem dikkatli yapılmalıdır çünkü ALTER TABLE ... PARTITION BY komutu tabloyu kilitler ve büyük tablolarda saatler alabilir.

Önerilen yaklaşım: Yeni tabloyu oluştur, veriyi kopyala, tabloları değiştir.

-- 1. Adim: Yeni partition'li tabloyu olustur
CREATE TABLE orders_partitioned LIKE orders;

ALTER TABLE orders_partitioned
PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2021 VALUES LESS THAN (2022),
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 2. Adim: Veriyi toplu kopyala (batch ile yap, sistemi bogma)
INSERT INTO orders_partitioned
SELECT * FROM orders
WHERE created_at < '2024-01-01';

-- 3. Adim: Tabloları rename ile cevir (atomik islem)
RENAME TABLE orders TO orders_old,
             orders_partitioned TO orders;

-- 4. Adim: Eski tabloyu kontrol ettikten sonra sil
-- DROP TABLE orders_old;

Büyük tablolarda 2. adımı pt-online-schema-change (Percona Toolkit) ile yapmak çok daha güvenlidir. Servis kesintisiz devam eder.

Partition Yönetimi: Günlük Operasyonlar

Mevcut Partition Bilgilerini Görüntüleme

-- Hangi partition'larda kac satir var?
SELECT
    PARTITION_NAME,
    TABLE_ROWS,
    AVG_ROW_LENGTH,
    ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS size_mb
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = 'mydb'
  AND TABLE_NAME = 'orders'
ORDER BY PARTITION_ORDINAL_POSITION;

Bu sorgu her partition’ın boyutunu ve satır sayısını gösterir. Eğer p_future partition’ı sürekli şişiyorsa yeni partition ekleme zamanı gelmiş demektir.

Partition’ları Birleştirme ve Bölme

Bazen partition planı değişmek zorunda kalır. REORGANIZE komutu hem bölme hem birleştirme için kullanılır:

-- Iki partition'i birlestir (ornek: 2021 ve 2022'yi tek partition yap)
ALTER TABLE orders
REORGANIZE PARTITION p2021, p2022 INTO (
    PARTITION p2021_2022 VALUES LESS THAN (2023)
);

-- Bir partition'i ikiye bol
ALTER TABLE orders
REORGANIZE PARTITION p_future INTO (
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

Partition’ı Boşaltmak

Veriyi silmeden partition’ı boşaltmak istersen TRUNCATE kullan:

-- Sadece p2021 partition'ini bosalt, diger veriler dokunulmasin
ALTER TABLE orders TRUNCATE PARTITION p2021;

HASH Partitioning: Dengeli Dağılım İçin

RANGE veya LIST için uygun bir kolon yoksa ama yine de veriyi böllmek istiyorsan HASH partitioning işe yarar. User tablosunu 4 eşit parçaya bölelim:

CREATE TABLE user_activity (
    activity_id  BIGINT NOT NULL AUTO_INCREMENT,
    user_id      INT NOT NULL,
    activity_type VARCHAR(50) NOT NULL,
    activity_data JSON,
    created_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (activity_id, user_id)
)
ENGINE = InnoDB
PARTITION BY HASH (user_id)
PARTITIONS 4;

HASH partitioning’de pruning daha sınırlıdır. WHERE user_id = 12345 gibi tam eşleşme sorgularında pruning çalışır ama aralık sorgularında tüm partition’lar taranır. Bu nedenle HASH, yüksek yazma yükü olan ve user_id bazlı nokta sorgularının yoğun olduğu senaryolara daha uygundur.

Subpartitioning: İkinci Seviye Bölümleme

Çok büyük sistemlerde hem zaman hem de kullanıcı bazlı bölümleme gerekebilir. MariaDB bunu RANGE+HASH kombinasyonu ile destekler:

CREATE TABLE transactions (
    txn_id       BIGINT NOT NULL,
    user_id      INT NOT NULL,
    amount       DECIMAL(12,2) NOT NULL,
    txn_date     DATE NOT NULL,
    PRIMARY KEY (txn_id, user_id, txn_date)
)
ENGINE = InnoDB
PARTITION BY RANGE (YEAR(txn_date))
SUBPARTITION BY HASH (user_id)
SUBPARTITIONS 4 (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026)
);

Bu yapıda 3 RANGE partition ve her birinde 4 HASH subpartition olmak üzere toplam 12 fiziksel segment oluşur. Ciddi ölçekte veri varsa faydalıdır, yoksa gereksiz karmaşıklık yaratır.

Partition Pruning’i Engelleyen Hatalar

Partition kullanıyorsun ama beklediğin performansı göremiyorsun? Büyük ihtimalle pruning çalışmıyordur. İşte en sık yapılan hatalar:

Fonksiyon veya tip uyumsuzluğu: Partition key olarak YEAR(created_at) kullandıysan WHERE koşulunda da aynı ifadeyi kullanmalısın. WHERE created_at BETWEEN '2023-01-01' AND '2023-12-31' şeklinde yazarsan MySQL içsel olarak bunu YEAR bazlı pruning’e çevirebilir; ama WHERE DATE(created_at) = '2023-06-15' yazarsan pruning çalışmaz.

Implicit type conversion: VARCHAR olarak tanımlanmış partition key’e integer değer geçmek, otomatik dönüşüm yüzünden pruning’i kırar.

OR koşulları: WHERE region_code = 1 OR region_code = 3 yerine WHERE region_code IN (1, 3) kullan. IN ile pruning çalışır, OR ile her zaman çalışmayabilir.

Subquery içinde partition key: WHERE user_id = (SELECT user_id FROM ...) gibi subquery’lerde MySQL sorgu optimizasyonunu tamamlamadan pruning kararı veremez ve tüm partition’ları tarayabilir.

Otomatik Partition Yönetimi: Event Scheduler ile

Log rotasyonunu elle yapmak yerine MySQL Event Scheduler ile otomatik hale getir:

-- Event Scheduler'i aktif et
SET GLOBAL event_scheduler = ON;

-- Her ayin 1'inde calisacak event
DELIMITER $$

CREATE EVENT auto_partition_maintenance
ON SCHEDULE EVERY 1 MONTH
STARTS '2024-02-01 02:00:00'
DO
BEGIN
    -- Alti aydan eski partition'i sil
    -- Adi dinamik olarak hesapla
    SET @drop_partition = CONCAT(
        'ALTER TABLE app_logs DROP PARTITION p',
        DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 6 MONTH), '%Y%m')
    );
    PREPARE stmt FROM @drop_partition;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;

    -- Gelecek ay icin yeni partition ekle
    SET @add_partition = CONCAT(
        'ALTER TABLE app_logs REORGANIZE PARTITION p_future INTO (',
        'PARTITION p', DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 MONTH), '%Y%m'),
        ' VALUES LESS THAN (TO_DAYS(''',
        DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 2 MONTH), '%Y-%m-01'),
        ''')), PARTITION p_future VALUES LESS THAN MAXVALUE)'
    );
    PREPARE stmt2 FROM @add_partition;
    EXECUTE stmt2;
    DEALLOCATE PREPARE stmt2;
END$$

DELIMITER ;

Bu event her ay çalışarak 6 aydan eski partition’ı otomatik siler ve gelecek ay için yeni partition oluşturur. Production’a almadan önce mutlaka test ortamında dene ve log kaydı ekle.

Partitioning ile İndeks Stratejisi

Partition kullanıyorsan indeks stratejini de buna göre ayarlamalısın.

Local vs Global Index: MySQL ve MariaDB’de tüm indeksler local’dir; yani her partition kendi indeks yapısını tutar. Bu, UNIQUE indeksler için kısıtlama getirir: UNIQUE indeks, partition key sütununu içermek zorundadır.

-- Bu CALISMAZ: unique_email partition key icermiyor
ALTER TABLE customers ADD UNIQUE KEY (email);

-- Bu CALISIR: email + partition_key kombinasyonu
ALTER TABLE customers ADD UNIQUE KEY (email, region_code);

Eğer partition key olmadan gerçek global unique constraint istiyorsan, uygulama katmanında kontrol etmek ya da ayrı bir lookup tablosu tutmak en pratik çözümdür.

Performance Monitoring: Partition Sorgularını İzleme

Partition’ların nasıl kullanıldığını izlemek için Performance Schema’dan faydalanabilirsin:

-- Hangi partition'lar en cok okunan/yazilan?
SELECT
    OBJECT_SCHEMA,
    OBJECT_NAME,
    PARTITION_NAME,
    COUNT_READ,
    COUNT_WRITE,
    COUNT_FETCH,
    SUM_TIMER_READ / 1e12 AS read_seconds
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'mydb'
  AND OBJECT_NAME = 'orders'
  AND PARTITION_NAME IS NOT NULL
ORDER BY COUNT_READ DESC;

Bu sorgu hangi partition’ların sık erişildiğini, hangilerinin neredeyse hiç kullanılmadığını gösterir. Sürekli p_future üzerinde yoğun erişim varsa yeni partition ekleme zamanın gelmiş demektir.

Sonuç

Partitioning, doğru kullanıldığında büyük veri tablolarını yönetmenin en etkili yollarından biridir. Aylık log temizliklerini saniyeler içinde tamamlamak, yıllık raporları dakikalar yerine saniyelerde almak, disk I/O’yu dengeli dağıtmak… bunların hepsi iyi tasarlanmış bir partition stratejisiyle mümkün.

Ancak partitioning bir sihirli değnek değil. Partition key seçimi her şeydir; yanlış seçilmiş bir partition key hem performans kazandırmaz hem de bakım yükü oluşturur. Yeni bir tablo tasarlarken şu soruları sor kendinize: Bu tablo ne kadar büyüyecek? En sık hangi sütuna göre sorgulanacak? Eski veri ne sıklıkla silinecek? Bu sorulara verilen cevaplar seni doğru partition stratejisine götürür.

Mevcut production tablolarını partition’a taşırken mutlaka staging ortamında test et, işlem sürelerini hesapla ve bakım penceresi planla. Büyük tablolarda pt-online-schema-change gibi araçlar hayat kurtarır.

Son olarak: partition kullandıktan sonra EXPLAIN ile partition pruning’in gerçekten çalıştığını doğrulamayı unutma. Pruning çalışmıyorsa tüm bu emek boşa gidebilir.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir