← Kursa Dön
📄 Text · 35 min

Veri Tipleri: INT, VARCHAR, TEXT, DATE, DECIMAL

Giriş — Neden Veri Tipleri Önemli?

Bir form doldurduğunu düşün: "Adınız" alanına bir metin yazarsın, "Doğum tarihiniz" alanına bir tarih girersin, "Yaşınız" alanına bir sayı. Hiç kimse yaş alanına "Merhaba" yazmaz — çünkü o alana sayı bekleniyor.

İşte veritabanındaki sütunlar da tam olarak böyle çalışır. Her sütuna bir veri tipi (data type) atarsın ve o sütun sadece o tipte veri kabul eder. Fiyat sütununa metin yazamazsın, tarih sütununa sayı giremezsin.

Veri tipleri, SQL öğrenmenin temel taşlarından biridir çünkü:

  • Tablo oluştururken her sütunun tipini belirlemelisin

  • Yanlış veri tipi seçimi performans sorunlarına yol açar

  • Yanlış veri tipi seçimi veri bütünlüğü sorunlarına yol açar

  • Doğru veri tipi, disk alanından tasarruf sağlar

Bu derste MySQL'deki tüm temel veri tiplerini öğrenecek, her birinin ne zaman kullanılacağını anlayacaksın.


Veri Tipi Kategorileri

MySQL'deki veri tiplerini üç ana kategoriye ayırabiliriz:

               Veri Tipleri
              /     |       \
         Sayısal   Metin    Tarih/Zaman
         /    \      |  \       |    \
      Tam   Ondalık  CHAR TEXT  DATE  DATETIME
      Sayı  Sayı    VARCHAR    TIME  TIMESTAMP

Şimdi her birini detaylıca inceleyelim.


1. Sayısal Veri Tipleri (Numeric Types)

Tam Sayılar (Integer Types)

Tam sayılar, ondalık kısmı olmayan sayılardır: 1, 42, -100, 0...

TipBoyutMinimum (Signed)Maksimum (Signed)Maksimum (Unsigned)
TINYINT1 byte-128127255
SMALLINT2 byte-32,76832,76765,535
MEDIUMINT3 byte-8,388,6088,388,60716,777,215
INT (veya INTEGER)4 byte-2,147,483,6482,147,483,6474,294,967,295
BIGINT8 byte-9.2 × 10¹⁸9.2 × 10¹⁸1.8 × 10¹⁹

🎯 Analoji: Veri tiplerini kutu boyutları gibi düşün. Bir zarfın içine kitap sığmaz, bir TIR'la tek bir mektup taşımak israf olur. Aynı şekilde, bir yaş bilgisini (0-150 arası) saklamak için BIGINT kullanmak gereksiz yer kaplar. TINYINT UNSIGNED (0-255) fazlasıyla yeterli.

Ne zaman hangisi?

-- Yaş, stok durumu gibi küçük sayılar
age TINYINT UNSIGNED,              -- 0-255 arası, 1 byte

-- Departman sayısı, kategori sayısı
department_id SMALLINT UNSIGNED,   -- 0-65535, 2 byte

-- Ürün ID, müşteri ID — çoğu uygulama için yeterli
customer_id INT UNSIGNED,          -- 0-4.2 milyar, 4 byte

-- Çok büyük sayılar gerektiğinde (tıklama sayısı, log ID'leri)
click_count BIGINT UNSIGNED,       -- Devasa sayılar, 8 byte

SIGNED vs UNSIGNED:

-- SIGNED (varsayılan): Negatif ve pozitif değer alabilir
temperature INT,           -- -2.1 milyar ile +2.1 milyar arası

-- UNSIGNED: Sadece pozitif değer alır ama üst limit 2 katına çıkar
product_id INT UNSIGNED,   -- 0 ile 4.2 milyar arası

⚠️ Dikkat: Primary key ve ID sütunları için UNSIGNED kullan. Negatif ID mantıksızdır ve UNSIGNED ile kullanabileceğin aralığı ikiye katlarsın.

📝 PostgreSQL Notu: PostgreSQL'de UNSIGNED yoktur. Bunun yerine SMALLINT, INTEGER, BIGINT kullanılır. SERIAL (otomatik artan) tipi de mevcuttur.

Ondalık Sayılar (Decimal/Floating-Point Types)

Para, ağırlık, oran gibi ondalıklı sayılar için kullanılır.

TipBoyutHassasiyetKullanım
FLOAT4 byte~7 basamakBilimsel hesaplamalar
DOUBLE8 byte~15 basamakDaha hassas bilimsel hesaplamalar
DECIMAL(M,D)DeğişkenTam hassasiyetPara, finans

FLOAT ve DOUBLE'ın Tuzağı:

-- ❌ FLOAT ile para hesabı — YANLIŞ!
CREATE TABLE bad_example (
    price FLOAT
);

INSERT INTO bad_example VALUES (19.99);
INSERT INTO bad_example VALUES (19.99);
INSERT INTO bad_example VALUES (19.99);

SELECT SUM(price) FROM bad_example;
-- Beklenti: 59.97
-- Gerçek sonuç: 59.970001220703125 (!!!)

Neden böyle oluyor? FLOAT ve DOUBLE sayıları ikili (binary) formatta saklar. Tıpkı 1/3'ün ondalık sistemde tam yazılamaması (0.333...) gibi, bazı ondalık sayılar ikili sistemde tam temsil edilemez. Bu da küçük yuvarlama hatalarına neden olur.

-- ✅ DECIMAL ile para hesabı — DOĞRU
CREATE TABLE good_example (
    price DECIMAL(10, 2)  -- Toplam 10 basamak, 2'si ondalık
);

INSERT INTO good_example VALUES (19.99);
INSERT INTO good_example VALUES (19.99);
INSERT INTO good_example VALUES (19.99);

SELECT SUM(price) FROM good_example;
-- Sonuç: 59.97 (TAM DOĞRU)

DECIMAL(M, D) sözdizimi:

  • M = Toplam basamak sayısı (precision)

  • D = Ondalık basamak sayısı (scale)

DECIMAL(10, 2)  -- 99999999.99'a kadar (8 tam + 2 ondalık)
DECIMAL(5, 2)   -- 999.99'a kadar (3 tam + 2 ondalık)
DECIMAL(8, 4)   -- 9999.9999'a kadar (4 tam + 4 ondalık)

⚠️ Dikkat: Para (fiyat, bakiye, tutar) için her zaman DECIMAL kullan, asla FLOAT veya DOUBLE kullanma. Kuruşluk farklar milyonlarca işlemde devasa tutarlara dönüşür.

E-ticaret veritabanımızda ondalık sayı kullanımı:

CREATE TABLE products (
    product_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,          -- Fiyat: max 99,999,999.99
    weight DECIMAL(8, 3),                    -- Ağırlık (kg): max 99999.999
    discount_percent DECIMAL(5, 2) DEFAULT 0 -- İndirim yüzdesi: max 100.00
);

2. Metin (String) Veri Tipleri

CHAR vs VARCHAR — En Kritik Ayrım

TipSabit/DeğişkenMax BoyutDepolama
CHAR(N)Sabit uzunluk255 karakterHer zaman N byte
VARCHAR(N)Değişken uzunluk65,535 karakterGerçek uzunluk + 1-2 byte

🎯 Analoji: CHAR(10) bir kutu gibidir — her zaman 10 birimlik yer kaplar, içine 3 harflik kelime koysan bile. VARCHAR(10) ise bir lastik çanta gibidir — 3 harflik kelime koyarsan 3+1 birimlik yer kaplar, 10 harflik kelime koyarsan 10+1.

-- CHAR(5) → Sabit 5 karakter
-- 'ABC' saklanır → 'ABC  ' (2 boşluk eklenir, her zaman 5 byte)
-- 'ABCDE' saklanır → 'ABCDE' (tam oturdu)

-- VARCHAR(5) → Değişken, max 5 karakter
-- 'ABC' saklanır → 'ABC' (3 byte + 1 byte uzunluk bilgisi = 4 byte)
-- 'ABCDE' saklanır → 'ABCDE' (5 byte + 1 byte = 6 byte)

Ne zaman CHAR, ne zaman VARCHAR?

-- ✅ CHAR — Uzunluğu sabit olan veriler
country_code CHAR(2),      -- 'TR', 'US', 'DE' — her zaman 2 karakter
gender CHAR(1),            -- 'M', 'F', 'X' — her zaman 1 karakter
phone_code CHAR(3),        -- '+90', '+44' — her zaman 2-3 karakter
currency_code CHAR(3),     -- 'TRY', 'USD', 'EUR' — her zaman 3 karakter

-- ✅ VARCHAR — Uzunluğu değişen veriler
first_name VARCHAR(50),    -- İsimler 2 ile 50 karakter arası değişir
email VARCHAR(100),        -- E-postalar çok farklı uzunlukta olabilir
address VARCHAR(500),      -- Adresler kısa da olabilir uzun da

💡 İpucu: Emin değilsen VARCHAR kullan. CHAR yalnızca verinin uzunluğunun gerçekten sabit olduğu durumlarda avantajlıdır (ve bu avantaj marginaldir). Modern MySQL'de VARCHAR performansı çok iyidir.

TEXT Tipleri

Uzun metinler için TEXT ailesini kullanırsın:

TipMax BoyutKullanım
TINYTEXT255 byteKısa notlar
TEXT65,535 byte (~64 KB)Açıklama, biyografi
MEDIUMTEXT16,777,215 byte (~16 MB)Makale, blog yazısı
LONGTEXT4,294,967,295 byte (~4 GB)Kitap, çok uzun dokümanlar
CREATE TABLE products (
    product_id INT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200) NOT NULL,
    short_description VARCHAR(500),      -- Kısa açıklama
    full_description TEXT,               -- Detaylı açıklama
    specifications MEDIUMTEXT            -- Teknik özellikler (uzun olabilir)
);

VARCHAR vs TEXT farkı:

ÖzellikVARCHAR(N)TEXT
Max boyut65,535 byte65,535 byte (TEXT), 16MB (MEDIUMTEXT)
IndexDoğrudan indexlenebilirPrefix index gerekir
Default değerVerilebilirMySQL'de verilemez
Bellekte depolamaSatır içindeAyrı bir alanda (overflow)
PerformansKısa metinler için daha iyiUzun metinler için uygun

⚠️ Dikkat: "Ne olur ne olmaz büyük koyayım" diye her sütunu TEXT yapma. 50 karakterlik bir isim sütununu TEXT yapmak, indexleme ve performans açısından dezavantajlıdır. Boyutu tahmin edebildiğin sütunlarda VARCHAR kullan.

ENUM ve SET

ENUM — Önceden tanımlı seçeneklerden birini seçer:

CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled') 
        DEFAULT 'pending'
);

-- ✅ Doğru — tanımlı değerlerden biri
INSERT INTO orders (status) VALUES ('shipped');

-- ❌ Hata — tanımlı olmayan değer
INSERT INTO orders (status) VALUES ('bekleniyor');
-- MySQL strict mode'da hata verir

SET — Önceden tanımlı seçeneklerden birden fazlasını seçer:

CREATE TABLE products (
    product_id INT PRIMARY KEY AUTO_INCREMENT,
    tags SET('yeni', 'indirimli', 'cok-satan', 'sinirli-stok')
);

INSERT INTO products (tags) VALUES ('yeni,indirimli');   -- İki etiket
INSERT INTO products (tags) VALUES ('cok-satan');         -- Tek etiket

💡 İpucu: ENUM sipariş durumu, cinsiyet gibi sabit ve az seçenekli durumlar için güzeldir. Ama seçenek listesi sık değişiyorsa (mesela ürün etiketleri), ENUM yerine ayrı bir referans tablosu kullanmak daha esnektir. Çünkü ENUM'a yeni değer eklemek ALTER TABLE gerektirir.


3. Tarih ve Zaman Veri Tipleri (Date/Time Types)

TipFormatAralıkBoyutKullanım
DATE'YYYY-MM-DD'1000-01-01 → 9999-12-313 byteDoğum tarihi, kayıt tarihi
TIME'HH:MM:SS'-838:59:59 → 838:59:593 byteSüre, saat bilgisi
DATETIME'YYYY-MM-DD HH:MM:SS'1000-01-01 → 9999-12-318 byteSipariş tarihi, olay zamanı
TIMESTAMP'YYYY-MM-DD HH:MM:SS'1970-01-01 → 2038-01-194 byteOluşturma/güncelleme zamanı
YEAR'YYYY'1901 → 21551 byteSadece yıl bilgisi

DATE — Sadece Tarih

CREATE TABLE customers (
    customer_id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50),
    birth_date DATE,                    -- Sadece tarih: '1990-05-15'
    registration_date DATE DEFAULT (CURRENT_DATE)  -- Bugünün tarihi
);

INSERT INTO customers (first_name, birth_date) 
VALUES ('Ahmet', '1990-05-15');

-- Tarih sorguları
SELECT * FROM customers WHERE birth_date > '1990-01-01';
SELECT * FROM customers WHERE YEAR(birth_date) = 1990;

DATETIME vs TIMESTAMP — Çok Önemli Fark!

Bu ikisi aynı formatta görünür ama davranışları çok farklıdır:

CREATE TABLE test_dates (
    id INT PRIMARY KEY AUTO_INCREMENT,
    created_datetime DATETIME,
    created_timestamp TIMESTAMP
);
ÖzellikDATETIMETIMESTAMP
Aralık1000-99991970-2038
Boyut8 byte4 byte
TimezoneTimezone bilgisi yokUTC'ye çevrilip saklanır, okunurken session timezone'a çevrilir
DefaultNULL (belirtilmezse)CURRENT_TIMESTAMP (ilk sütun için)
Auto-updateManuel ayarlanırON UPDATE CURRENT_TIMESTAMP ile otomatik

🎯 Analoji: DATETIME bir fotoğraftır — "15 Ocak 2024 saat 14:30" der ve bu bilgi hiç değişmez, hangi ülkede olursan ol aynı kalır. TIMESTAMP ise bir dünya saatidir — UTC'ye göre saklanır ve sen neredeysen o timezone'a göre gösterilir. İstanbul'daki "17:00" ile Londra'daki "14:00" aslında aynı TIMESTAMP'tır.

-- Gerçek dünya kullanımı
CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    order_date DATETIME NOT NULL,           -- Siparişin verildiği an
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,  -- Kayıt oluşturulma
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 
               ON UPDATE CURRENT_TIMESTAMP   -- Her güncelleme otomatik
);

INSERT INTO orders (order_date) VALUES ('2024-01-15 14:30:00');

-- updated_at otomatik güncellenir
UPDATE orders SET order_date = '2024-01-15 15:00:00' WHERE order_id = 1;
-- updated_at şimdi güncelleme zamanını gösterir

⚠️ Dikkat: TIMESTAMP tipi 2038 problemi taşır. 2038-01-19 03:14:07 UTC'den sonraki tarihleri saklayamaz (32-bit integer sınırı). Uzun vadeli tarih saklamak istiyorsan DATETIME kullan. MySQL 8.0.28+ sürümlerinde TIMESTAMP artık 64-bit destekli olmaya başladı ama yine de dikkatli ol.

Hangi durumda hangisi?

-- DATETIME kullan:
birth_date DATE,                -- Doğum tarihi (timezone önemsiz)
event_date DATETIME,            -- Etkinlik tarihi (belirli bir an)
invoice_date DATETIME,          -- Fatura tarihi

-- TIMESTAMP kullan:
created_at TIMESTAMP,           -- Kayıt oluşturulma zamanı
updated_at TIMESTAMP,           -- Son güncelleme zamanı
last_login TIMESTAMP,           -- Son giriş zamanı

4. Boolean Veri Tipi

MySQL'de gerçek bir BOOLEAN tipi yoktur — BOOLEAN yazarsan MySQL onu TINYINT(1)'e çevirir:

CREATE TABLE products (
    product_id INT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200),
    is_active BOOLEAN DEFAULT TRUE,     -- Aslında TINYINT(1)
    is_featured BOOLEAN DEFAULT FALSE   -- 0 = FALSE, 1 = TRUE
);

-- Değer atama
INSERT INTO products (product_name, is_active, is_featured) 
VALUES ('Laptop', TRUE, FALSE);
-- Arka planda: TRUE = 1, FALSE = 0

-- Sorgulama
SELECT * FROM products WHERE is_active = TRUE;
-- veya
SELECT * FROM products WHERE is_active = 1;
-- veya (kısa yol)
SELECT * FROM products WHERE is_active;  -- 0 olmayan her değer "truthy"

📝 PostgreSQL Notu: PostgreSQL'de gerçek bir BOOLEAN tipi vardır. TRUE, FALSE ve NULL değerlerini kabul eder. Ayrıca 'yes', 'no', 'on', 'off', '1', '0' gibi alternatif değerler de dönüştürülür.

💡 İpucu: Boolean sütun isimlendirmesinde is_, has_, can_ prefix'leri kullan: is_active, has_discount, can_return. Bu, sütunun boolean olduğunu isimden anlaşılır kılar.


5. Binary ve Blob Veri Tipleri

Binary tipler, ikili veri (resim, dosya, şifrelenmiş veri) saklamak için kullanılır:

TipMax BoyutKullanım
BINARY(N)255 byteSabit uzunluklu binary (UUID, hash)
VARBINARY(N)65,535 byteDeğişken uzunluklu binary
TINYBLOB255 byteÇok küçük binary veri
BLOB65 KBİkon, küçük resim
MEDIUMBLOB16 MBResim, döküman
LONGBLOB4 GBVideo, büyük dosya
-- UUID binary olarak saklamak (16 byte — VARCHAR(36)'dan verimli)
CREATE TABLE sessions (
    session_id BINARY(16) PRIMARY KEY,
    user_id INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Şifre hash'i saklamak
CREATE TABLE users (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    password_hash VARBINARY(255) NOT NULL
);

⚠️ Dikkat: Resim ve dosyaları veritabanında saklamak genellikle iyi bir fikir değildir. Veritabanı büyür, backup'lar yavaşlar, performans düşer. En iyi pratik: dosyaları dosya sisteminde veya cloud storage'da (S3, GCS) sakla, veritabanında sadece dosya yolunu (path) tut.


6. JSON Veri Tipi

MySQL 5.7'den itibaren JSON veri tipi destekleniyor:

CREATE TABLE products (
    product_id INT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200),
    specifications JSON
);

INSERT INTO products (product_name, specifications) VALUES (
    'iPhone 15',
    '{"color": "blue", "storage": "256GB", "ram": "6GB", "screen": "6.1 inch"}'
);

-- JSON içinden veri çekme
SELECT product_name,
       JSON_EXTRACT(specifications, '$.color') AS color,
       specifications->>'$.storage' AS storage  -- Kısa yol operatörü
FROM products;

💡 İpucu: JSON tipi esnek yapılar için kullanışlıdır — ürün özellikleri gibi her ürünün farklı alanları olabilecek durumlar. Ama ilişkisel veritabanında her şeyi JSON'a tıkmak, ilişkisel modelin avantajlarından vazgeçmek demektir. JSON'ı ek veri için kullan, ana veri için normal sütunlar tercih et.


Veri Tipi Seçim Rehberi

Bir tablo tasarlarken hangi veri tipini seçeceğine karar vermek için şu tabloyu kullan:

VeriÖnerilen TipNeden
ID (primary key)INT UNSIGNED AUTO_INCREMENTHızlı, kompakt, otomatik artan
Ad, soyadVARCHAR(50)Değişken uzunluk, makul sınır
E-postaVARCHAR(100)RFC standardı max 254 karakter ama 100 pratikte yeter
TelefonVARCHAR(20)Ülke kodu dahil, farklı formatlar
Para (fiyat, tutar)DECIMAL(10,2)Tam hassasiyet, kuruş desteği
Adet, miktarINT UNSIGNEDNegatif adet olmaz
Açıklama (kısa)VARCHAR(500)Index desteği
Açıklama (uzun)TEXT64KB'a kadar
Doğum tarihiDATESadece tarih yeterli
Sipariş tarihiDATETIMESaat de önemli
Kayıt zamanıTIMESTAMPAuto-update, timezone-aware
Evet/HayırBOOLEAN (TINYINT(1))is_active, has_discount
DurumENUM(...)Belirli seçeneklerden biri
Ülke koduCHAR(2)Sabit uzunluk, ISO 3166-1
IP adresiVARCHAR(45)IPv6 desteği (max 45 karakter)

Gerçek Dünya Örneği — E-Ticaret Tabloları

Öğrendiğimiz veri tiplerini kullanarak e-ticaret tablolarımızı tasarlayalım:

-- Kategoriler
CREATE TABLE categories (
    category_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    category_name VARCHAR(100) NOT NULL,
    parent_category_id INT UNSIGNED NULL,
    description TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Ürünler
CREATE TABLE products (
    product_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200) NOT NULL,
    category_id INT UNSIGNED,
    price DECIMAL(10, 2) NOT NULL,
    compare_price DECIMAL(10, 2),          -- Karşılaştırma fiyatı (üzeri çizili)
    cost_price DECIMAL(10, 2),             -- Maliyet fiyatı
    stock_quantity INT UNSIGNED DEFAULT 0,
    sku VARCHAR(50) UNIQUE,                -- Stok kodu
    weight DECIMAL(8, 3),                  -- Ağırlık (kg)
    is_active BOOLEAN DEFAULT TRUE,
    is_featured BOOLEAN DEFAULT FALSE,
    short_description VARCHAR(500),
    full_description TEXT,
    specifications JSON,                   -- Esnek özellikler
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Müşteriler
CREATE TABLE customers (
    customer_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    phone VARCHAR(20),
    birth_date DATE,
    gender ENUM('male', 'female', 'other'),
    city VARCHAR(50),
    address TEXT,
    postal_code VARCHAR(10),
    is_active BOOLEAN DEFAULT TRUE,
    registration_date DATE DEFAULT (CURRENT_DATE),
    last_login TIMESTAMP NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Siparişler
CREATE TABLE orders (
    order_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    customer_id INT UNSIGNED NOT NULL,
    order_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0,
    discount_amount DECIMAL(10, 2) DEFAULT 0,
    shipping_cost DECIMAL(8, 2) DEFAULT 0,
    status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled') 
        DEFAULT 'pending',
    shipping_address TEXT,
    notes TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- Sipariş kalemleri
CREATE TABLE order_items (
    item_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    order_id INT UNSIGNED NOT NULL,
    product_id INT UNSIGNED NOT NULL,
    quantity INT UNSIGNED NOT NULL DEFAULT 1,
    unit_price DECIMAL(10, 2) NOT NULL,
    discount_percent DECIMAL(5, 2) DEFAULT 0,
    line_total DECIMAL(10, 2) GENERATED ALWAYS AS 
        (quantity * unit_price * (1 - discount_percent / 100)) STORED
);

Bu tablolarda dikkat edilmesi gereken noktalar:

  • Para sütunlarında DECIMAL kullandık

  • ID'lerde INT UNSIGNED AUTO_INCREMENT kullandık

  • Sabit değer listelerinde ENUM kullandık

  • Zaman damgalarında TIMESTAMP ve ON UPDATE kullandık

  • Boolean sütunlara is_ prefix'i koyduk

  • line_total bir generated column — otomatik hesaplanıyor


VARCHAR(N) — N Değerini Nasıl Seçerim?

Bu, yeni başlayanların en çok sorduğu sorulardan biri. N değeri fazla büyük olursa yer israfı olur mu?

Kısa cevap: VARCHAR(255) ile VARCHAR(50) arasında disk kullanımı farkı yoktur — çünkü VARCHAR sadece gerçek veriyi saklar. Ama bazı incelikler var:

  1. Bellek kullanımı: MySQL bazı durumlarda (temporary table, sort buffer) VARCHAR(N)'in N değerine göre bellek ayırır. Bu yüzden VARCHAR(10000) gereksiz yere bellek tüketebilir.

  2. Dokümantasyon: VARCHAR(50) yazmak "bu sütuna 50 karakterden uzun veri gelmez" demektir — kodunuzu okuyanlar için değerli bir bilgidir.

  3. Doğrulama: Veritabanı seviyesinde otomatik uzunluk kontrolü sağlar.

Pratik kurallar:

  • İsim, soyad: VARCHAR(50)

  • E-posta: VARCHAR(100) (ya da VARCHAR(255) — ama 100 pratikte yeter)

  • Başlık: VARCHAR(200)

  • URL: VARCHAR(2048) (tarayıcı limiti)

  • Kısa açıklama: VARCHAR(500)

  • Belirsiz ama kısa metin: VARCHAR(255) (geleneksel güvenli değer)


Tür Dönüşümü (Type Casting) — Kısa Bir Ön Bakış

MySQL bazı durumlarda otomatik tür dönüşümü yapar, bazılarında sen CAST() veya CONVERT() kullanırsın. Bu konuyu ilerideki derslerde detaylıca işleyeceğiz ama temel bir örnek görelim:

-- Otomatik dönüşüm (implicit casting)
SELECT '10' + 5;  -- Sonuç: 15 (MySQL '10'u sayıya çevirdi)

-- Manuel dönüşüm (explicit casting)  
SELECT CAST('2024-01-15' AS DATE);
SELECT CAST(19.99 AS SIGNED);  -- 20 (yuvarlama)

⚠️ Dikkat: Otomatik tür dönüşümüne güvenme. WHERE price = '19.99' çalışır (MySQL string'i DECIMAL'a çevirir) ama WHERE phone = 5551234 yazmak beklenmedik sonuçlar doğurabilir. Veri tipine uygun değer kullan.


Sıkça Yapılan Hatalar

  1. Para için FLOAT kullanmak: Bu dersin en önemli mesajı: para, fiyat, tutar, bakiye gibi değerler için DECIMAL kullan. FLOAT yuvarlama hataları yapar ve bu hatalar birikerek ciddi sorunlara yol açar.

  2. Her şeyi VARCHAR(255) yapmak: Tembel yaklaşım. Her sütunun mantıklı bir sınırı olmalı. İsim alanını VARCHAR(255) yapmak teknik olarak çalışır ama VARCHAR(50) çok daha uygun ve anlamlı.

  3. Tarih bilgisini VARCHAR'da saklamak: VARCHAR(10)'a '2024-01-15' yazmak çalışır ama tarih karşılaştırma, tarih hesaplama yapamaz. DATE tipini kullan — veritabanı motoru tarih işlemlerini çok daha verimli yapar.

  4. Telefon numarasını INT yapmak: Telefon numaraları sayı değildir — başında sıfır olabilir (+905551234567), tire veya boşluk içerebilir. VARCHAR(20) kullan.

  5. UNSIGNED'ı unutmak: Negatif olması mümkün olmayan değerler (ID, stok, adet) için UNSIGNED kullan. Hem daha güvenli, hem daha geniş aralık.

  6. TIMESTAMP'in 2038 limitini bilmemek: Uzun vadeli tarihleri TIMESTAMP'ta saklamak 2038'de sorun çıkarır. Tarihler DATETIME, sadece otomatik oluşturulma/güncelleme zamanları TIMESTAMP olsun.


Özet

  • Sayısal tipler: Tam sayılar için INT ailesi, para için DECIMAL, bilimsel hesap için FLOAT/DOUBLE

  • Metin tipleri: Kısa ve sınırlı metinler için VARCHAR, uzun metinler için TEXT, sabit uzunluk için CHAR

  • Tarih tipleri: Sadece tarih için DATE, tarih+saat için DATETIME, otomatik zaman damgası için TIMESTAMP

  • Boolean: BOOLEAN (aslında TINYINT(1))

  • ENUM: Sabit seçenek listesi

  • JSON: Esnek, yarı-yapılandırılmış veri

  • Para, fiyat, tutar → her zaman DECIMAL

  • Telefon, posta kodu → her zaman VARCHAR (sayı değil!)

  • Primary key → INT UNSIGNED AUTO_INCREMENT

  • Doğru veri tipi seçimi performans, güvenlik ve veri bütünlüğü açısından kritiktir