MariaDB ve MySQL’de ON DUPLICATE KEY UPDATE ile Upsert İşlemi

Veritabanı yönetiminde en sık karşılaşılan senaryolardan biri şudur: “Bu kayıt varsa güncelle, yoksa ekle.” Bu işlem için ayrı ayrı SELECT, INSERT ve UPDATE sorguları yazmak hem zahmetli hem de race condition gibi sorunlara açık bir yaklaşımdır. MySQL ve MariaDB’nin sunduğu ON DUPLICATE KEY UPDATE sözdizimi tam olarak bu sorunu tek bir atomik işlemle çözer. Upsert (update + insert) olarak da bilinen bu yöntem, üretim ortamlarında sayaçlar, önbellekler, istatistik tabloları ve senkronizasyon senaryolarında hayat kurtarır.

ON DUPLICATE KEY UPDATE Nedir?

ON DUPLICATE KEY UPDATE, bir INSERT sorgusuna eklenen ve şunu söyleyen bir uzantıdır: “Eğer eklemeye çalıştığın kayıt birincil anahtar (PRIMARY KEY) veya benzersiz indeks (UNIQUE INDEX) ihlali yaratıyorsa, INSERT yapmak yerine şu UPDATE işlemini uygula.”

Bu yaklaşımın en büyük avantajı atomikliktir. Uygulama katmanında önce SELECT, sonra koşula göre INSERT veya UPDATE yapan mantık, iki farklı istek arasında başka bir process’in araya girmesiyle tutarsız sonuçlar üretebilir. ON DUPLICATE KEY UPDATE bu riski tamamen ortadan kaldırır.

Temel sözdizimi şu şekildedir:

INSERT INTO tablo_adi (kolon1, kolon2, kolon3)
VALUES (deger1, deger2, deger3)
ON DUPLICATE KEY UPDATE
    kolon2 = VALUES(kolon2),
    kolon3 = VALUES(kolon3);

Burada VALUES(kolon_adi) ifadesi, INSERT edilmeye çalışılan ama çakışma yüzünden kullanılamayan değeri ifade eder. Yani “INSERT’te ne gönderdiysem onu kullan” demektir.

Temel Bir Örnek ile Başlayalım

Diyelim ki bir web sitesinin ziyaretçi sayacını tutan basit bir tablo var:

CREATE TABLE sayfa_istatistikleri (
    sayfa_id    INT           NOT NULL,
    tarih       DATE          NOT NULL,
    gorunum     INT           DEFAULT 0,
    tekil_ziyaret INT         DEFAULT 0,
    PRIMARY KEY (sayfa_id, tarih),
    INDEX idx_tarih (tarih)
) ENGINE=InnoDB;

Her sayfa görüntülendiğinde bu tabloya kayıt atmak istiyoruz. Eğer o gün için kayıt yoksa yeni ekle, varsa sayacı artır:

INSERT INTO sayfa_istatistikleri (sayfa_id, tarih, gorunum, tekil_ziyaret)
VALUES (42, CURDATE(), 1, 1)
ON DUPLICATE KEY UPDATE
    gorunum       = gorunum + 1,
    tekil_ziyaret = tekil_ziyaret + 1;

Bu sorgu her çalıştığında şu işi yapar:

  • Eğer bugün için sayfa_id = 42 kaydı yoksa yeni satır ekler, her iki sayaç 1 olur.
  • Eğer kayıt varsa sadece iki sayacı birer artırır, hiçbir şey eklemez.

Uygulama katmanında 3-4 satır mantık yerine tek bir SQL satırı. Güzel, değil mi?

VALUES() Fonksiyonu vs Doğrudan Değer

Upsert yazarken iki farklı yaklaşım göreceksiniz. Farkı anlamak önemli:

-- Yaklaşım 1: VALUES() fonksiyonu ile (önerilen)
INSERT INTO urunler (urun_kodu, stok_miktari, son_guncelleme)
VALUES ('SKU-1234', 50, NOW())
ON DUPLICATE KEY UPDATE
    stok_miktari   = VALUES(stok_miktari),
    son_guncelleme = VALUES(son_guncelleme);

-- Yaklaşım 2: Doğrudan değer yazarak
INSERT INTO urunler (urun_kodu, stok_miktari, son_guncelleme)
VALUES ('SKU-1234', 50, NOW())
ON DUPLICATE KEY UPDATE
    stok_miktari   = 50,
    son_guncelleme = NOW();

Yaklaşım 1 daha temiz ve esnek çünkü değeri tek yerde tanımlıyorsunuz. Çok satırlı INSERT işlemlerinde VALUES() fonksiyonu hangi satırın değerini kullanacağını otomatik çözüyor. Yaklaşım 2’de NOW() iki kez çağrılacağından teorik olarak farklı bir zaman damgası üretebilir, bu tür edge case’lerden kaçınmak için her zaman VALUES() tercih edin.

Not: MariaDB 10.3.3+ ve MySQL 8.0.19+ sürümlerinde VALUES() fonksiyonu deprecated sayılıyor ve bunun yerine alias kullanımı öneriliyor:

-- Modern sözdizimi (MariaDB 10.3.3+ / MySQL 8.0.19+)
INSERT INTO urunler (urun_kodu, stok_miktari, son_guncelleme)
VALUES ('SKU-1234', 50, NOW()) AS yeni
ON DUPLICATE KEY UPDATE
    stok_miktari   = yeni.stok_miktari,
    son_guncelleme = yeni.son_guncelleme;

Çok Satırlı Upsert İşlemleri

ON DUPLICATE KEY UPDATE‘in gerçek gücü çok satırlı INSERT işlemlerinde ortaya çıkar. Örneğin bir dış sistemden toplu ürün güncellemesi alıyorsunuz:

INSERT INTO urunler (urun_kodu, urun_adi, fiyat, kategori_id)
VALUES
    ('SKU-001', 'Laptop Pro 15',   12500.00, 5),
    ('SKU-002', 'Kablosuz Fare',    450.00, 8),
    ('SKU-003', 'USB-C Hub',        320.00, 8),
    ('SKU-004', 'Mekanik Klavye',  1200.00, 8),
    ('SKU-005', 'Webcam HD',        750.00, 9)
ON DUPLICATE KEY UPDATE
    urun_adi    = VALUES(urun_adi),
    fiyat       = VALUES(fiyat),
    kategori_id = VALUES(kategori_id);

Bu tek sorgu 5 satırı işler. Her biri için ayrı ayrı “var mı yok mu” kontrolü yapılmaz, veritabanı motoru bunu kendi içinde halleder. 5000 satırlık bir CSV import işlemi için bu performans farkı çok ciddi boyutlara ulaşır.

Etkilenen Satır Sayısı ve ROW_COUNT()

Bu noktada bir detayı paylaşmak isterim çünkü başlangıçta kafamı çok karıştırmıştı. ON DUPLICATE KEY UPDATE işleminin ardından ROW_COUNT() fonksiyonu veya bağlantı kütüphanesinin döndürdüğü “etkilenen satır sayısı” beklenenden farklı çıkabilir:

  • 1 döndürürse: Yeni satır eklendi (INSERT yapıldı).
  • 2 döndürürse: Mevcut satır güncellendi (UPDATE yapıldı).
  • 0 döndürürse: Satır zaten vardı ama UPDATE sonrasında hiçbir değer değişmedi.
INSERT INTO ayarlar (anahtar, deger)
VALUES ('max_baglanti', '100')
ON DUPLICATE KEY UPDATE
    deger = VALUES(deger);

SELECT ROW_COUNT() AS etkilenen_satir;
-- İlk çalıştırmada: 1 (INSERT)
-- İkinci çalıştırmada (aynı değerle): 0 (hiçbir şey değişmedi)
-- Farklı değerle: 2 (UPDATE)

Uygulama katmanında bu değeri kontrol ediyorsanız 0 ve 2’nin her ikisinin de “kayıt zaten vardı” anlamına geldiğini unutmayın.

Gerçek Dünya Senaryosu: Kullanıcı Oturum Takibi

Bir SaaS uygulamasında kullanıcıların son oturum bilgilerini tuttuğunuzu düşünün. Her login işleminde bu tabloyu güncellemek istiyorsunuz:

CREATE TABLE kullanici_oturum (
    kullanici_id    BIGINT       NOT NULL,
    son_giris       DATETIME     NOT NULL,
    giris_ip        VARCHAR(45)  NOT NULL,
    toplam_giris    INT          DEFAULT 1,
    son_user_agent  VARCHAR(512),
    PRIMARY KEY (kullanici_id)
) ENGINE=InnoDB;

-- Her login işleminde çağrılan sorgu
INSERT INTO kullanici_oturum
    (kullanici_id, son_giris, giris_ip, toplam_giris, son_user_agent)
VALUES
    (1001, NOW(), '85.103.45.67', 1, 'Mozilla/5.0 (Windows NT 10.0)...')
ON DUPLICATE KEY UPDATE
    son_giris      = VALUES(son_giris),
    giris_ip       = VALUES(giris_ip),
    toplam_giris   = toplam_giris + 1,
    son_user_agent = VALUES(son_user_agent);

Dikkat edin: toplam_giris için VALUES() kullanmıyoruz çünkü INSERT’te gelen değer 1, ama güncelleme sırasında mevcut değere 1 eklemek istiyoruz. Bu esneklik ON DUPLICATE KEY UPDATE‘i gerçekten güçlü kılan şeydir: UPDATE kısmında INSERT değerini de mevcut değeri de kullanabilirsiniz.

Gerçek Dünya Senaryosu: Ürün Envanter Senkronizasyonu

Bir ERP sisteminden periyodik olarak envanter verisi çektiğinizi ve bunu MariaDB’nizde tuttuğunuzu düşünün. Her senkronizasyonda hem yeni ürünler eklemeli hem de mevcut ürünleri güncellemeliyiz:

CREATE TABLE envanter (
    sku             VARCHAR(50)    NOT NULL,
    depo_kodu       VARCHAR(10)    NOT NULL,
    miktar          DECIMAL(10,2)  DEFAULT 0,
    rezerv_miktar   DECIMAL(10,2)  DEFAULT 0,
    son_sync        TIMESTAMP      DEFAULT CURRENT_TIMESTAMP,
    sync_kaynak     VARCHAR(50),
    PRIMARY KEY (sku, depo_kodu),
    INDEX idx_son_sync (son_sync)
) ENGINE=InnoDB;

-- ERP'den gelen toplu veri senkronizasyonu
INSERT INTO envanter (sku, depo_kodu, miktar, rezerv_miktar, son_sync, sync_kaynak)
VALUES
    ('P-10045', 'DEP-IST', 150.00, 20.00, NOW(), 'ERP-SYNC-v2'),
    ('P-10045', 'DEP-ANK',  45.00,  5.00, NOW(), 'ERP-SYNC-v2'),
    ('P-20871', 'DEP-IST',   0.00,  0.00, NOW(), 'ERP-SYNC-v2'),
    ('P-33412', 'DEP-IZM', 230.00, 15.00, NOW(), 'ERP-SYNC-v2')
ON DUPLICATE KEY UPDATE
    miktar        = VALUES(miktar),
    rezerv_miktar = VALUES(rezerv_miktar),
    son_sync      = VALUES(son_sync),
    sync_kaynak   = VALUES(sync_kaynak);

Bu senaryo için önemli bir optimizasyon notu: Eğer binlerce satır senkronize ediyorsanız sorguyu transaction içine alın:

START TRANSACTION;

INSERT INTO envanter (sku, depo_kodu, miktar, rezerv_miktar, son_sync, sync_kaynak)
VALUES
    ('P-10045', 'DEP-IST', 150.00, 20.00, NOW(), 'ERP-SYNC-v2'),
    ('P-10045', 'DEP-ANK',  45.00,  5.00, NOW(), 'ERP-SYNC-v2')
    -- ... binlerce satır
ON DUPLICATE KEY UPDATE
    miktar        = VALUES(miktar),
    rezerv_miktar = VALUES(rezerv_miktar),
    son_sync      = VALUES(son_sync),
    sync_kaynak   = VALUES(sync_kaynak);

COMMIT;

AUTO_INCREMENT ve Dikkat Edilmesi Gereken Bir Tuzak

ON DUPLICATE KEY UPDATE ile çalışırken AUTO_INCREMENT sütunlarında bir davranışa dikkat etmek gerekir. Her çakışmada, yani UPDATE yapıldığında bile, AUTO_INCREMENT sayacı artabilir. Bu InnoDB’nin çalışma şeklinden kaynaklanır.

CREATE TABLE log_ozet (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    log_tipi    VARCHAR(50) UNIQUE NOT NULL,
    adet        INT DEFAULT 0,
    son_tarih   DATETIME
) ENGINE=InnoDB;

-- Bu sorgu her çalıştığında AUTO_INCREMENT artabilir
-- Update yapılsa bile id sayacı boşluklar bırakabilir
INSERT INTO log_ozet (log_tipi, adet, son_tarih)
VALUES ('ERROR', 1, NOW())
ON DUPLICATE KEY UPDATE
    adet      = adet + 1,
    son_tarih = NOW();

Yüksek frekanslı upsert işlemlerinde AUTO_INCREMENT değerlerinde boşluklar oluşur. Bu genellikle sorun değildir çünkü AUTO_INCREMENT zaten boşluksuz ardışık değer garantisi vermez. Ama id değerlerinin her zaman ardışık olmasını bekleyen bir sistemle entegre çalışıyorsanız bunu göz önünde bulundurun.

REPLACE INTO ile Karşılaştırma

MariaDB ve MySQL’de upsert için bir alternatif daha var: REPLACE INTO. Ancak ikisi arasındaki fark kritiktir:

REPLACE INTO şunu yapar: Çakışma varsa önce mevcut kaydı siler, sonra yeni kaydı ekler. Bu DELETE + INSERT anlamına gelir.

ON DUPLICATE KEY UPDATE ise sadece UPDATE yapar.

Bu farkın pratikte ne anlama geldiğini şöyle açıklayabilirim:

  • REPLACE INTO kullandığınızda, tabloda updated_at gibi bir sütununuz varsa ve bunu UPDATE kısmında belirtmediyseniz, o sütun varsayılan değerine sıfırlanır. Çünkü kayıt silindi ve yeniden oluşturuldu.
  • Foreign key constraint’leriniz varsa REPLACE INTO ciddi sorunlara yol açabilir çünkü önce silme işlemi yapılır.
  • ON DUPLICATE KEY UPDATE ile yalnızca belirttiğiniz kolonları güncelliyorsunuz, diğerleri dokunulmadan kalıyor.

Genel tavsiye: Neredeyse her zaman ON DUPLICATE KEY UPDATE tercih edin. REPLACE INTO çok spesifik senaryolarda faydalı olabilir ama varsayılan seçeneğiniz olmamalı.

Koşullu Güncelleme: Her Zaman Üzerine Yazma

Bazen güncelleme yaparken “sadece daha yeni veri varsa güncelle” gibi bir mantık uygulamak istersiniz. Bunu IF veya GREATEST/LEAST fonksiyonlarıyla yapabilirsiniz:

-- Sadece yeni fiyat eskisinden farklıysa güncelle,
-- ayrıca fiyat geçmişini de kaydet
INSERT INTO fiyat_takip (urun_id, fiyat, fiyat_tarihi, guncelleme_sayisi)
VALUES (501, 299.99, NOW(), 1)
ON DUPLICATE KEY UPDATE
    onceki_fiyat    = fiyat,
    fiyat           = IF(VALUES(fiyat) != fiyat, VALUES(fiyat), fiyat),
    fiyat_tarihi    = IF(VALUES(fiyat) != fiyat, VALUES(fiyat_tarihi), fiyat_tarihi),
    guncelleme_sayisi = guncelleme_sayisi + 1;

Ya da timestamp karşılaştırmasıyla sadece daha yeni veriyi kabul eden bir yaklaşım:

-- Sadece gelen veri daha güncel ise güncelle
INSERT INTO sensor_verileri (sensor_id, olcum_degeri, olcum_zamani)
VALUES (88, 23.7, '2024-01-15 14:30:00')
ON DUPLICATE KEY UPDATE
    olcum_degeri = IF(
        VALUES(olcum_zamani) > olcum_zamani,
        VALUES(olcum_degeri),
        olcum_degeri
    ),
    olcum_zamani = GREATEST(VALUES(olcum_zamani), olcum_zamani);

Bu yaklaşım özellikle IoT ve telemetri senaryolarında, gecikmiş paketlerin eski veriyle yeni veriyi ezmesini önlemek için kullanışlıdır.

Performans İpuçları

Yüksek trafikli sistemlerde ON DUPLICATE KEY UPDATE kullanırken şunlara dikkat edin:

  • Batch büyüklüğü: Tek bir INSERT INTO … VALUES (…), (…), (…) ile gönderilen satır sayısını makul tutun. 1000-5000 satır arası genellikle iyi bir denge noktasıdır. Çok büyük batch’ler lock süresini uzatır.
  • İndeks seçimi: Sözdiziminin çalışması için tabloda mutlaka PRIMARY KEY veya UNIQUE INDEX olmalıdır. Birden fazla UNIQUE INDEX varsa her biri çakışma kontrolü için taranır, bu performansı etkiler. Gereksiz unique constraint’lerden kaçının.
  • InnoDB buffer pool: Upsert yoğun tablolarınızın sık erişilen kısımlarının buffer pool’da kalmasını sağlayın. innodb_buffer_pool_size ayarını buna göre boyutlandırın.
  • INSERT IGNORE alternatifi: Sadece “yoksa ekle, varsa hiçbir şey yapma” ihtiyacınız varsa ON DUPLICATE KEY UPDATE yerine INSERT IGNORE kullanın. Daha hafif bir işlemdir çünkü UPDATE kısmı yoktur.
-- Sadece yoksa ekle, varsa dokunma
INSERT IGNORE INTO varsayilan_ayarlar (anahtar, deger, aciklama)
VALUES
    ('tema',        'koyu',  'Arayuz temasi'),
    ('dil',         'tr',    'Sistem dili'),
    ('zaman_dilimi','Europe/Istanbul', 'Zaman dilimi');

Stored Procedure ile Upsert Sarmalama

Karmaşık upsert mantığını stored procedure içine almak, uygulama katmanını temiz tutar ve güvenlik açısından da faydalıdır:

DELIMITER //

CREATE PROCEDURE musteri_upsert(
    IN p_musteri_no     VARCHAR(20),
    IN p_ad_soyad       VARCHAR(100),
    IN p_email          VARCHAR(255),
    IN p_telefon        VARCHAR(20),
    IN p_guncelleme_ip  VARCHAR(45)
)
BEGIN
    INSERT INTO musteriler
        (musteri_no, ad_soyad, email, telefon, kayit_tarihi, son_guncelleme_ip)
    VALUES
        (p_musteri_no, p_ad_soyad, p_email, p_telefon, NOW(), p_guncelleme_ip)
    ON DUPLICATE KEY UPDATE
        ad_soyad            = VALUES(ad_soyad),
        email               = VALUES(email),
        telefon             = VALUES(telefon),
        son_guncelleme_tarihi = NOW(),
        son_guncelleme_ip   = VALUES(son_guncelleme_ip),
        guncelleme_sayisi   = guncelleme_sayisi + 1;

    -- Uygulama katmanına işlem tipini bildir
    SELECT ROW_COUNT() AS etkilenen_satir,
           CASE ROW_COUNT()
               WHEN 1 THEN 'INSERT'
               WHEN 2 THEN 'UPDATE'
               ELSE 'NO_CHANGE'
           END AS islem_tipi;
END //

DELIMITER ;

-- Kullanımı
CALL musteri_upsert(
    'MUS-2024-00145',
    'Ahmet Yilmaz',
    '[email protected]',
    '+905551234567',
    '192.168.1.50'
);

Sık Yapılan Hatalar

UNIQUE INDEX olmadan kullanmaya çalışmak: ON DUPLICATE KEY UPDATE sadece PRIMARY KEY veya UNIQUE INDEX ihlali durumunda tetiklenir. Normal indeksler veya hiç kısıtlama yoksa sorgu her zaman INSERT yapar.

UPDATE kısmında VALUES(id) kullanmak: PRIMARY KEY kolonunu ON DUPLICATE KEY UPDATE bloğunda güncellemeye çalışmak hata üretebilir veya beklenmedik sonuçlar doğurabilir. PRIMARY KEY’i asla UPDATE kısmına dahil etmeyin.

Trigger çakışmaları: Tabloda INSERT ve UPDATE trigger’ları varsa davranışı test edin. INSERT trigger’ı her zaman tetiklenmez; eğer UPDATE yapılırsa sadece BEFORE INSERT ve AFTER INSERT tetiklenir, UPDATE trigger’ları tetiklenmez. Bu bazen audit log gibi sistemlerde boşluklar yaratır.

Sonuç

ON DUPLICATE KEY UPDATE, sysadmin ve backend geliştirici olarak en sık başvurduğum SQL özelliklerinden biri. Uygulama katmanında “önce sorgula, sonra ekle ya da güncelle” mantığı yerine tek bir atomik işlemle bu ihtiyacı çözmek hem kodu temiz tutar hem de gereksiz veritabanı roundtrip’lerini ortadan kaldırır.

Pratik olarak şunu önerebilirim: İstatistik tabloları, önbellek tablolar, senkronizasyon işlemleri ve kullanıcı tercih yönetimi gibi senaryolarda bu sözdizimini refleks olarak kullanın. Sadece dikkat etmeniz gereken iki şey var: tablonuzda mutlaka PRIMARY KEY veya UNIQUE INDEX olsun, ve AUTO_INCREMENT boşluklarından rahatsız olmayın.

MariaDB 10.5+ ve MySQL 8.0+ kullanıyorsanız VALUES() fonksiyonu yerine alias sözdizimini tercih edin; bu hem daha okunaklı hem de gelecekte deprecation sorunuyla karşılaşmazsınız. Eski sürümlerle çalışıyorsanız klasik VALUES() fonksiyonu tamamen güvenli ve stabil şekilde çalışmaya devam ediyor.

Bir yanıt yazın

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