Yıllar önce bir e-ticaret projesinde, sipariş tablosu 200 milyon satırı geçince sorguların ne kadar yavaşladığını bizzat yaşadım. SELECT sorguları dakikalarca çalışıyor, raporlar zaman aşımına uğruyor, müşteriler şikayet ediyordu. O günden bu yana MySQL partitioning konusu benim için sadece bir özellik değil, gerçek bir kurtarıcı haline geldi. Bu yazıda büyük tablolarla uğraşan her sysadmin’in bilmesi gereken MySQL partition yönetimini tüm detaylarıyla ele alacağız.
MySQL Partitioning Nedir ve Ne Zaman Kullanmalısınız?
MySQL partitioning, büyük bir tabloyu mantıksal olarak daha küçük parçalara bölme işlemidir. Fiziksel olarak veriler ayrı dosyalarda saklanır, ancak uygulama tarafından tek bir tablo olarak görülür. Bu sayede sorgular tüm veriyi taramak yerine yalnızca ilgili partition’lara erişir, bu da partition pruning olarak adlandırılır.
Ne zaman partition kullanmalısınız? Birkaç pratik kural:
- Tablonuz 10 milyon satırı geçtiyse ve sorgu performansı düşüyorsa
- Tarih bazlı veri arşivleme ihtiyacınız varsa (log tabloları, sipariş geçmişi gibi)
- Eski verileri hızlıca silmeniz gerekiyorsa
- Veriyi farklı depolama alanlarına dağıtmak istiyorsanız
- Birden fazla CPU çekirdeğini paralel sorgu için kullanmak istiyorsanız
Şunu da açıkça belirtelim: Partition, yanlış kullanıldığında performansı artırmak yerine düşürür. Özellikle partition key’i sorgularda kullanmıyorsanız, partition kullanmak durumu daha da kötüleştirebilir.
Partition Türleri
MySQL dört temel partition türü sunar. Hangisini kullanacağınız, veri yapınıza ve sorgu alışkanlıklarınıza bağlıdır.
RANGE Partitioning
En yaygın kullanılan türdür. Belirli bir kolonun değer aralığına göre bölümleme yapar. Tarih bazlı tablolar için mükemmeldir.
CREATE TABLE siparisler (
siparis_id INT NOT NULL,
musteri_id INT NOT NULL,
siparis_tarihi DATE NOT NULL,
toplam_tutar DECIMAL(10,2),
durum VARCHAR(20)
)
PARTITION BY RANGE (YEAR(siparis_tarihi)) (
PARTITION p2020 VALUES LESS THAN (2021),
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_gelecek VALUES LESS THAN MAXVALUE
);
MAXVALUE içeren son partition çok önemlidir. Bunu koymayı unutursanız ve tanımlanmış aralığın dışında bir değer gelirse MySQL hata verir.
LIST Partitioning
Belirli değer listelerine göre bölümleme yapar. Bölgesel veya kategorik ayrımlar için kullanışlıdır.
CREATE TABLE urunler (
urun_id INT NOT NULL,
urun_adi VARCHAR(100),
kategori_id INT NOT NULL,
fiyat DECIMAL(10,2),
stok INT
)
PARTITION BY LIST (kategori_id) (
PARTITION p_elektronik VALUES IN (1, 2, 3, 4),
PARTITION p_giyim VALUES IN (5, 6, 7, 8),
PARTITION p_ev VALUES IN (9, 10, 11, 12),
PARTITION p_diger VALUES IN (13, 14, 15)
);
HASH Partitioning
Belirli sayıda partition’a eşit dağılım için kullanılır. MySQL otomatik olarak hangi verinin nereye gideceğini hesaplar.
CREATE TABLE kullanici_aktiviteleri (
aktivite_id BIGINT NOT NULL AUTO_INCREMENT,
kullanici_id INT NOT NULL,
aktivite_tip VARCHAR(50),
zaman DATETIME,
PRIMARY KEY (aktivite_id, kullanici_id)
)
PARTITION BY HASH (kullanici_id)
PARTITIONS 8;
KEY Partitioning
HASH’e benzer, ancak MySQL’in kendi hash fonksiyonunu kullanır ve PRIMARY KEY veya UNIQUE KEY kolonlarıyla çalışır.
CREATE TABLE session_data (
session_id VARCHAR(64) NOT NULL,
kullanici_id INT,
veri TEXT,
olusturma TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (session_id)
)
PARTITION BY KEY (session_id)
PARTITIONS 16;
Mevcut Tabloları Partition’a Alma
Pratikte en sık karşılaşılan senaryo budur: Elinizde zaten devasa bir tablo var ve onu partition’lamak istiyorsunuz. Bunu yapmanın birkaç yolu var.
ALTER TABLE ile Dönüştürme
-- Dikkat: Bu işlem büyük tablolarda saatler sürebilir ve tabloyu kilitler
ALTER TABLE log_tablosu
PARTITION BY RANGE (UNIX_TIMESTAMP(log_tarihi)) (
PARTITION p_eski VALUES LESS THAN (UNIX_TIMESTAMP('2023-01-01')),
PARTITION p_2023_q1 VALUES LESS THAN (UNIX_TIMESTAMP('2023-04-01')),
PARTITION p_2023_q2 VALUES LESS THAN (UNIX_TIMESTAMP('2023-07-01')),
PARTITION p_2023_q3 VALUES LESS THAN (UNIX_TIMESTAMP('2023-10-01')),
PARTITION p_2023_q4 VALUES LESS THAN (UNIX_TIMESTAMP('2024-01-01')),
PARTITION p_2024 VALUES LESS THAN MAXVALUE
);
Büyük tablolarda bu yaklaşım yerine pt-online-schema-change veya gh-ost kullanmanızı şiddetle tavsiye ederim. Production ortamında doğrudan ALTER TABLE çalıştırmak tablo kilidine yol açar ve downtime yaratır.
Düşük Downtime ile Migration
# pt-online-schema-change ile partition ekleme
# Önce aracı yükleyin (Percona Toolkit)
apt-get install percona-toolkit
# Sonra tabloyu canlı ortamda dönüştürün
pt-online-schema-change
--alter "PARTITION BY RANGE (YEAR(siparis_tarihi)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p_gelecek VALUES LESS THAN MAXVALUE
)"
--host=localhost
--user=root
--password=sifre
--execute
D=veritabani_adi,t=siparisler
Partition Yönetimi: Ekleme, Silme ve Düzenleme
Partition’ların asıl gücü dinamik yönetimden gelir. Özellikle eski verileri silmek DELETE yerine DROP PARTITION ile yapıldığında performans farkı muazzamdır.
-- Yeni partition ekleme (önce MAXVALUE partition varsa onu kaldırın)
ALTER TABLE siparisler
REORGANIZE PARTITION p_gelecek INTO (
PARTITION p2025 VALUES LESS THAN (2026),
PARTITION p_gelecek VALUES LESS THAN MAXVALUE
);
-- Belirli bir partition'ı silme (içindeki verilerle birlikte)
-- DELETE ile karşılaştırın: 50 milyon satır için DELETE dakikalar alır,
-- DROP PARTITION ise saniyeler içinde tamamlanır
ALTER TABLE siparisler DROP PARTITION p2020;
-- Partition'daki verileri boşaltma (yapıyı koruyarak)
ALTER TABLE siparisler TRUNCATE PARTITION p2021;
-- Partition bilgilerini görüntüleme
SELECT
PARTITION_NAME,
TABLE_ROWS,
DATA_LENGTH,
INDEX_LENGTH,
PARTITION_DESCRIPTION
FROM information_schema.PARTITIONS
WHERE TABLE_NAME = 'siparisler'
AND TABLE_SCHEMA = 'veritabani_adi';
Otomatik Partition Oluşturma: Gerçek Dünya Senaryosu
Bir log yönetim sisteminde çalıştığınızı düşünün. Her ay için otomatik partition oluşturmanız ve eski olanları silmeniz gerekiyor. Ben bu iş için bir stored procedure ve event scheduler kullanıyorum.
-- Aylık partition yönetim prosedürü
DELIMITER $$
CREATE PROCEDURE partition_yonet()
BEGIN
DECLARE sonraki_ay VARCHAR(7);
DECLARE partition_adi VARCHAR(20);
DECLARE partition_deger INT;
-- Gelecek ay için partition oluştur
SET sonraki_ay = DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 2 MONTH), '%Y_%m');
SET partition_adi = CONCAT('p_', sonraki_ay);
SET partition_deger = UNIX_TIMESTAMP(DATE_FORMAT(
DATE_ADD(NOW(), INTERVAL 3 MONTH), '%Y-%m-01'
));
-- Partition zaten var mı kontrol et
IF NOT EXISTS (
SELECT 1 FROM information_schema.PARTITIONS
WHERE TABLE_NAME = 'uygulama_loglari'
AND PARTITION_NAME = partition_adi
) THEN
SET @sql = CONCAT(
'ALTER TABLE uygulama_loglari REORGANIZE PARTITION p_gelecek INTO (',
'PARTITION ', partition_adi, ' VALUES LESS THAN (', partition_deger, '),',
'PARTITION p_gelecek VALUES LESS THAN MAXVALUE)'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
-- 12 aydan eski partition'ları sil
SET @eski_partition = CONCAT('p_', DATE_FORMAT(
DATE_SUB(NOW(), INTERVAL 13 MONTH), '%Y_%m'
));
IF EXISTS (
SELECT 1 FROM information_schema.PARTITIONS
WHERE TABLE_NAME = 'uygulama_loglari'
AND PARTITION_NAME = @eski_partition
) THEN
SET @sql = CONCAT('ALTER TABLE uygulama_loglari DROP PARTITION ', @eski_partition);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END IF;
END$$
DELIMITER ;
-- Her ayın ilk günü sabah 2'de çalıştır
CREATE EVENT otomatik_partition_yonetim
ON SCHEDULE EVERY 1 MONTH
STARTS '2024-02-01 02:00:00'
DO CALL partition_yonet();
Event Scheduler’ın aktif olduğunu kontrol etmeyi unutmayın:
-- Event Scheduler durumunu kontrol et
SHOW VARIABLES LIKE 'event_scheduler';
-- Kapalıysa açın
SET GLOBAL event_scheduler = ON;
-- my.cnf'e de ekleyin kalıcı olsun
# /etc/mysql/mysql.conf.d/mysqld.cnf
# event_scheduler = ON
Partition Pruning: Sorguların Doğru Partition’ı Kullandığından Emin Olun
Partition kullanıyorsunuz ama sorgular yavaş mı? Muhtemelen partition pruning çalışmıyordur. EXPLAIN ile kontrol edin:
-- Partition pruning çalışıyor mu?
EXPLAIN SELECT *
FROM siparisler
WHERE siparis_tarihi BETWEEN '2024-01-01' AND '2024-12-31';
-- Çıktıda 'partitions' kolonuna bakın
-- Sadece p2024 görünüyorsa pruning çalışıyor
-- Tüm partition'lar görünüyorsa sorguyu gözden geçirin
-- YANLIS: Pruning çalışmaz çünkü fonksiyon uygulanıyor
SELECT * FROM siparisler WHERE DATE_FORMAT(siparis_tarihi, '%Y') = '2024';
-- DOGRU: Partition key doğrudan karşılaştırılıyor
SELECT * FROM siparisler
WHERE siparis_tarihi >= '2024-01-01' AND siparis_tarihi < '2025-01-01';
Partition pruning’i engelleyen yaygın hatalar:
- Fonksiyon kullanımı:
WHERE YEAR(tarih) = 2024yerineWHERE tarih BETWEEN...kullanın - Tip uyumsuzluğu: Partition key INT ise sorgu da INT ile karşılaştırın, string ile değil
- OR kullanımı:
WHERE bolge_id = 1 OR bolge_id = 5bazen tüm partition’ları tarayabilir - NULL değerler: RANGE partition’da NULL değerler en küçük partition’a düşer, dikkatli olun
Subpartitioning
Daha gelişmiş senaryolar için partition içinde partition oluşturabilirsiniz. Örneğin hem tarih hem bölgeye göre bölümleme:
CREATE TABLE satis_verileri (
satis_id BIGINT NOT NULL,
bolge_id INT NOT NULL,
satis_tarihi DATE NOT NULL,
tutar DECIMAL(12,2),
PRIMARY KEY (satis_id, bolge_id, satis_tarihi)
)
PARTITION BY RANGE (YEAR(satis_tarihi))
SUBPARTITION BY HASH (bolge_id)
SUBPARTITIONS 4 (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p2025 VALUES LESS THAN (2026)
);
Subpartitioning’i dikkatli kullanın. Her partition x subpartition kombinasyonu ayrı bir dosya oluşturur ve dosya sistemi limitlerine çarpabilirsiniz. Genellikle büyük veri ambarı senaryoları dışında gerekli olmaz.
Partition ve Index Stratejisi
Partition ile index birlikte kullanımı kritik bir konudur. Bazı önemli noktalar:
- MySQL’de global index yoktur. Her partition kendi index’ine sahiptir.
UNIQUE KEYvePRIMARY KEYconstraint’leri partition key’i içermek zorundadır.- Bu kural bazen şema tasarımını zorlaştırır.
-- Bu HATALI çünkü siparis_id tek başına unique key olamaz partition'lı tabloda
CREATE TABLE siparisler (
siparis_id INT NOT NULL AUTO_INCREMENT,
siparis_tarihi DATE NOT NULL,
tutar DECIMAL(10,2),
PRIMARY KEY (siparis_id) -- HATA!
)
PARTITION BY RANGE (YEAR(siparis_tarihi)) (...);
-- DOGRU: Primary key partition key'i içermeli
CREATE TABLE siparisler (
siparis_id INT NOT NULL AUTO_INCREMENT,
siparis_tarihi DATE NOT NULL,
tutar DECIMAL(10,2),
PRIMARY KEY (siparis_id, siparis_tarihi) -- Her ikisi birlikte
)
PARTITION BY RANGE (YEAR(siparis_tarihi)) (...);
Bu durum uygulamanızı etkileyebilir. Sipariş ID’ye göre arama yapıyorsanız tüm partition’lar taranacaktır. Bu durumu aşmak için ya arama sorgularına tarih filtresi ekleyin ya da ayrı bir lookup tablosu tutun.
Performans Izleme ve Troubleshooting
Partition’larınızın ne kadar veri tuttuğunu ve performansa etkisini düzenli izlemeniz gerekir:
-- Partition boyutlarını detaylı görüntüle
SELECT
PARTITION_NAME AS 'Partition',
TABLE_ROWS AS 'Satir Sayisi',
ROUND(DATA_LENGTH / 1024 / 1024, 2) AS 'Veri (MB)',
ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS 'Index (MB)',
PARTITION_DESCRIPTION AS 'Sinir Degeri'
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = 'veritabani_adi'
AND TABLE_NAME = 'siparisler'
ORDER BY PARTITION_ORDINAL_POSITION;
-- Hangi sorguların partition scan yaptığını bul
SELECT
digest_text,
count_star,
avg_timer_wait / 1000000000 AS avg_ms,
sum_rows_examined / count_star AS avg_rows_examined
FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text LIKE '%siparisler%'
ORDER BY avg_timer_wait DESC
LIMIT 20;
Partition’ların durumu beklediğiniz gibi değilse şu kontrolleri yapın:
-- Partition istatistiklerini güncelle
ANALYZE TABLE siparisler;
-- Tablo ve partition bütünlüğünü kontrol et
CHECK TABLE siparisler;
-- Partition'lar arası veri dengesizliğini kontrol et
-- (HASH partition için önemli)
SELECT PARTITION_NAME, TABLE_ROWS
FROM information_schema.PARTITIONS
WHERE TABLE_NAME = 'kullanici_aktiviteleri'
ORDER BY TABLE_ROWS DESC;
Partition Exchange: Arşivleme için Güçlü Bir Araç
Partition exchange, bir partition’ın içeriğini başka bir tablo ile takas etmenizi sağlar. Arşivleme senaryolarında çok kullanışlıdır:
-- Önce arşiv tablosunu oluştur (partition'lı tabloyla aynı yapıda)
CREATE TABLE siparisler_arsiv_2022 LIKE siparisler;
-- Partition'ı kaldır (arşiv tablosu partition'sız olmalı)
ALTER TABLE siparisler_arsiv_2022 REMOVE PARTITIONING;
-- 2022 partition'ını arşiv tablosuyla değiştir
-- Bu işlem saniyeler içinde tamamlanır, veri kopyalanmaz!
ALTER TABLE siparisler
EXCHANGE PARTITION p2022
WITH TABLE siparisler_arsiv_2022;
-- Artık p2022 boş, siparisler_arsiv_2022 eski veriyi tutuyor
-- Arşiv tablosunu başka bir veritabanına taşıyabilir veya
-- sıkıştırılmış formatla kaydedebilirsiniz
-- Arşiv tablosunu sıkıştırılmış diske taşı
ALTER TABLE siparisler_arsiv_2022
ROW_FORMAT=COMPRESSED
KEY_BLOCK_SIZE=8;
Bu teknik özellikle GDPR uyumluluğu için belirli dönemin verilerini silmeniz gerektiğinde veya eski verileri daha ucuz storage’a taşımanız gerektiğinde altın değerindedir.
Dikkat Edilmesi Gereken Sınırlamalar
MySQL partition’larının bazı önemli sınırlamaları var, bunları önceden bilmek can kurtarır:
- Maksimum partition sayısı: Tablo başına 8192 partition (MySQL 5.7+). Daha eskisinde 1024.
- Foreign key desteği yok: Partition’lı tablolarda FOREIGN KEY constraint kullanamazsınız. Uygulama katmanında yönetmeniz gerekir.
- Full-text index: Partition’lı tablolarda FULLTEXT index desteklenmez.
- Spatial index: Aynı şekilde desteklenmez.
- Temporary table: Geçici tablolar partition’lanamaz.
- InnoDB ve partition: InnoDB ile partition birlikte iyi çalışır ancak tablespace yönetiminde dikkatli olun.
Özellikle foreign key yokluğu büyük bir kısıtlamadır. Eğer tablonuz başka tablolarla foreign key ilişkisine sahipse ya ilişkileri uygulama katmanına taşımanız ya da partition kullanımından vazgeçmeniz gerekir.
Sonuç
MySQL partitioning, doğru senaryoda kullanıldığında gerçekten hayat kurtarıcıdır. Başta bahsettiğim o 200 milyon satırlık sipariş tablosunda RANGE partition uyguladıktan sonra aylık raporların çalışma süresi 4 dakikadan 8 saniyeye düştü. Eski partition’ları DROP PARTITION ile silmek ise artık saniyeler alıyor.
Özetlemek gerekirse pratik tavsiyelerim şunlar:
- Başlamadan önce tasarım yapın: Partition key seçimi en kritik karardır, sonradan değiştirmek zahmetlidir.
- Sorgu alışkanlıklarını analiz edin: Sorgularınız partition key’i mutlaka filtre olarak kullanmalı, aksi halde pruning çalışmaz.
- Foreign key gerektiren tablolara dikkat edin: Partition bu tablolarda işe yaramaz.
- Otomatik partition yönetimi kurun: Manuel yönetim er ya da geç unutulur, event scheduler kullanın.
- Exchange partition’ı öğrenin: Arşivleme senaryolarında
DROP PARTITION‘dan çok daha esnektir. - Test ortamında deneyin: Partition migration işlemlerini önce test ortamında uygulayın, süreyi ölçün.
EXPLAINkullanmayı alışkanlık edinin: Sorguların hangi partition’ları taradığını her zaman doğrulayın.
Partition’lama bir sihirli değnek değildir. Yanlış kullanımı performansı artırmak yerine düşürür. Ama doğru tasarlanmış bir partition stratejisi, büyük ölçekli sistemlerde indexleme ve query optimization’dan sonra gelen en güçlü performans aracıdır.