MariaDB ve MySQL’de Toplu INSERT ile Çoklu Satır Ekleme
Veritabanı yönetiminde performans genellikle küçük detaylarda saklıdır. Yüzlerce ya da binlerce satırı tek tek INSERT ile eklemek yerine toplu INSERT kullanmak, hem sunucu yükünü dramatik biçimde azaltır hem de işlem süresini ciddi oranda kısaltır. Bu yazıda MariaDB ve MySQL üzerinde toplu INSERT’in nasıl çalıştığını, gerçek dünya senaryolarında nasıl kullanıldığını ve dikkat etmeniz gereken noktalari ele alacağım.
Toplu INSERT Nedir ve Neden Önemlidir?
Normal şartlarda bir tabloya veri eklerken çoğu geliştirici veya sysadmin şöyle bir yaklaşım benimser:
INSERT INTO kullanicilar (ad, soyad, email) VALUES ('Ali', 'Yilmaz', '[email protected]');
INSERT INTO kullanicilar (ad, soyad, email) VALUES ('Veli', 'Kaya', '[email protected]');
INSERT INTO kullanicilar (ad, soyad, email) VALUES ('Ayse', 'Demir', '[email protected]');
Bu yaklaşımda her INSERT ayrı bir SQL ifadesi olarak sunucuya gönderilir. Sunucu her seferinde sorguyu parse eder, execution plan oluşturur, disk I/O yapar ve transaction yönetir. 10 satır için bu çok fark yaratmaz. Ama 10.000 satır için ciddi bir performans sorunu haline gelir.
Toplu INSERT ise aynı işlemi tek bir sorgu içinde yapar:
INSERT INTO kullanicilar (ad, soyad, email) VALUES
('Ali', 'Yilmaz', '[email protected]'),
('Veli', 'Kaya', '[email protected]'),
('Ayse', 'Demir', '[email protected]');
Bu yaklaşımın temel avantajları şunlardır:
- Parse maliyeti: Sunucu tek bir sorguyu parse eder, üç ayrı sorgu yerine
- Network round-trip: İstemci ile sunucu arasındaki gidiş-dönüş sayısı azalır
- Transaction overhead: InnoDB transaction log yazma işlemi birleştirilir
- Index güncelleme: Toplu işlemde index güncellemeleri daha verimli yapılır
- Auto-increment kilidi: Her satır için ayrı ayrı kilit alınmaz
Gerçek testlerde 1000 satırı tek tek INSERT ile eklemek yaklaşık 2-5 saniye sürerken, toplu INSERT ile aynı işlem 50-200 milisaniyede tamamlanabilir. Bu fark üretim ortamında son derece kritiktir.
Temel Sözdizimi ve Kullanımı
Toplu INSERT’in temel yapısını anlamak için önce basit bir tablo oluşturalım:
CREATE TABLE urunler (
id INT AUTO_INCREMENT PRIMARY KEY,
urun_adi VARCHAR(100) NOT NULL,
kategori VARCHAR(50),
fiyat DECIMAL(10,2),
stok INT DEFAULT 0,
olusturma_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Şimdi bu tabloya tek sorguda birden fazla satır ekleyelim:
INSERT INTO urunler (urun_adi, kategori, fiyat, stok) VALUES
('Laptop Pro 15', 'Elektronik', 12500.00, 45),
('Mekanik Klavye', 'Elektronik', 750.00, 120),
('Oyuncu Mouse', 'Elektronik', 450.00, 89),
('USB Hub 7 Port', 'Aksesuar', 185.00, 200),
('Laptop Cantasi', 'Aksesuar', 320.00, 75),
('Monitor 27 Inc', 'Elektronik', 4200.00, 30),
('HDMI Kablo 2m', 'Aksesuar', 65.00, 500);
Burada dikkat edilmesi gereken nokta, her VALUES grubunun parantez içinde olması ve aralarında virgül bulunmasıdır. Son satırda virgül değil noktalı virgül kullanılır.
Gerçek Dünya Senaryosu 1: E-Ticaret Ürün Yükleme
Diyelim ki bir e-ticaret sitesinin veritabanını yönetiyorsunuz ve tedarikçiden gelen bir Excel dosyasından yüzlerce ürünü sisteme aktarmanız gerekiyor. Bu tür senaryolarda toplu INSERT hayat kurtarır.
Önce bir Python scripti ile CSV’yi okuyup SQL üretebilirsiniz, ama doğrudan SQL tarafında da şöyle bir yapı kurabilirsiniz:
-- Önce geçici bir staging tablosu oluşturalım
CREATE TEMPORARY TABLE temp_urunler LIKE urunler;
-- Toplu veri ekle
INSERT INTO temp_urunler (urun_adi, kategori, fiyat, stok) VALUES
('Samsung Galaxy S24', 'Telefon', 28000.00, 15),
('iPhone 15 Pro', 'Telefon', 45000.00, 8),
('Xiaomi 14', 'Telefon', 22000.00, 22),
('OnePlus 12', 'Telefon', 25000.00, 11),
('Google Pixel 8', 'Telefon', 30000.00, 6),
('Sony Xperia 1 V', 'Telefon', 35000.00, 4),
('Motorola Edge 40', 'Telefon', 18000.00, 19),
('Nokia G60', 'Telefon', 8500.00, 35),
('Realme 11 Pro', 'Telefon', 11000.00, 28),
('Oppo Reno 10', 'Telefon', 14000.00, 17);
-- Staging'den ana tabloya aktar, duplicate kontrolü ile
INSERT INTO urunler (urun_adi, kategori, fiyat, stok)
SELECT urun_adi, kategori, fiyat, stok
FROM temp_urunler
WHERE urun_adi NOT IN (SELECT urun_adi FROM urunler);
Bu yaklaşım hem toplu eklemenin performans avantajını kullanır hem de mevcut kayıtları bozmaz.
INSERT IGNORE ve ON DUPLICATE KEY UPDATE
Üretim ortamında sıkça karşılaşılan bir durum şudur: Veri eklerken bazı kayıtlar zaten mevcut olabilir. Bu durumda hata almamak için iki farklı yaklaşım kullanabilirsiniz.
INSERT IGNORE ile çakışan kayıtları sessizce atla:
INSERT IGNORE INTO kullanicilar (id, kullanici_adi, email, kayit_tarihi) VALUES
(1, 'admin', '[email protected]', '2024-01-15'),
(2, 'mehmet.yilmaz', '[email protected]', '2024-01-16'),
(3, 'fatma.kaya', '[email protected]', '2024-01-17'),
(4, 'hasan.demir', '[email protected]', '2024-01-18'),
(5, 'zeynep.ozturk', '[email protected]', '2024-01-19');
Eğer id=1 zaten varsa INSERT IGNORE onu atlar ve diğerlerini eklemeye devam eder. Hata fırlatmaz.
ON DUPLICATE KEY UPDATE ile mevcut kaydı güncelle:
INSERT INTO urun_stok (urun_id, depo_id, miktar, son_guncelleme) VALUES
(101, 1, 50, NOW()),
(102, 1, 30, NOW()),
(103, 1, 75, NOW()),
(101, 2, 20, NOW()),
(102, 2, 45, NOW())
ON DUPLICATE KEY UPDATE
miktar = VALUES(miktar),
son_guncelleme = VALUES(son_guncelleme);
Bu yaklaşım özellikle stok yönetimi, sayaç güncellemeleri ve upsert (güncelle yoksa ekle) senaryolarında çok işe yarar. Warehouse yönetim sistemlerinde günde onlarca kez çalışan bir script için idealdir.
Gerçek Dünya Senaryosu 2: Log Verisi Arşivleme
Sistem yöneticileri için sık karşılaşılan bir senaryo: Uygulama loglarını veritabanına toplu olarak yazmak. Diyelim ki bir web uygulamasının access loglarını parse edip veritabanına aktarıyorsunuz.
-- Log tablosu
CREATE TABLE access_logs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
ip_adresi VARCHAR(45),
istek_yontemi ENUM('GET','POST','PUT','DELETE','HEAD'),
url VARCHAR(500),
http_kodu SMALLINT,
yanit_suresi INT,
kullanici_ajan TEXT,
log_zamani DATETIME,
INDEX idx_log_zamani (log_zamani),
INDEX idx_ip (ip_adresi),
INDEX idx_http_kodu (http_kodu)
);
-- Toplu log ekleme (gerçekte bir script bunu parse ederek üretir)
INSERT INTO access_logs
(ip_adresi, istek_yontemi, url, http_kodu, yanit_suresi, log_zamani)
VALUES
('192.168.1.100', 'GET', '/index.html', 200, 45, '2024-11-15 10:23:01'),
('192.168.1.101', 'POST', '/api/login', 200, 123, '2024-11-15 10:23:02'),
('10.0.0.55', 'GET', '/api/urunler', 200, 89, '2024-11-15 10:23:03'),
('192.168.1.100', 'GET', '/api/sepet', 401, 12, '2024-11-15 10:23:04'),
('203.0.113.10', 'GET', '/admin', 403, 8, '2024-11-15 10:23:05'),
('192.168.1.105', 'DELETE', '/api/urun/45', 204, 67, '2024-11-15 10:23:06'),
('10.0.0.60', 'PUT', '/api/profil', 200, 156, '2024-11-15 10:23:07'),
('192.168.2.20', 'GET', '/sayfa-bulunamadi', 404, 11, '2024-11-15 10:23:08');
Bu yapıyı bir bash scripti içinde şöyle kullanabilirsiniz:
#!/bin/bash
# Log dosyasini parse edip toplu INSERT olusturan script
LOG_FILE="/var/log/nginx/access.log"
DB_HOST="localhost"
DB_USER="log_writer"
DB_PASS="guvenli_sifre"
DB_NAME="uygulama_db"
BATCH_SIZE=500
# Son 1 saatin loglarini al ve SQL olustur
python3 << 'EOF'
import re
import subprocess
from datetime import datetime, timedelta
log_pattern = r'(S+) S+ S+ [([^]]+)] "(S+) (S+) S+" (d+) d+ "[^"]*" "([^"]*)"'
values_list = []
with open('/var/log/nginx/access.log', 'r') as f:
for line in f:
match = re.match(log_pattern, line)
if match:
ip, zaman, metod, url, kod, ua = match.groups()
url = url.replace("'", "''")[:500]
ua = ua.replace("'", "''")[:200]
values_list.append(f"('{ip}', '{metod}', '{url}', {kod}, '{ua}')")
if len(values_list) >= 500:
sql = "INSERT INTO access_logs (ip_adresi, istek_yontemi, url, http_kodu, kullanici_ajan) VALUES " + ",".join(values_list) + ";"
print(sql)
values_list = []
if values_list:
sql = "INSERT INTO access_logs (ip_adresi, istek_yontemi, url, http_kodu, kullanici_ajan) VALUES " + ",".join(values_list) + ";"
print(sql)
EOF
Performans Optimizasyonu: Batch Boyutu ve Transaction Yönetimi
Toplu INSERT kullanırken batch boyutu kritik bir parametredir. Her şeyi tek sorguda yapmak her zaman en iyi seçenek değildir. Çok büyük batch’ler bellek sorunlarına ve lock contention’a yol açabilir.
Genel kural olarak şu yaklaşım işe yarar:
- Küçük satırlar (< 1KB): 1000-5000 satır per batch
- Orta büyüklükte satırlar (1-10KB): 200-500 satır per batch
- Büyük satırlar (> 10KB): 50-100 satır per batch
Transaction ile sarmalamak performansı daha da artırır:
-- Transaction ile toplu INSERT - InnoDB için önerilir
START TRANSACTION;
INSERT INTO siparis_detaylari (siparis_id, urun_id, miktar, birim_fiyat) VALUES
(1001, 45, 2, 750.00),
(1001, 67, 1, 1200.00),
(1001, 23, 3, 89.90),
(1002, 12, 1, 4200.00),
(1002, 45, 2, 750.00),
(1003, 89, 5, 45.00),
(1003, 90, 5, 45.00),
(1003, 91, 5, 45.00);
-- Siparis toplamlarini guncelle
UPDATE siparisler s
JOIN (
SELECT siparis_id, SUM(miktar * birim_fiyat) AS toplam
FROM siparis_detaylari
WHERE siparis_id IN (1001, 1002, 1003)
GROUP BY siparis_id
) t ON s.id = t.siparis_id
SET s.toplam_tutar = t.toplam;
COMMIT;
Transaction kullanmanın avantajları:
- Atomicity: Ya hepsi ya hiçbiri prensibi
- Performance: InnoDB redo log’u toplu yazar
- Consistency: Ara durumlar görünmez
- Rollback imkanı: Hata durumunda geri alınabilir
INSERT … SELECT ile Tablo Kopyalama
Başka bir tablodan veri çekerek toplu INSERT yapmak çok yaygın bir senaryodur. Özellikle veri arşivleme, raporlama tabloları oluşturma veya veri migrasyon işlemlerinde kullanılır.
-- Eski siparisleri arsiv tablosuna tasi
INSERT INTO siparisler_arsiv
(siparis_id, musteri_id, toplam_tutar, siparis_tarihi, durum)
SELECT
id,
musteri_id,
toplam_tutar,
siparis_tarihi,
durum
FROM siparisler
WHERE siparis_tarihi < DATE_SUB(NOW(), INTERVAL 2 YEAR)
AND durum IN ('tamamlandi', 'iptal_edildi');
-- Arsivlenen kayitlari sil
DELETE FROM siparisler
WHERE siparis_tarihi < DATE_SUB(NOW(), INTERVAL 2 YEAR)
AND durum IN ('tamamlandi', 'iptal_edildi');
Bu yaklaşım özellikle büyük tablolarda partition pruning ile birleştirildiğinde son derece verimlidir.
LOAD DATA INFILE: Maksimum Performans İçin
Gerçekten büyük veri setleri için (milyonlarca satır) LOAD DATA INFILE toplu INSERT’ten bile çok daha hızlıdır. Ama bu da özünde toplu veri yükleme ailesine dahildir:
-- Önce CSV dosyasini hazirla
-- /tmp/urunler.csv icerigi:
-- "Laptop A100",Elektronik,8500.00,25
-- "Tablet B200",Elektronik,3200.00,40
LOAD DATA INFILE '/tmp/urunler.csv'
INTO TABLE urunler
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY 'n'
(urun_adi, kategori, fiyat, stok);
-- MySQL 8.0+ ve MariaDB 10.3+ için LOAD DATA LOCAL INFILE
-- istemci tarafindan dosya gonderilir
LOAD DATA LOCAL INFILE '/home/admin/yeni_urunler.csv'
INTO TABLE urunler
CHARACTER SET utf8mb4
FIELDS TERMINATED BY ','
OPTIONALLY ENCLOSED BY '"'
LINES TERMINATED BY 'rn'
IGNORE 1 LINES
(urun_adi, kategori, fiyat, stok);
LOAD DATA INFILE, toplu INSERT’e göre yaklaşık 10-20 kat daha hızlıdır. Milyonlarca satır içeren CSV dosyalarını birkaç dakikada yükleyebilir.
Dikkat Edilmesi Gereken Noktalar
Toplu INSERT kullanırken dikkat etmeniz gereken birkaç önemli nokta var:
- max_allowed_packet: MariaDB/MySQL’de tek bir sorgunun maksimum boyutunu belirler. Varsayılan değer genellikle 16MB’dır. Çok büyük batch’lerde bu limitin aşılmamasına dikkat edin.
-- Mevcut limiti kontrol et
SHOW VARIABLES LIKE 'max_allowed_packet';
-- Gerekirse artir (my.cnf veya oturum bazinda)
SET SESSION max_allowed_packet = 67108864; -- 64MB
- innodb_buffer_pool_size: Büyük toplu işlemler sırasında buffer pool yeterliliği kritiktir. Yetersiz buffer pool, disk I/O’yu artırır.
- SQL injection riski: Uygulama kodundan toplu INSERT üretirken prepared statement kullanın, string concatenation ile SQL üretmekten kaçının.
- Character encoding: UTF-8 karakterler içeren veriler için bağlantı charset’ini doğru ayarlayın:
SET NAMES utf8mb4;
SET CHARACTER SET utf8mb4;
INSERT INTO haberler (baslik, icerik, yazar) VALUES
('Türkçe Karakter Testi', 'Güzel çiçekler açıyor bahçede', 'Ömer Şahin'),
('Sistem Güncellemesi', 'Şubat ayında yapılacak güncellemeler hakkında', 'Gülşen Yıldız'),
('Ağ Altyapısı', 'İstanbul ofisindeki ağ altyapısı yenileniyor', 'Çağrı Öztürk');
- Trigger performansı: Tabloda BEFORE INSERT veya AFTER INSERT trigger varsa, toplu INSERT’te her satır için trigger çalışır. Bu performans etkisini göz önünde bulundurun.
- Foreign key constraint’leri: Büyük veri yüklemelerinde geçici olarak foreign key kontrollerini devre dışı bırakmak işlemi hızlandırır:
SET FOREIGN_KEY_CHECKS = 0;
INSERT INTO siparis_detaylari (siparis_id, urun_id, miktar, birim_fiyat) VALUES
(2001, 101, 3, 450.00),
(2001, 102, 1, 1200.00),
(2002, 103, 2, 780.00),
(2002, 104, 4, 120.00),
(2003, 101, 1, 450.00);
SET FOREIGN_KEY_CHECKS = 1;
Dikkat: FK kontrollerini kapattıktan sonra yüklediğiniz verinin gerçekten tutarlı olduğundan emin olun. Aksi takdirde veri bütünlüğü bozulur.
Monitoring ve Hata Ayıklama
Toplu INSERT işlemlerini izlemek için şu sorguları kullanabilirsiniz:
-- Aktif uzun suren sorgulari goruntule
SHOW PROCESSLIST;
-- InnoDB durum bilgisi - toplu yazma performansini incele
SHOW ENGINE INNODB STATUSG
-- Son toplu islemin row count'unu al
SELECT ROW_COUNT();
-- Tablo istatistiklerini guncelle (buyuk insert sonrasi)
ANALYZE TABLE urunler;
ANALYZE TABLE siparis_detaylari;
Eğer toplu INSERT sırasında deadlock veya lock timeout alıyorsanız, şu ayarları kontrol edin:
- innodb_lock_wait_timeout: Varsayılan 50 saniye. Büyük batch’lerde artırılabilir.
- innodb_deadlock_detect: Açık olması durumunda otomatik deadlock tespiti yapar.
Sonuç
Toplu INSERT kullanımı, veritabanı performansını artırmanın en basit ve etkili yollarından biridir. Tek satırlık INSERT’lerden toplu yapıya geçmek için büyük bir refactoring gerekmez. Uygulamanızın veri yazma katmanında bu değişikliği yapmanız, özellikle yüksek trafikli üretim ortamlarında çok ciddi performans kazanımları sağlar.
Özetle şu prensipleri aklınızda tutun:
- Her zaman batch boyutunu kontrol altında tutun, tek sorguda milyonlarca satır yazmaya çalışmayın
- InnoDB ile transaction sarmalama kullanın
- Büyük veri migrasyonlarında LOAD DATA INFILE’i değerlendirin
- max_allowed_packet ve innodb_buffer_pool_size ayarlarını gözden geçirin
- Uygulama kodunda mutlaka prepared statement kullanın
- ON DUPLICATE KEY UPDATE ve INSERT IGNORE’u gerçek iş mantığınıza göre seçin
Bu teknikleri doğru uygulayarak veritabanı sunucularınızın daha verimli çalışmasını sağlayabilir, gereksiz kaynak tüketimini ortadan kaldırabilirsiniz. Bir sonraki yazıda büyük tablolarda partition yönetimi ve arşivleme stratejilerine bakacağız.
