INSERT INTO SELECT ile Tablodan Tabloya Veri Kopyalama

Veritabanı yönetiminde en sık karşılaşılan ihtiyaçlardan biri, bir tablodan diğerine veri taşımak ya da kopyalamaktır. Bu işlemi elle yapmaya çalışmak, özellikle milyonlarca satırlık verilerle uğraşırken hem zaman kaybı hem de hata riski demektir. MySQL ve MariaDB’nin sunduğu INSERT INTO SELECT yapısı, bu süreci tek bir SQL cümlesiyle çözüme kavuşturur. Yedekleme senaryolarından veri migrasyonuna, raporlama tablolarının oluşturulmasından arşivleme işlemlerine kadar pek çok alanda günlük hayatın vazgeçilmez aracıdır. Bu yazıda INSERT INTO SELECT komutunu tüm nüanslarıyla ele alacak, gerçek dünya senaryolarıyla pekiştireceğiz.

INSERT INTO SELECT Nedir ve Nasıl Çalışır?

INSERT INTO SELECT, bir SELECT sorgusunun döndürdüğü sonuç kümesini doğrudan başka bir tabloya yazan SQL ifadesidir. Klasik INSERT INTO ... VALUES(...) yapısından farkı, değerleri elle yazmak yerine başka bir sorgudan dinamik olarak çekmesidir.

Temel sözdizimi şöyledir:

INSERT INTO hedef_tablo (sutun1, sutun2, sutun3)
SELECT sutun1, sutun2, sutun3
FROM kaynak_tablo
WHERE kosul;

Bu yapıda dikkat edilmesi gereken birkaç temel nokta var:

  • Sütun sırası önemlidir: INSERT INTO kısmında belirtilen sütunlar ile SELECT kısmındaki sütunlar birebir eşleşmelidir.
  • Veri tipi uyumluluğu: Kaynak ve hedef sütunların veri tipleri uyumlu olmalıdır. Aksi hâlde implicit conversion yaşanır ya da hata alırsınız.
  • Hedef tablo mevcut olmalıdır: INSERT INTO SELECT, hedef tabloyu oluşturmaz. Tablo yoksa önce CREATE TABLE ile oluşturmanız gerekir. Tabloyu hem oluşturup hem doldurmak istiyorsanız CREATE TABLE ... SELECT yapısını kullanmalısınız.
  • Transaction desteği: InnoDB motorunda bu işlem transaction kapsamında gerçekleşir, dolayısıyla bir hata durumunda geri alınabilir.

Temel Kullanım Örnekleri

Tüm Satırları Kopyalamak

En basit kullanım, bir tablodaki tüm satırları başka bir tabloya kopyalamaktır. Diyelim ki customers tablonuzun tam bir kopyasını customers_backup tablosuna atmak istiyorsunuz:

-- Önce hedef tabloyu oluştur
CREATE TABLE customers_backup LIKE customers;

-- Tüm verileri kopyala
INSERT INTO customers_backup
SELECT * FROM customers;

CREATE TABLE ... LIKE komutu, kaynak tablonun yapısını (indeksler dahil) aynen kopyalar ama veri aktarmaz. Ardından INSERT INTO SELECT ile verileri taşırız. Bu iki adımlı yaklaşım özellikle yedekleme senaryolarında çok kullanışlıdır.

Belirli Sütunları Kopyalamak

Çoğu zaman tablonun tamamını değil, belirli sütunları kopyalamak istersiniz:

INSERT INTO musteri_ozet (musteri_id, ad, soyad, email)
SELECT id, first_name, last_name, email
FROM customers
WHERE aktif = 1;

Burada customers tablosundaki aktif müşterilerin yalnızca kimlik ve iletişim bilgilerini musteri_ozet adlı özet tabloya alıyoruz. Sütun adlarının farklı olması sorun değil, önemli olan sıraların eşleşmesidir.

WHERE Koşuluyla Filtreleyerek Kopyalamak

Gerçek dünyada nadiren tüm tabloyu kopyalarsınız. Filtreleme kriterleri belirleyerek yalnızca ihtiyaç duyduğunuz veriyi almak hem performans hem de depolama açısından avantajlıdır:

INSERT INTO arsiv_siparisler (siparis_id, musteri_id, siparis_tarihi, toplam_tutar, durum)
SELECT id, musteri_id, created_at, total_amount, status
FROM siparisler
WHERE created_at < '2023-01-01'
  AND status IN ('tamamlandi', 'iptal');

Bu örnekte 2023 yılından önce tamamlanan veya iptal edilen siparişleri arşiv tablosuna taşıyoruz. Bunun ardından kaynak tablodan bu kayıtları silebiliriz; bu yaklaşım büyük tablolarda sorgu performansını ciddi ölçüde artırır.

Gerçek Dünya Senaryoları

Senaryo 1: Aylık Raporlama Tablosu Oluşturma

E-ticaret sistemlerinde sıkça karşılaşılan bir durum: Her ay sonunda o ayki satış verilerini ayrı bir raporlama tablosuna çekmek. Bu sayede raporlama sorguları canlı tabloyu zorlamaz.

-- Ocak 2024 satış verilerini raporlama tablosuna aktar
INSERT INTO aylik_satis_raporu (yil, ay, kategori, toplam_satis, siparis_adedi, ortalama_sepet)
SELECT 
    YEAR(s.created_at) AS yil,
    MONTH(s.created_at) AS ay,
    p.kategori,
    SUM(sd.miktar * sd.birim_fiyat) AS toplam_satis,
    COUNT(DISTINCT s.id) AS siparis_adedi,
    AVG(s.total_amount) AS ortalama_sepet
FROM siparisler s
JOIN siparis_detaylari sd ON s.id = sd.siparis_id
JOIN urunler p ON sd.urun_id = p.id
WHERE s.created_at >= '2024-01-01'
  AND s.created_at < '2024-02-01'
  AND s.durum = 'tamamlandi'
GROUP BY YEAR(s.created_at), MONTH(s.created_at), p.kategori;

Bu sorgu join, gruplama ve aggregate fonksiyonları birlikte kullanarak doğrudan özet raporlama tablosuna veri yazıyor. Klasik bir ETL (Extract, Transform, Load) adımı olarak düşünebilirsiniz.

Senaryo 2: Veri Migrasyon – Eski Sistemden Yeni Sisteme

Uygulama sürüm güncellemelerinde tablo yapıları değişebilir. Eski tablodaki verileri yeni yapıya uyarlayarak aktarmak gerekir:

-- Eski users tablosundan yeni members tablosuna veri aktar
INSERT INTO members (
    member_id,
    username,
    email_address,
    full_name,
    registration_date,
    account_status,
    migrated_from
)
SELECT 
    id,
    kullanici_adi,
    eposta,
    CONCAT(ad, ' ', soyad),
    kayit_tarihi,
    CASE 
        WHEN aktif = 1 THEN 'active'
        WHEN aktif = 0 AND son_giris > DATE_SUB(NOW(), INTERVAL 1 YEAR) THEN 'inactive'
        ELSE 'suspended'
    END,
    'legacy_users'
FROM eski_kullanicilar
WHERE silinmis = 0;

Bu örnekte CONCAT ile iki ayrı alan birleştiriliyor, CASE WHEN ile eski sistemin 0/1 aktif mantığı yeni sistemin string durum alanına dönüştürülüyor ve migrasyon kaynağı bir sabit değer olarak ekleniyor.

Senaryo 3: Log Arşivleme

Uygulama log tabloları zamanla çok büyür ve performansı etkiler. Eski logları arşiv tablosuna taşımak klasik bir DBA işlemidir:

-- 90 günden eski logları arşivle
INSERT INTO uygulama_log_arsiv 
SELECT * FROM uygulama_log
WHERE log_tarihi < DATE_SUB(NOW(), INTERVAL 90 DAY)
  AND log_seviyesi IN ('INFO', 'DEBUG');

-- Arşivleme başarılıysa kaynak tablodan sil
DELETE FROM uygulama_log
WHERE log_tarihi < DATE_SUB(NOW(), INTERVAL 90 DAY)
  AND log_seviyesi IN ('INFO', 'DEBUG');

Bu işlemi production ortamında çalıştıracaksanız, büyük veri setleri için toplu (batch) yapın. Yoksa uzun süren bir transaction tablo üzerinde kilit oluşturur.

INSERT IGNORE ve ON DUPLICATE KEY UPDATE

Hedef tabloda unique index ya da primary key varsa, tekrar eden kayıtları eklemeye çalıştığınızda hata alırsınız. Bunu yönetmek için iki seçenek var:

INSERT IGNORE

Duplicate kayıtları sessizce atlayarak devam eder:

INSERT IGNORE INTO urun_fiyat_listesi (urun_id, fiyat, guncelleme_tarihi)
SELECT urun_id, satis_fiyati, CURDATE()
FROM gunluk_fiyat_guncellemeleri
WHERE tarih = CURDATE();

INSERT IGNORE kullanırken dikkatli olun. Sadece duplicate key hatalarını değil, bazı veri tipi hatalarını da yutabilir. Önemli verilerle çalışıyorsanız log veya ROW_COUNT() ile kaç satırın eklendiğini kontrol edin.

ON DUPLICATE KEY UPDATE

Kayıt zaten varsa güncellemek, yoksa eklemek istediğinizde kullanın:

INSERT INTO stok_durumu (urun_id, depo_id, miktar, son_guncelleme)
SELECT 
    urun_id,
    depo_id,
    SUM(miktar),
    NOW()
FROM stok_hareketleri
WHERE tarih = CURDATE()
GROUP BY urun_id, depo_id
ON DUPLICATE KEY UPDATE
    miktar = VALUES(miktar),
    son_guncelleme = VALUES(son_guncelleme);

Bu yapı UPSERT mantığıyla çalışır. Stok tablosunda o ürün ve depo kombinasyonu varsa günceller, yoksa yeni kayıt ekler. E-ticaret, lojistik gibi sistemlerde çok yaygın kullanılan bir pattern’dır.

Büyük Veri Setlerinde Batch Kopyalama

Milyonlarca satırlık tabloları tek seferde kopyalamak production ortamında tehlikelidir. Uzun süren transaction, tablo kilitleri ve aşırı bellek kullanımı gibi sorunlara yol açar. Bu durumda batch (toplu) yaklaşım benimsenmelidir:

-- Batch boyutu belirle
SET @batch_size = 10000;
SET @offset = 0;
SET @toplam_aktarilan = 0;

-- Stored procedure ile batch kopyalama
DELIMITER //
CREATE PROCEDURE batch_veri_kopyala()
BEGIN
    DECLARE devam INT DEFAULT 1;
    DECLARE aktarilan INT;
    
    WHILE devam = 1 DO
        INSERT INTO hedef_tablo (id, veri, tarih)
        SELECT id, veri, tarih
        FROM kaynak_tablo
        WHERE id > @offset
        ORDER BY id
        LIMIT 10000;
        
        SET aktarilan = ROW_COUNT();
        SET @toplam_aktarilan = @toplam_aktarilan + aktarilan;
        
        IF aktarilan < 10000 THEN
            SET devam = 0;
        END IF;
        
        -- Her batch arasında kısa bekleme (production ortamı için)
        DO SLEEP(0.1);
        
        SET @offset = @offset + aktarilan;
    END WHILE;
    
    SELECT CONCAT(@toplam_aktarilan, ' satır aktarıldı.') AS sonuc;
END //
DELIMITER ;

-- Çalıştır
CALL batch_veri_kopyala();

Bu stored procedure her turda 10.000 kayıt aktarır, aralarında 100ms bekler ve toplam aktarılan satır sayısını raporlar. Production yüküne göre batch boyutunu ve bekleme süresini ayarlayın.

Performans İpuçları

Büyük veri setleriyle çalışırken INSERT INTO SELECT performansını artırmak için şu noktalara dikkat edin:

  • İndeksleri geçici olarak devre dışı bırakın: Hedef tablo yeniyse, önce veriyi aktarıp sonra indeks oluşturmak çok daha hızlıdır.
-- İndeksleri kapat (sadece MyISAM için)
ALTER TABLE hedef_tablo DISABLE KEYS;

INSERT INTO hedef_tablo
SELECT * FROM kaynak_tablo;

-- İndeksleri aç
ALTER TABLE hedef_tablo ENABLE KEYS;
  • InnoDB için autocommit’i kapatın: Her satır için ayrı transaction yerine toplu commit yapmak ciddi hız farkı yaratır.
SET autocommit = 0;

INSERT INTO hedef_tablo
SELECT * FROM kaynak_tablo
WHERE sart = 'deger';

COMMIT;

SET autocommit = 1;
  • innodb_buffer_pool_size kontrolü: Büyük veri transferlerinde buffer pool boyutunun yeterli olduğundan emin olun. Yetersiz buffer pool, disk I/O’yu patlatar.
  • SELECT sorgusuna uygun indeks ekleyin: Kaynak tabloda WHERE koşulunda kullandığınız sütunların indeksli olduğundan emin olun. EXPLAIN ile kontrol edin:
EXPLAIN SELECT id, veri, tarih
FROM kaynak_tablo
WHERE created_at < '2023-01-01'
  AND durum = 'arsiv';

EXPLAIN çıktısında type kolonunun ALL (full table scan) yerine ref veya range olmasını hedefleyin.

Farklı Veritabanları Arasında Kopyalama

Aynı MySQL/MariaDB instance üzerindeki farklı veritabanları (schema) arasında da INSERT INTO SELECT kullanabilirsiniz. Yalnızca tablo adlarının önüne veritabanı adını eklemeniz yeterlidir:

-- production veritabanından staging'e veri kopyala
INSERT INTO staging_db.urunler (id, ad, fiyat, stok, kategori_id)
SELECT id, urun_adi, liste_fiyati, stok_adedi, kategori_id
FROM production_db.urunler
WHERE aktif = 1
  AND stok_adedi > 0;

Bu yaklaşım özellikle şu senaryolarda işe yarar:

  • Test/staging ortamı hazırlama: Production verisinin bir alt kümesini test ortamına almak
  • Çok kiracılı (multi-tenant) sistemler: Bir kiracının verilerini başka bir şemaya taşımak
  • Database sharding: Verileri mantıksal olarak birden fazla veritabanına dağıtmak

Dikkat Edilmesi Gereken Durumlar

INSERT INTO SELECT kullanırken bazı tuzaklara düşmemek için şu noktalara özen gösterin:

  • Kaynak ve hedef aynı tablo olamaz: INSERT INTO tablo SELECT * FROM tablo gibi bir kullanım tutarsız sonuçlar doğurabilir. Bu işlem için önce geçici bir tablo oluşturun.
  • AUTO_INCREMENT çakışması: Hedef tabloda AUTO_INCREMENT sütun varsa ve kaynak tablodaki ID’leri de aktarıyorsanız, mevcut en yüksek değerin üzerine çıkmamasına dikkat edin. Gerekirse ALTER TABLE hedef_tablo AUTO_INCREMENT = 1000000; ile manuel ayar yapın.
  • Foreign key kısıtlamaları: Hedef tabloda foreign key varsa, referans verilen kayıtların önce mevcut olması gerekir. Büyük migrasyon işlemlerinde FK kontrollerini geçici olarak kapatmak gerekebilir:
SET foreign_key_checks = 0;

INSERT INTO siparis_detaylari
SELECT * FROM eski_siparis_detaylari;

SET foreign_key_checks = 1;

Bu komutu kullandıktan sonra mutlaka referans bütünlüğünü manuel kontrol edin.

  • Timezone farklılıkları: DATETIME veya TIMESTAMP sütunlarında, farklı timezone ayarlı sunucular arasında veri taşırken zaman kaymalarına dikkat edin. CONVERT_TZ() fonksiyonunu gerektiğinde kullanın.
  • Karakter seti uyumsuzluğu: Kaynak ve hedef tablonun karakter seti farklıysa (örneğin biri latin1, diğeri utf8mb4) Türkçe karakter sorunları yaşayabilirsiniz. Önceden SHOW CREATE TABLE ile kontrol edin.

Sonuç

INSERT INTO SELECT, MySQL ve MariaDB ekosisteminde veri yönetiminin temel taşlarından biridir. Basit yedekleme işlemlerinden karmaşık ETL süreçlerine, log arşivlemeden veri migrasyonuna kadar geniş bir kullanım alanı vardır.

Günlük sysadmin hayatında bu komutu etkin kullanabilmek için şu alışkanlıkları edinmenizi öneririm: Her önemli INSERT INTO SELECT işlemini önce transaction içinde test edin ve ROW_COUNT() ile kaç satırın etkilendiğini doğrulayın. Production üzerinde büyük veri aktarımı yapacaksanız, batch yaklaşımını tercih edin ve işlemi yoğun olmayan saatlere planlayın. EXPLAIN planını ihmal etmeyin; kaynak sorguda full table scan yaşandığını fark etmeden milyonlarca satırlık bir işlem başlatmak ciddi sonuçlar doğurabilir.

Son olarak, veri aktarımı öncesinde mutlaka hedef tablonun yapısını SHOW CREATE TABLE ile doğrulayın, gerekirse sütun eşleşmelerini DESCRIBE çıktılarını yan yana koyarak kontrol edin. Küçük bir veri tipi uyumsuzluğu ya da charset farkı, saatler süren bir migrasyon işleminin sonunda beklenmedik sonuçlar üretebilir. Dikkatli planlama ve doğru araçlarla INSERT INTO SELECT, veritabanı yönetiminde en güvenilir silahlarınızdan biri hâline gelecektir.

Bir yanıt yazın

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