Veritabanı Yönetimi

    Doğru Veri Tipi Seçimi: INT, VARCHAR ve DECIMAL

    Tam sayı, metin, ondalık ve tarih tiplerini doğru seçmenin performans ve doğruluk etkisi.

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

    Veri tipi seçimi, şema tasarımının en sessiz ama en uzun ömürlü kararıdır. Bir sütunu BIGINT yerine INT yapmak bugün hiçbir fark yaratmaz gibi görünür; tablo 80 milyon satıra ulaştığında ise aynı karar gigabaytlarca disk, birkaç kat daha küçük indeks ve gözle görülür bir sorgu hızı farkı anlamına gelir. Tersi de doğrudur: fiyatı FLOAT olarak tanımladığınız bir tabloda toplamlar kuruş kuruş kaymaya başladığında sorun artık performans değil, muhasebedir.

    Bu rehberde tam sayı, metin, ondalık, tarih ve mantıksal tipleri hangi ölçütle seçeceğinizi anlatacağım. Her tipin gerçek disk maliyetini, sınır değerlerini, indekslenebilirliğini ve MySQL ile PostgreSQL arasındaki farkları tablolar üzerinden göstereceğim. Sonunda da "ne olur ne olmaz" diye her sütunu VARCHAR(255) yapan yaklaşımın neye mal olduğunu somut biçimde göreceksiniz.

    Veri Tipi Seçimi Neden Performansı Doğrudan Etkiler#

    Veritabanı motorları veriyi disk üzerinde sabit boyutlu sayfalar hâlinde tutar; InnoDB'de bu sayfa varsayılan olarak 16 KB'dır. Bir satır ne kadar küçükse tek sayfaya o kadar çok satır sığar, aynı sorgu için o kadar az sayfa okunur ve bellekteki tampon havuzuna (buffer pool) o kadar çok veri sığar. Yani veri tipi seçimi doğrudan "kaç disk okuması yapıyorum" sorusunun cevabıdır.

    Etki indekslerde daha da belirgindir. InnoDB'de ikincil indeksler birincil anahtarın kopyasını içinde taşır; birincil anahtarınız 4 baytlık bir INT yerine 36 karakterlik bir metinse, her ikincil indeks satır başına onlarca fazladan bayt taşır. Beş ikincil indeksi olan bir tabloda bu fark çarpılarak büyür. Somutlaştıralım: 50 milyon satırlık bir tabloda bir sütunu BIGINTten INTe çekmek 8 bayt yerine 4 bayt demektir, yani sadece o sütun için yaklaşık 200 MB tasarruf; aynı sütun üç indekste geçiyorsa tasarruf katlanır.

    Tam Sayılar: TINYINT'ten BIGINT'e#

    MySQL beş tam sayı tipi sunar ve aralarındaki tek fark kapladıkları bayt sayısıdır. Seçim kuralı basittir: değerin ulaşabileceği en yüksek sınırı düşünün ve o sınırı karşılayan en küçük tipi seçin.

    TipBoyutİşaretli aralıkİşaretsiz üst sınırTipik kullanım
    TINYINT1 bayt-128 … 127255Durum kodu, yaş, puan
    SMALLINT2 bayt-32.768 … 32.76765.535Şehir/ülke kodu, stok adedi
    MEDIUMINT3 bayt±8,3 milyon16.777.215Orta ölçekli kayıt sayacı
    INT4 bayt±2,1 milyar4.294.967.295Çoğu tablonun birincil anahtarı
    BIGINT8 bayt±9,2 kentilyonÇok yüksekLog tabloları, para (kuruş cinsinden)

    Sütun asla negatif değer almayacaksa UNSIGNED ekleyin; bu, aynı bayt sayısıyla üst sınırı iki katına çıkarır ve aynı zamanda "burada negatif olamaz" kuralını veritabanı düzeyinde uygulamış olursunuz.

    -- Kullanıcı tablosu: id asla negatif olmayacak
    CREATE TABLE kullanicilar (
      id            INT UNSIGNED NOT NULL AUTO_INCREMENT,
      durum         TINYINT UNSIGNED NOT NULL DEFAULT 1,  -- 0 pasif, 1 aktif, 2 askıda
      giris_sayisi  MEDIUMINT UNSIGNED NOT NULL DEFAULT 0,
      PRIMARY KEY (id)
    ) ENGINE=InnoDB;
    

    Burada sık karşılaşılan bir yanlış anlamayı da düzeltelim: INT(11) yazmak sütunu 11 haneli yapmaz. Parantez içindeki sayı yalnızca eski istemcilerin çıktıyı hizalaması için kullanılan bir görüntüleme genişliğidir, depolamayı hiç etkilemez ve MySQL 8 ile birlikte kullanımdan kaldırılmıştır. INT(4) ile INT(11) diskte birebir aynı 4 baytı kaplar.

    Metin Alanları: CHAR, VARCHAR ve TEXT#

    Metin tarafında üç seçenek vardır ve aralarındaki fark, uzunluğun sabit mi değişken mi olduğu ve verinin satır içinde mi saklandığıdır.

    CHAR(n) sabit uzunluktadır: CHAR(10) alanına "abc" yazarsanız kalan yedi karakter boşlukla doldurulur ve okurken bu boşluklar kırpılır. Uzunluğu gerçekten sabit olan alanlar için idealdir: ülke kodu (CHAR(2)), para birimi kodu (CHAR(3)), MD5 karması (CHAR(32)). VARCHAR(n) ise yalnızca kullanılan kadar yer kaplar, üzerine 1 veya 2 baytlık bir uzunluk öneki ekler (255 bayta kadar 1, üstünde 2 bayt). TEXT ailesi ise büyük metinler içindir ve verinin bir kısmı satır dışına, ayrı sayfalara taşınabilir.

    TipAzami boyutSatır içinde miVarsayılan değerİndeks
    CHAR(n)255 karakterEvetVerilebilirTam indekslenebilir
    VARCHAR(n)Satır sınırına kadarEvetVerilebilirUzunsa önek gerekir
    TINYTEXT255 baytKısmenMySQL 8.0.13 öncesi hayırÖnek gerekir
    TEXT64 KBHayırMySQL 8.0.13 öncesi hayırÖnek gerekir
    MEDIUMTEXT16 MBHayırAynıÖnek gerekir
    LONGTEXT4 GBHayırAynıÖnek gerekir

    Kritik nokta karakter setidir. utf8mb4 ile bir karakter 4 bayta kadar çıkabilir, dolayısıyla VARCHAR(255) en kötü durumda 1020 bayt yer ayırabilir. InnoDB'de bir indeks anahtarı DYNAMIC satır biçiminde en fazla 3072 bayt olabilir; bu da utf8mb4 ile indekslenebilir azami metin uzunluğunun kabaca 768 karakter olduğu anlamına gelir. Bir e-posta sütununu VARCHAR(255) yapmak sorun değildir, ama iki metin sütununu birleştirip tek indekse koyarsanız bu sınıra çarpabilirsiniz.

    -- İyi tasarlanmış bir metin şeması
    CREATE TABLE musteriler (
      id          INT UNSIGNED NOT NULL AUTO_INCREMENT,
      ulke_kodu   CHAR(2) NOT NULL,              -- her zaman 2 karakter
      eposta      VARCHAR(190) NOT NULL,         -- utf8mb4 ile güvenle indekslenir
      ad_soyad    VARCHAR(120) NOT NULL,
      notlar      TEXT NULL,                     -- uzun ve nadiren okunan alan
      PRIMARY KEY (id),
      UNIQUE KEY uq_eposta (eposta)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    

    Sık okunan sütunlarla nadiren okunan uzun metinleri ayırmak da bir tasarım tercihidir; sütunları hangi tabloya koyacağınızın kuralını veritabanı normalizasyonu rehberinde ayrıntılı bulabilirsiniz.

    Para ve Ondalık: DECIMAL, FLOAT ve DOUBLE#

    Bu bölüm rehberin en önemli kısmı olabilir, çünkü buradaki hata sessizce para kaybettirir. FLOAT ve DOUBLE ikili (binary) kayan noktalı sayılardır; 0,1 gibi ondalık değerler ikilik tabanda tam olarak gösterilemez, dolayısıyla saklanan değer istediğiniz değerin çok yakınıdır ama birebir aynısı değildir. Bir hesapta bu fark görünmez, on binlerce satır toplandığında görünür hâle gelir.

    CREATE TABLE fiyat_testi (
      f FLOAT(10,2),
      d DECIMAL(10,2)
    );
    INSERT INTO fiyat_testi VALUES (0.1, 0.1), (0.2, 0.2);
    
    -- FLOAT sütunu beklenen 0.30 yerine kayan bir sonuç üretebilir,
    -- DECIMAL sütunu her zaman 0.30 döner.
    SELECT SUM(f), SUM(d) FROM fiyat_testi;
    

    Kural nettir: para, vergi oranı, stok miktarı gibi kesinlik gerektiren her şey DECIMAL olmalıdır. DECIMAL(p, s) biçiminde p toplam basamak sayısı, s ondalık basamak sayısıdır. Türk lirası tutarları için DECIMAL(12, 2) çoğu senaryoyu rahatça karşılar; kripto veya döviz kuru gibi daha hassas alanlar için DECIMAL(18, 8) kullanılır.

    FLOAT ve DOUBLE'ın meşru kullanım alanları da vardır: koordinat, sensör ölçümü, bilimsel hesaplama, makine öğrenmesi skoru gibi zaten yaklaşık olan büyüklükler. Buralarda küçük boyut ve hızlı aritmetik avantajdır.

    TipBoyutKesinlikDoğru kullanım
    DECIMAL(12,2)DeğişkenTamFiyat, tutar, bakiye
    FLOAT4 bayt~7 anlamlı basamakSensör, koordinat
    DOUBLE8 bayt~15 anlamlı basamakBilimsel hesap
    BIGINT (kuruş)8 baytTamYüksek hacimli finans

    Son satırdaki yaklaşım da yaygındır: tutarı 19.90 yerine 1990 olarak, yani kuruş cinsinden tam sayı saklamak. Bu yöntem tamamen kesindir ve aritmetiği hızlıdır; karşılığında her okuma ve yazmada 100'e bölüp çarpmayı uygulama katmanında hatırlamanız gerekir.

    Tarih ve Saat Tipleri#

    Tarih tarafında yapılan en yaygın hata, tarihi metin olarak saklamaktır. VARCHAR(20) içinde tutulan bir tarih, aralık sorgularında indeks kullanamaz, sıralaması alfabetiktir ve zaman dilimi kavramı yoktur. Doğru tipler şunlardır:

    TipBoyutAralıkZaman dilimi çevirisi
    DATE3 bayt1000-01-01 … 9999-12-31Yok
    DATETIME5 bayt1000-01-01 … 9999-12-31Yok, ne yazdıysanız o
    TIMESTAMP4 bayt1970 … 2038Var, UTC'ye çevrilir
    TIME3 baytSüre değeriYok
    YEAR1 bayt1901 … 2155Yok

    DATETIME ile TIMESTAMP arasındaki fark, bilmeden seçilirse en çok baş ağrıtan konudur. TIMESTAMP yazarken oturumun zaman diliminden UTC'ye çevrilir, okurken geri çevrilir; sunucunun zaman dilimi değişirse eski kayıtların görünen değeri de değişir. DATETIME ise ne yazdıysanız onu saklar, hiçbir çeviri yapmaz. Uluslararası bir uygulamada genellikle en sağlam yöntem, sunucuyu UTC'de çalıştırmak ve DATETIME kullanarak çeviriyi uygulama katmanında yapmaktır. Ayrıca TIMESTAMP'ın 2038 sınırı, uzun vadeli tarih tutan alanlar (sözleşme bitişi, doğum tarihi) için doğrudan diskalifiye edici bir kısıttır.

    CREATE TABLE siparisler (
      id             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      olusturuldu    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      guncellendi    DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      teslim_tarihi  DATE NULL,          -- saat bilgisi gereksiz
      tutar          DECIMAL(12,2) NOT NULL,
      PRIMARY KEY (id),
      KEY idx_olusturuldu (olusturuldu)  -- tarih aralığı sorguları için
    ) ENGINE=InnoDB;
    

    MySQL ve PostgreSQL Arasındaki Farklar#

    İki motor arasında geçiş yapıyorsanız veri tipi eşlemesi ilk tökezleyeceğiniz yerdir. PostgreSQL'de UNSIGNED diye bir kavram yoktur; negatif olmamasını istiyorsanız CHECK (deger >= 0) kısıtı eklersiniz. Yine PostgreSQL'de varchar ile text arasında performans farkı yoktur, ikisi de aynı depolama mekanizmasını kullanır; uzunluk sınırı sadece bir kısıttır. MySQL'de ise TEXT gerçekten farklı davranır.

    İhtiyaçMySQLPostgreSQL
    Otomatik artan anahtarINT UNSIGNED AUTO_INCREMENTINT GENERATED ALWAYS AS IDENTITY
    Sınırsız metinLONGTEXTtext
    Kesin ondalıkDECIMAL(12,2)numeric(12,2)
    Zaman dilimli anYok, DATETIME + UTCtimestamptz
    MantıksalTINYINT(1)boolean
    JSONJSONjsonb (indekslenebilir)
    Sabit seçenek listesiENUM(...)CREATE TYPE ... AS ENUM

    PostgreSQL'in timestamptz tipi, MySQL'de karşılığı olmayan gerçek bir avantajdır: değeri UTC olarak saklar ama tipin kendisi zaman dilimi farkındalığını taşır. İki motorun genel karşılaştırması için MySQL ve PostgreSQL karşılaştırması yazısına bakabilirsiniz.

    Sık Yapılan Hatalar#

    Her sütunu VARCHAR(255) yapmak. Bu, "hangisi olacağını bilmiyorum" demenin şemadaki karşılığıdır. Sonuç: geçici tablolar bellekte değil diskte oluşur (MySQL geçici tablolarda VARCHAR alanını azami uzunluğuyla ayırabilir), indeksler şişer, üstelik veri doğrulaması da kaybolur. Alanın gerçek azami uzunluğunu düşünüp yazın.

    Boole değerini metin olarak saklamak. 'evet' / 'hayir' ya da 'true' / 'false' yerine MySQL'de TINYINT(1), PostgreSQL'de boolean kullanın. Metin karşılaştırması hem yavaştır hem de büyük/küçük harf ve yazım varyasyonu üretir.

    Telefon numarasını sayı olarak saklamak. Telefon bir sayı değil, bir tanımlayıcı dizisidir: başındaki sıfır kaybolur, artı işareti ve uluslararası önek saklanamaz. Doğru tip VARCHAR(20)'dir.

    Para için FLOAT kullanmak. Yukarıda ayrıntısını verdim; bu hata muhasebe tutarsızlığı olarak geri döner ve genellikle aylar sonra fark edilir.

    Sonradan büyütürüm diye BIGINT'i her yere koymak. Gerçekten 2,1 milyarı geçecek bir sayaç mı? Kullanıcı tablosunda büyük ihtimalle hayır; ancak günlük (log) veya olay tablosunda büyük ihtimalle evet. Kararı tabloya göre verin, kopyala-yapıştır alışkanlığıyla değil. Birincil anahtar tipini seçerken UUID mi auto increment mi karşılaştırması da işinize yarar.

    Tipi değiştirmenin bedava olduğunu sanmak. Büyük bir tabloda ALTER TABLE çalıştırmak tablonun yeniden yazılmasına ve saatler süren kilitlenmeye yol açabilir. Doğru tipi baştan seçmek, sonradan düzeltmekten kat kat ucuzdur.

    Sıkça Sorulan Sorular#

    VARCHAR mı TEXT mi kullanmalıyım#

    Metin belirli ve makul bir uzunluk sınırına sahipse VARCHAR kullanın; bu alan satır içinde saklanır, doğrudan indekslenebilir ve varsayılan değer alabilir. Uzunluk gerçekten öngörülemiyorsa ve alan nadiren okunuyorsa (yorum gövdesi, ürün açıklaması) TEXT doğru seçimdir. Pratik ölçüt şudur: alanı WHERE veya ORDER BY içinde kullanacaksanız VARCHAR, sadece gösterecekseniz TEXT.

    INT(11) ile INT(4) arasında fark var mı#

    Depolama açısından hiçbir fark yoktur, ikisi de 4 bayttır ve aynı değer aralığını kabul eder. Parantez içindeki sayı yalnızca bazı eski istemcilerin çıktıyı hizalarken kullandığı bir görüntüleme genişliğidir. MySQL 8 ile bu gösterim kullanımdan kaldırıldığı için yeni şemalarda parantez yazmadan sadece INT demeniz yeterlidir.

    Para alanı için hangi tipi seçmeliyim#

    Kesinlik gerektiği için DECIMAL kullanın; Türk lirası tutarları için DECIMAL(12,2) çoğu uygulamayı fazlasıyla karşılar. FLOAT veya DOUBLE yaklaşık tiplerdir ve toplamlarda kuruş sapmaları üretir. Çok yüksek işlem hacminiz varsa alternatif olarak tutarı kuruş cinsinden BIGINT saklayabilirsiniz, ancak bu durumda dönüşümü uygulama katmanında tutarlı biçimde yapmanız gerekir.

    DATETIME mi TIMESTAMP mı kullanmalıyım#

    TIMESTAMP 4 bayt yer kaplar ve zaman dilimi çevirisi yapar, ancak 2038 yılı sınırı vardır. DATETIME 5 bayttır, çeviri yapmaz ve 9999 yılına kadar gider. Genel tavsiye, sunucuyu UTC'de çalıştırıp DATETIME kullanmak ve zaman dilimi dönüşümünü uygulamada yapmaktır; ileri tarihli alanlarda ise TIMESTAMP zaten kullanılamaz.

    utf8mb4 kullanmak yer israfı mı#

    Hayır. utf8mb4 yalnızca gerçekten 4 bayt gereken karakterlerde 4 bayt kullanır; Latin harfleri hâlâ 1 bayttır. Karşılığında emoji ve tam Unicode desteği elde edersiniz. Dikkat etmeniz gereken tek şey indeks uzunluğudur: azami anahtar boyutu bayt cinsinden hesaplandığı için çok uzun metin sütunlarını indekslerken önek uzunluğu belirtmeniz gerekebilir.

    Veri tipini sonradan değiştirmek ne kadar sürer#

    Küçük tablolarda saniyeler, milyonlarca satırlık tablolarda saatler sürebilir; süre satır sayısına, disk hızına ve MySQL sürümünün o tip değişikliğini yerinde (ALGORITHM=INPLACE) yapıp yapamamasına bağlıdır. Tablonun yeniden yazılması gereken durumlarda üretim ortamında doğrudan çalıştırmak yerine bakım penceresi planlayın ya da çevrimiçi şema değişikliği araçları kullanın. Her hâlükârda önce yedek alın.

    Sütuna NOT NULL koymak performansı etkiler mi#

    Etkisi küçüktür ama olumludur: NULL kabul eden her sütun için satır başına bir bit ek yer ayrılır ve sorgu planlayıcı NULL ihtimalini de hesaba katmak zorunda kalır. Asıl kazanç doğruluk tarafındadır; NOT NULL kısıtı eksik veri girişini veritabanı düzeyinde engeller. Bir alan gerçekten "bilinmiyor" değerini taşıyabiliyorsa NULL bırakın, aksi hâlde NOT NULL varsayılanınız olsun.

    Kapanış#

    Veri tipi seçimi, şemanın geri dönüşü en pahalı kararıdır ve iyi haber, kuralların basit olmasıdır: değeri karşılayan en küçük tipi seçin, parayı her zaman DECIMAL ile saklayın, tarihi metin olarak tutmayın ve VARCHAR(255) alışkanlığını bırakıp alanın gerçek uzunluğunu düşünün. Bir de tipleri baştan doğru koymanın, sonradan ALTER TABLE ile düzeltmekten çok daha ucuz olduğunu unutmayın.

    Şemanızı kurarken veritabanı sunucunuzun kaynaklarını da doğru boyutlandırmak gerekir; tam yetkiyle kendi MySQL veya PostgreSQL ayarlarınızı yapmak isterseniz VDS ve bulut sunucu paketlerimiz uygun bir zemin sunar. Şema tasarımı ve optimizasyonunu bize bırakmak isterseniz sunucu yönetimi hizmetimiz bu işi de kapsıyor; büyüyen bir mağaza altyapısı için e-ticaret hosting paketleri hazır ayarlarla gelir.

    Şema TasarımıMySQLPostgreSQL

    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.