Veritabanı Yönetimi

    UUID mi Auto Increment mi: Primary Key Seçimi

    Birincil anahtar olarak UUID ile otomatik artan sayı arasındaki gerçek performans farkı.

    11 dk okuma Güncellendi: 25 Ağustos 2026

    UUID mi auto increment mi sorusu, şema tasarımında en çok tartışılan ama en az ölçülen konulardan biridir. Bir tarafta "UUID modern, dağıtık sistemlerde şart, kimlik tahmin edilemez" savunusu, diğer tarafta "UUID indeksi şişirir, ekleme performansını öldürür" karşı çıkışı vardır. İkisi de kısmen doğrudur ve doğru cevap hangi tarafta durduğunuza değil, hangi iş yükünü çalıştırdığınıza bağlıdır.

    Bu rehberde iki yaklaşımı somut sayılarla karşılaştıracağım: indeks boyutu farkı nereden geliyor, sayfa bölünmesi neden ekleme hızını düşürüyor, UUID'yi CHAR(36) yerine BINARY(16) saklamak neyi değiştiriyor ve zaman sıralı UUID varyantları bu problemi ne kadar çözüyor. Sonunda da hangi senaryoda hangisini seçmeniz gerektiğini net bir karar tablosuna bağlayacağım.

    İki Yaklaşımın Temel Farkı#

    AUTO_INCREMENT, veritabanının tuttuğu tek bir sayaçtan sırayla değer üretir. Bu sayaç merkezîdir: değeri alabilmek için veritabanına yazmanız gerekir, yani kimliği ancak INSERT sırasında öğrenirsiniz. UUID ise 128 bitlik, evrensel olarak benzersiz olduğu varsayılan bir değerdir ve uygulama tarafında, veritabanına hiç danışmadan üretilebilir.

    Bu tek fark, geri kalan her şeyi belirler:

    ÖlçütAUTO_INCREMENTUUID
    Üretim yeriVeritabanıUygulama (veya veritabanı)
    Genişlik4 veya 8 bayt16 bayt (ikili), 36 bayt (metin)
    SıralılıkArtanv4'te rastgele, v7'de zaman sıralı
    Çakışma riskiÇok sunucuda varPratikte yok
    Tahmin edilebilirlikYüksekYok
    Ekleme öncesi kimlikAlınamazAlınabilir
    İş bilgisi sızdırmaKayıt sayısını ele verirVermez

    "Ekleme öncesi kimlik alınabilir" satırı sanıldığından önemlidir. Bir sipariş ve ona bağlı yirmi kalemi tek seferde yazmak istiyorsanız, AUTO_INCREMENT ile önce siparişi eklemek, üretilen kimliği okumak ve sonra kalemleri yazmak zorundasınız. UUID ile tüm kimlikleri bellekte üretip hepsini tek toplu işlemde gönderebilirsiniz. Dağıtık sistemlerde ve mesaj kuyruğu tabanlı mimarilerde bu, ciddi bir sadeleşmedir.

    Auto Increment: Güçlü ve Zayıf Yanları#

    AUTO_INCREMENT dar, artan ve okunabilir. InnoDB'nin kümelenmiş indeks yapısıyla mükemmel uyuşur: her yeni satır B-ağacının sağ ucuna eklenir, sayfa bölünmesi neredeyse hiç yaşanmaz, tablo düzenli büyür. İkincil indeksler birincil anahtarın kopyasını taşıdığı için 4 baytlık bir anahtar indeksleri de olabildiğince küçük tutar.

    Zayıf yanları da net:

    Tahmin edilebilirlik. /fatura/1042 adresini gören biri /fatura/1043 deneyecektir. Yetkilendirme kontrolü sunucu tarafında doğru yapılıyorsa bu bir güvenlik açığı değildir; ama kimlikler yine de iş bilgisi sızdırır. Bir rakip, bir ay arayla iki sipariş verip kimlikler arasındaki farka bakarak aylık sipariş hacminizi hesaplayabilir.

    Çok kaynaklı yazmada çakışma. İki sunucu bağımsız olarak yazıyorsa aynı sayıyı üretebilirler. Klasik çözüm, sunuculara farklı başlangıç ve artış değerleri vermektir:

    -- Sunucu A: 1, 3, 5, 7...  |  Sunucu B: 2, 4, 6, 8...
    SET GLOBAL auto_increment_offset    = 1;
    SET GLOBAL auto_increment_increment = 2;
    

    Bu yöntem iki sunucu için çalışır ama üçüncüyü eklediğinizde tüm şemayı yeniden planlamanız gerekir. Çok kaynaklı yazma senaryolarını MySQL replikasyon kurulumu yazısında ayrıca ele alıyorum.

    Boşluklar. AUTO_INCREMENT sayaç değerleri geri alınmaz; başarısız bir INSERT ya da geri alınan bir işlem sayıyı yakar. Kimlikler bu yüzden asla "kesintisiz sıra" garantisi vermez ve fatura numarası gibi yasal olarak ardışık olması gereken alanlar için doğrudan kullanılmamalıdır.

    -- Bir sonraki değeri görmek için
    SHOW TABLE STATUS LIKE 'siparisler';
    -- Auto_increment sütunu bir sonraki üretilecek değeri gösterir
    

    UUID: Hangi Sürüm, Nasıl Saklanır#

    UUID'nin birkaç sürümü vardır ve seçim doğrudan performansı etkiler. En yaygını sürüm 4'tür: neredeyse tamamen rastgele bitlerden oluşur, hiçbir sıralama taşımaz. Sürüm 1 zaman damgası içerir ama alanları öyle diziler ki metin olarak sıralandığında kronolojik sıra vermez. Daha yeni sürüm 7 ise zaman damgasını en anlamlı bitlere koyar, dolayısıyla üretildiği sırayla artar; bu, aşağıda anlatacağım sayfa bölünmesi problemini büyük ölçüde ortadan kaldırır.

    Saklama biçimi ise en kritik karardır. UUID'yi metin olarak saklamak yaygın ama pahalı bir alışkanlıktır:

    -- Kötü: 36 bayt, üstelik tireleri de saklıyor
    kimlik CHAR(36) NOT NULL
    
    -- İyi: 16 bayt, yarı yarıdan fazla tasarruf
    kimlik BINARY(16) NOT NULL
    

    MySQL 8, dönüşüm için yerleşik fonksiyonlar sunar:

    CREATE TABLE belgeler (
      id        BINARY(16) NOT NULL,
      baslik    VARCHAR(200) NOT NULL,
      olusturuldu DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (id)
    ) ENGINE=InnoDB;
    
    -- Yazarken metni ikiliye çevir; ikinci parametre 1 ise
    -- v1 UUID'nin zaman alanları takas edilerek SIRALI hâle getirilir
    INSERT INTO belgeler (id, baslik)
    VALUES (UUID_TO_BIN(UUID(), 1), 'Teklif metni');
    
    -- Okurken geri metne çevir
    SELECT BIN_TO_UUID(id, 1) AS id, baslik FROM belgeler;
    

    PostgreSQL'de durum daha basittir: yerleşik bir uuid tipi vardır ve zaten 16 bayt saklar, ayrıca dönüşüm yapmanıza gerek kalmaz.

    CREATE TABLE belgeler (
      id     uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      baslik varchar(200) NOT NULL
    );
    

    İki motorun tip sistemleri arasındaki diğer farklar için MySQL ve PostgreSQL karşılaştırması yazısına bakabilirsiniz.

    Performans: Sayfa Bölünmesi ve İndeks Boyutu#

    UUID'nin performans itirazı iki gerçek olguya dayanır ve ikisini ayrı ayrı anlamak gerekir.

    Birincisi indeks boyutu. InnoDB'de her ikincil indeks yaprağı birincil anahtarın kopyasını taşır. Dört ikincil indeksi olan 10 milyon satırlık bir tabloda birincil anahtar 8 baytlık BIGINT yerine 16 baytlık BINARY(16) ise, sadece bu kopyalar için satır başına 32 bayt fazladan yük gelir; kabaca 320 MB ek indeks demektir. CHAR(36) kullanıyorsanız fark satır başına 112 bayta, yani gigabayt seviyesine çıkar. Tampon havuzuna sığmayan indeks, disk okuması demektir.

    İkincisi sayfa bölünmesi. Artan bir anahtarla her yeni satır ağacın en sağındaki sayfaya eklenir; sayfa dolunca yenisi açılır ve doluluk oranı yüksek kalır. Rastgele bir anahtarla ise her ekleme ağacın rastgele bir yerine düşer; hedef sayfa doluysa ikiye bölünür ve iki yarım dolu sayfa oluşur. Tablo zamanla parçalanır, aynı veri daha fazla sayfaya yayılır ve okuma da yavaşlar.

    Anahtar biçimiBayt (PK)Ekleme davranışıİkincil indeks yükü
    INT AUTO_INCREMENT4Sağ uca ekler, bölünme yokEn düşük
    BIGINT AUTO_INCREMENT8Sağ uca ekler, bölünme yokDüşük
    UUIDv7 / sıralı UUID, BINARY(16)16Neredeyse sıralı, bölünme azOrta
    UUIDv4, BINARY(16)16Rastgele, bölünme yoğunOrta-yüksek
    UUIDv4, CHAR(36)36Rastgele, bölünme yoğunEn yüksek

    Tablonun ne kadar parçalandığını kabaca ölçebilirsiniz:

    -- data_free yüksekse tablo içinde boşluk birikmiş demektir
    SELECT table_name,
           ROUND(data_length/1024/1024, 1)  AS veri_mb,
           ROUND(index_length/1024/1024, 1) AS indeks_mb,
           ROUND(data_free/1024/1024, 1)    AS bos_mb
    FROM   information_schema.tables
    WHERE  table_schema = DATABASE()
    ORDER  BY data_free DESC;
    

    Buradaki kritik nokta şudur: sorunun kaynağı UUID'nin kendisi değil, rastgeleliğidir. Zaman sıralı bir UUID kullanırsanız sayfa bölünmesi problemi büyük ölçüde kaybolur ve geriye yalnızca 16 bayt genişlik maliyeti kalır ki bu çoğu uygulama için kabul edilebilir bir bedeldir.

    Karma Yaklaşımlar ve Alternatifler#

    İki uçtan birini seçmek zorunda değilsiniz. Pratikte çok işe yarayan birkaç ara yol vardır.

    İçeride sayı, dışarıda opak kimlik. Birincil anahtar ve tüm yabancı anahtarlar BIGINT AUTO_INCREMENT kalır; tabloya ek olarak bir public_id BINARY(16) sütunu eklenir ve dış dünyaya yalnızca bu gösterilir. Böylece indeks performansını kaybetmeden numaraların tahmin edilmesini de engellersiniz.

    CREATE TABLE faturalar (
      id         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      public_id  BINARY(16) NOT NULL,
      musteri_id INT UNSIGNED NOT NULL,
      tutar      DECIMAL(12,2) NOT NULL,
      PRIMARY KEY (id),
      UNIQUE KEY uq_public_id (public_id),   -- dış aramalar bu indeksten
      KEY idx_musteri (musteri_id)
    ) ENGINE=InnoDB;
    

    ULID. 128 bitlik, zaman sıralı ve metin biçimi 26 karakterlik büyük harf tabanlı bir tanımlayıcıdır. Sıralılığı sayesinde ekleme davranışı UUIDv7'ye benzer, metin hâli UUID'den kısa ve URL dostudur. Veritabanında yine ikili olarak 16 bayt saklanabilir.

    Snowflake tarzı kimlikler. Zaman damgası, düğüm kimliği ve sayaçtan oluşan 64 bitlik tam sayılardır. BIGINT genişliğinde kalırlar, artan sıradadırlar ve dağıtık üretime izin verirler; karşılığında bir kimlik üretim servisi ya da her düğüme benzersiz numara atama disiplini gerektirirler.

    Kimlik sütununun tipini seçerken genel veri tipi ölçütlerini de gözden geçirmek isterseniz doğru veri tipi seçimi rehberi tamamlayıcı olacaktır.

    Hangi Durumda Ne Seçmelisiniz#

    Kararı somutlaştıralım:

    SenaryoTercihGerekçe
    Tek veritabanı, klasik web uygulamasıBIGINT AUTO_INCREMENTEn dar indeks, en hızlı ekleme
    Kimlikler URL'de görünüyorAUTO_INCREMENT + opak public idPerformans korunur, sızıntı olmaz
    Çok kaynaklı yazma, parçalama (sharding)Sıralı UUID veya SnowflakeMerkezî sayaç mümkün değil
    Çevrimdışı istemci, sonra eşitlemeUUIDKimlik istemcide üretilmeli
    Mikroservisler arası veri taşımaUUIDÇakışma ve yeniden numaralandırma yok
    Yüksek hacimli günlük/olay tablosuBIGINT AUTO_INCREMENTEkleme hızı ve boyut kritik
    Kimlik dış sisteme ihraç edilecekUUIDSistemler arası benzersizlik

    Basit bir başlangıç kuralı: veritabanınız tekse ve kimlikleri dışarı vermeniz gerekmiyorsa AUTO_INCREMENT kullanın. Bu ikisinden biri değişiyorsa, sıralı bir UUID varyantına geçin. UUIDv4'ü birincil anahtar yapmak ise yalnızca gerçekten rastgeleliğe ihtiyaç duyduğunuz özel durumlarda mantıklıdır.

    Sık Yapılan Hatalar#

    UUID'yi CHAR(36) saklamak. En yaygın ve en pahalı hata budur. 36 bayt yerine BINARY(16) kullanmak indeks boyutunu yarıdan fazla düşürür, üstelik dönüşüm fonksiyonları MySQL 8'de hazır gelir.

    UUIDv4'ü kümelenmiş anahtar yapıp performans sorununa şaşırmak. Rastgele anahtar, InnoDB'nin en sevmediği ekleme desenidir. Sıralı bir varyanta geçmek çoğu zaman kodda tek satırlık bir değişikliktir.

    "UUID kullanıyorum, yetkilendirmeye gerek yok" sanmak. Tahmin edilemez kimlik bir yetkilendirme mekanizması değildir; kimliği ele geçiren biri kaydı yine okur. Sunucu tarafında sahiplik kontrolü her hâlükârda yapılmalıdır.

    Anahtar tipini tablolar arasında karıştırmak. Ana tabloda BINARY(16), bağlı tabloda CHAR(36) kullanmak yabancı anahtar kısıtı kurmanızı engeller. Anahtar tipi, yabancı anahtar sütunlarında birebir aynı olmalıdır; bu konuyu primary key ve foreign key tasarımı yazısında ayrıntılandırdım.

    AUTO_INCREMENT boşluklarını hata sanmak. Geri alınan işlemler sayacı yakar, bu normaldir ve düzeltmeye çalışmak gereksizdir. Kesintisiz sıra gerekiyorsa (fatura numarası gibi) bunu ayrı bir sütunda ve ayrı bir mantıkla üretin.

    Milyonlarca satırlık tabloda anahtar tipini sonradan değiştirmek. Bu işlem tablonun ve tüm yabancı anahtarların yeniden yazılmasını gerektirir; saatler sürer ve uzun kilitlenme yaratır. Kararı baştan vermek çok daha ucuzdur.

    Sıkça Sorulan Sorular#

    UUID gerçekten auto increment'ten yavaş mı#

    Rastgele UUID (v4) belirgin biçimde daha yavaş ekleme yapar, çünkü her satır indeks ağacının rastgele bir noktasına düşer ve sayfa bölünmesine yol açar. Zaman sıralı bir UUID (v7 veya sıralı hâle getirilmiş v1) kullanır ve BINARY(16) saklarsanız fark çoğu iş yükünde ölçülemeyecek kadar küçülür. Yani "UUID yavaştır" ifadesi eksiktir; yavaş olan rastgelelik ile metin depolamadır.

    UUID'yi CHAR(36) yerine BINARY(16) saklamak ne kazandırır#

    Doğrudan 20 bayt tasarruf sağlar, ama asıl kazanç bu değildir: InnoDB'de her ikincil indeks birincil anahtarın kopyasını taşıdığı için tasarruf indeks sayısıyla çarpılır. Dört ikincil indeksi olan bir tabloda satır başına 100 baytın üzerinde fark oluşur. Daha küçük indeks, tampon havuzuna daha çok veri sığması ve daha az disk okuması demektir.

    Auto increment kimlikler güvenlik açığı mı#

    Kendi başlarına açık değildirler; asıl açık, yetkilendirme kontrolünü atlayıp kimliğe güvenmektir. Ancak sıralı kimlikler iş bilgisi sızdırır: iki kayıt arasındaki farka bakarak günlük veya aylık işlem hacminizi tahmin etmek mümkündür. Bu bilgiyi gizlemek istiyorsanız dahili anahtarı sayı olarak tutup dışarıya opak bir kimlik göstermek en verimli yoldur.

    UUID çakışma ihtimali var mı#

    Pratikte yok denecek kadar düşüktür; v4 için 122 bit rastgelelik, doğru bir rastgele sayı kaynağı kullanıldığında çakışmayı gerçekçi olmayan bir ihtimale indirir. Asıl risk zayıf rastgele kaynağıdır: kriptografik olmayan bir üreteç, aynı anda başlayan çok sayıda süreçte benzer değerler üretebilir. Dil ve kütüphane seçerken kriptografik güvenli üreteç kullandığından emin olun.

    Var olan tabloda auto increment'ten UUID'ye nasıl geçerim#

    En güvenli yol kademeli geçiştir: önce tabloya UNIQUE kısıtlı yeni bir UUID sütunu ekleyin, mevcut satırları toplu güncellemeyle doldurun, uygulamayı bu sütunu okuyacak şekilde güncelleyin ve ancak her şey oturduktan sonra birincil anahtarı taşımayı değerlendirin. Milyonlarca satırlık tablolarda anahtar değişimi tüm yabancı anahtarları da etkilediği için bakım penceresi ve tam yedek şarttır. Çoğu durumda birincil anahtarı hiç değiştirmeden opak kimlik sütunuyla yaşamak yeterli olur.

    Hangi UUID sürümünü kullanmalıyım#

    Veritabanı anahtarı olarak kullanacaksanız zaman sıralı bir sürüm tercih edin; bu, ekleme sırasında sayfa bölünmesini en aza indirir. MySQL 8 kullanıyorsanız UUID_TO_BIN(UUID(), 1) ile üretilen değerler zaten sıralı hâle gelir. Tamamen rastgeleliğin gerektiği durumlarda (tahmin edilemez oturum jetonu gibi) v4 doğru tercihtir, ama o değer birincil anahtar olmak zorunda değildir.

    Küçük bir projede bu karar gerçekten önemli mi#

    Birkaç bin satırlık tablolarda ölçülebilir bir fark görmezsiniz; her iki seçenek de sorunsuz çalışır. Kararın önemi tablo büyüdükçe ve indeks sayısı arttıkça ortaya çıkar, üstelik sonradan değiştirmek pahalıdır. Bu yüzden küçük projelerde bile varsayılanınızı bilinçli seçmek, ileride ücretsiz bir sigorta olur.

    Kapanış#

    Bu tartışmada taraf tutmak yerine iş yükünüze bakmak en doğru yaklaşımdır. Aklınızda kalması gereken dört şey: tek veritabanlı klasik uygulamalarda BIGINT AUTO_INCREMENT hâlâ en verimli seçim, UUID gerekiyorsa mutlaka BINARY(16) ve zaman sıralı bir varyant kullanın, dış dünyaya kimlik gösterme ihtiyacı için dahili anahtarı değiştirmek yerine ayrı bir opak sütun ekleyin, ve hangi seçeneği kullanırsanız kullanın yetkilendirmeyi kimliğin gizliliğine dayandırmayın.

    Anahtar tipi değişikliği gibi büyük şema müdahaleleri, üzerinde özgürce çalışabileceğiniz bir sunucu ve sağlam bir yedek gerektirir. Tam yetkili VDS ve bulut sunucu paketlerimizle kendi MySQL veya PostgreSQL ayarlarınızı yapabilir, geçiş öncesi güvenlik için yedekleme hizmetimizden yararlanabilir, planlama ve uygulama desteğini sunucu yönetimi hizmetimize bırakabilirsiniz.

    Şema TasarımıInnoDBPerformans

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.