← Kursa Dön
📄 Text · 35 min

Tablo Oluşturma: CREATE TABLE ve Constraints

Giriş — Veritabanının İskeleti

Önceki derste veritabanımızı oluşturduk — artık boş bir bina var. Bu derste o binanın odalarını oluşturacağız: tablolar. Tablolar, veritabanının temel yapı taşlarıdır. İyi tasarlanmış tablolar, uygulamanın sağlıklı çalışmasının temelidir.

Bu derste CREATE TABLE komutunun tüm detaylarını, constraints (kısıtlamalar) kavramını ve e-ticaret veritabanımızın tablolarını birlikte oluşturacağız.

🎯 Analoji: Bir Excel dosyası açıp sütun başlıkları yazarken her sütuna "bu sütunda ne tür veri olacak" diye düşünürsün. CREATE TABLE da aynı şeyi yapar ama çok daha güçlü kurallar koyabilirsin: "Bu sütun boş kalamaz", "Bu değer benzersiz olmalı", "Bu sütun başka tabloya referans veriyor" gibi.


CREATE TABLE — Temel Sözdizimi

CREATE TABLE tablo_adi (
    sutun1_adi VERI_TIPI [KISITLAMALAR],
    sutun2_adi VERI_TIPI [KISITLAMALAR],
    ...
    [TABLO_SEVIYESI_KISITLAMALAR]
);

İlk Tablomuz — En Basit Hali

USE e_commerce;

CREATE TABLE categories (
    category_id INT,
    category_name VARCHAR(100)
);

Bu tablo çalışır ama çok eksik. Hiçbir kısıtlama (constraint) yok — herhangi bir değer girebilirsin, tekrar eden değer girebilirsin, hatta boş bile bırakabilirsin. Gerçek dünyada buna izin vermeyiz.

Kısıtlamalarla Birlikte

DROP TABLE IF EXISTS categories;

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
);

Şimdi bu tabloda ciddi kurallar var. Her bir constraint'i detaylıca inceleyelim.


Constraints (Kısıtlamalar) — Verinin Koruyucu Kalkanı

Constraint'ler, tabloya girilen verilerin belirli kurallara uymasını zorunlu kılar. Veritabanı seviyesinde veri bütünlüğünü korumanın en güçlü yoludur.

🎯 Analoji: Online bir form dolduruyor olduğunu düşün. "E-posta" alanına "merhaba" yazınca form kırmızı uyarı veriyor: "Geçerli bir e-posta girin." İşte constraint'ler veritabanı seviyesinde aynı işi yapar — ama form doğrulamasından çok daha güvenilirdir çünkü uygulamayı atlasan bile veritabanı veriyi reddeder.

1. PRIMARY KEY — Birincil Anahtar

Her tabloda, her satırı benzersiz şekilde tanımlayan bir sütun (veya sütun kombinasyonu) olmalıdır:

-- Sütun seviyesinde PRIMARY KEY
CREATE TABLE products (
    product_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200) NOT NULL
);

-- Tablo seviyesinde PRIMARY KEY (aynı şey, farklı yazım)
CREATE TABLE products (
    product_id INT UNSIGNED AUTO_INCREMENT,
    product_name VARCHAR(200) NOT NULL,
    PRIMARY KEY (product_id)
);

PRIMARY KEY kuralları:

  • Değer benzersiz olmalı — aynı değer iki kez olamaz

  • NULL olamaz — her satırda bir değer olmalı

  • Tabloda tek bir primary key olabilir

  • Otomatik olarak bir index oluşturulur (performans için)

Bileşik Primary Key (Composite Primary Key):

Bazen tek bir sütun yetmez — birden fazla sütunun kombinasyonu benzersiz olabilir:

CREATE TABLE order_items (
    order_id INT UNSIGNED,
    product_id INT UNSIGNED,
    quantity INT UNSIGNED NOT NULL DEFAULT 1,
    unit_price DECIMAL(10,2) NOT NULL,
    PRIMARY KEY (order_id, product_id)  -- İkisi birlikte benzersiz
);

-- Bu durumda:
-- (101, 5) → ✅ İlk kayıt
-- (101, 8) → ✅ Farklı ürün, aynı sipariş
-- (102, 5) → ✅ Aynı ürün, farklı sipariş
-- (101, 5) → ❌ HATA! Bu kombinasyon zaten var

💡 İpucu: Composite primary key yerine genellikle surrogate key (yapay anahtar) tercih edilir — yani item_id INT AUTO_INCREMENT gibi ayrı bir ID sütunu ekleyip onu primary key yapmak. Daha pratik, JOIN'lerde daha kolay ve ORM'lerle daha uyumlu.

AUTO_INCREMENT — Otomatik Artan Değer

CREATE TABLE customers (
    customer_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL
);

-- ID belirtmeden ekleme — otomatik artar
INSERT INTO customers (first_name) VALUES ('Ahmet');   -- customer_id = 1
INSERT INTO customers (first_name) VALUES ('Zeynep');  -- customer_id = 2
INSERT INTO customers (first_name) VALUES ('Mehmet');  -- customer_id = 3

-- ID 2'yi silsek bile, sonraki kayıt 4 olur (3'e geri dönmez!)
DELETE FROM customers WHERE customer_id = 2;
INSERT INTO customers (first_name) VALUES ('Ayşe');    -- customer_id = 4 (2 değil!)

AUTO_INCREMENT davranışı:

  • Her yeni kayıtta otomatik 1 artar

  • Silinen değerler tekrar kullanılmaz

  • Son değeri görmek: SELECT LAST_INSERT_ID();

  • Başlangıç değerini ayarlamak: AUTO_INCREMENT = 1000

-- 1000'den başlat
CREATE TABLE invoices (
    invoice_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    amount DECIMAL(10,2)
) AUTO_INCREMENT = 1000;

INSERT INTO invoices (amount) VALUES (599.99);
-- invoice_id = 1000

📝 PostgreSQL Notu: PostgreSQL'de AUTO_INCREMENT yerine SERIAL veya GENERATED ALWAYS AS IDENTITY kullanılır: ``sql -- PostgreSQL CREATE TABLE customers ( customer_id SERIAL PRIMARY KEY, -- veya customer_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY ); ``

2. NOT NULL — Boş Bırakılamaz

CREATE TABLE customers (
    customer_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,   -- Boş bırakılamaz
    last_name VARCHAR(50) NOT NULL,    -- Boş bırakılamaz
    email VARCHAR(100) NOT NULL,       -- Boş bırakılamaz
    phone VARCHAR(20),                 -- NULL olabilir (NOT NULL yok)
    city VARCHAR(50)                   -- NULL olabilir
);

-- ✅ Başarılı
INSERT INTO customers (first_name, last_name, email) 
VALUES ('Ahmet', 'Yılmaz', 'ahmet@email.com');

-- ❌ HATA — first_name NOT NULL ama değer verilmedi
INSERT INTO customers (last_name, email) 
VALUES ('Kaya', 'zeynep@email.com');
-- ERROR: Column 'first_name' cannot be null

Ne zaman NOT NULL kullanmalı?

  • Mutlaka olması gereken veriler: isim, e-posta, fiyat, sipariş tarihi

  • İş mantığı gereği zorunlu olan alanlar

Ne zaman NULL'a izin vermeli?

  • Opsiyonel bilgiler: telefon, ikinci adres, doğum tarihi

  • Henüz bilinmeyen veriler: kargo tarihi (sipariş verildi ama henüz kargoya verilmedi)

3. UNIQUE — Benzersizlik Kısıtlaması

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  -- Benzersiz VE boş olamaz
);

-- ✅ Başarılı
INSERT INTO customers (first_name, last_name, email) 
VALUES ('Ahmet', 'Yılmaz', 'ahmet@email.com');

-- ❌ HATA — aynı e-posta zaten var
INSERT INTO customers (first_name, last_name, email) 
VALUES ('Ali', 'Demir', 'ahmet@email.com');
-- ERROR: Duplicate entry 'ahmet@email.com' for key 'email'

PRIMARY KEY vs UNIQUE farkı:

ÖzellikPRIMARY KEYUNIQUE
NULL olabilir miHayırEvet (bir kez)
Tabloda kaç taneSadece 1Birden fazla
Index oluşturur muEvet (clustered)Evet (non-clustered)
AmacıSatırı tanımlamakTekrarı önlemek
-- Birden fazla sütun birlikte unique
CREATE TABLE employees (
    employee_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE,
    department_id INT UNSIGNED,
    -- İsim + departman kombinasyonu unique olsun
    UNIQUE KEY unique_name_dept (first_name, last_name, department_id)
);

4. DEFAULT — Varsayılan Değer

CREATE TABLE products (
    product_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock_quantity INT UNSIGNED DEFAULT 0,        -- Varsayılan: 0
    is_active BOOLEAN DEFAULT TRUE,                -- Varsayılan: TRUE
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Varsayılan: şu anki zaman
);

-- stock_quantity ve is_active belirtilmezse varsayılan değer kullanılır
INSERT INTO products (product_name, price) 
VALUES ('Kablosuz Mouse', 299.99);
-- stock_quantity = 0, is_active = TRUE, created_at = şu an

DEFAULT ile kullanılabilecek değerler:

  • Sabit değer: DEFAULT 0, DEFAULT 'pending', DEFAULT TRUE

  • Fonksiyon: DEFAULT CURRENT_TIMESTAMP, DEFAULT (CURRENT_DATE)

  • Expression (MySQL 8.0+): DEFAULT (UUID()), DEFAULT (price * 1.18)

5. CHECK — Değer Kontrolü

CREATE TABLE products (
    product_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    stock_quantity INT UNSIGNED DEFAULT 0,
    discount_percent DECIMAL(5,2) DEFAULT 0,
    
    -- CHECK kısıtlamaları
    CHECK (price > 0),                         -- Fiyat pozitif olmalı
    CHECK (discount_percent BETWEEN 0 AND 100), -- İndirim %0-100 arası
    CHECK (stock_quantity >= 0)                 -- Stok negatif olamaz
);

-- ✅ Başarılı
INSERT INTO products (product_name, price, discount_percent) 
VALUES ('Laptop', 15999.99, 10);

-- ❌ HATA — negatif fiyat
INSERT INTO products (product_name, price) 
VALUES ('Hatalı Ürün', -50);
-- ERROR: Check constraint 'products_chk_1' is violated

-- ❌ HATA — %100'den fazla indirim
INSERT INTO products (product_name, price, discount_percent) 
VALUES ('Bedava Ürün', 100, 150);
-- ERROR: Check constraint is violated

⚠️ Dikkat: MySQL 8.0.16 öncesinde CHECK constraint'i kabul edilir ama uygulanmaz (parse eder ama kontrol etmez). MySQL 8.0.16+ sürümünde düzgün çalışır. Versiyon kontrolü önemli!

📝 PostgreSQL Notu: PostgreSQL'de CHECK constraint'leri baştan beri tam desteklenir ve çok daha güçlüdür.

6. FOREIGN KEY — Yabancı Anahtar

Tablolar arası ilişkiyi kurar ve referans bütünlüğünü korur:

-- Önce referans verilen tablo (parent)
CREATE TABLE categories (
    category_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    category_name VARCHAR(100) NOT NULL
);

-- Sonra referans veren tablo (child)
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,
    
    -- Foreign key tanımı
    FOREIGN KEY (category_id) REFERENCES categories(category_id)
);

-- Önce kategori ekle
INSERT INTO categories (category_name) VALUES ('Elektronik');  -- ID: 1
INSERT INTO categories (category_name) VALUES ('Giyim');       -- ID: 2

-- ✅ Başarılı — category_id = 1 categories tablosunda var
INSERT INTO products (product_name, category_id, price) 
VALUES ('Laptop', 1, 15999.99);

-- ❌ HATA — category_id = 99 categories tablosunda yok
INSERT INTO products (product_name, category_id, price) 
VALUES ('Hayalet Ürün', 99, 100);
-- ERROR: Cannot add or update a child row: a foreign key constraint fails

ON DELETE ve ON UPDATE davranışları:

Parent tablodaki satır silindiğinde veya güncellendiğinde child tabloda ne olacağını belirler:

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,
    
    FOREIGN KEY (category_id) REFERENCES categories(category_id)
        ON DELETE SET NULL      -- Kategori silinirse, ürünün category_id'si NULL olsun
        ON UPDATE CASCADE       -- Kategori ID'si değişirse, ürünlerde de değişsin
);
SeçenekON DELETE davranışıON UPDATE davranışı
RESTRICT (varsayılan)Silmeye izin vermezGüncellemeye izin vermez
CASCADEChild satırları da silerChild'daki referansı günceller
SET NULLChild'daki FK'yı NULL yaparChild'daki FK'yı NULL yapar
SET DEFAULTVarsayılan değere ayarlarVarsayılan değere ayarlar
NO ACTIONRESTRICT ile aynı (MySQL'de)RESTRICT ile aynı
-- RESTRICT (varsayılan) — En güvenli
-- Siparişi olan müşteriyi silmeye çalışırsan HATA alırsın
CREATE TABLE orders (
    order_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    customer_id INT UNSIGNED NOT NULL,
    FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
        ON DELETE RESTRICT
);

-- CASCADE — Tehlikeli ama bazen gerekli
-- Siparişi silince, sipariş kalemlerini de otomatik sil
CREATE TABLE order_items (
    item_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    order_id INT UNSIGNED NOT NULL,
    FOREIGN KEY (order_id) REFERENCES orders(order_id)
        ON DELETE CASCADE
);

-- SET NULL — Orta yol
-- Kategorisi silinince, ürün kategorisiz kalsın (NULL)
CREATE TABLE products (
    product_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    category_id INT UNSIGNED,
    FOREIGN KEY (category_id) REFERENCES categories(category_id)
        ON DELETE SET NULL
);

⚠️ Dikkat: ON DELETE CASCADE çok güçlü ve tehlikeli bir özelliktir. Bir parent satırı sildiğinde otomatik olarak tüm ilişkili child satırlar da silinir. Eğer zincirleme foreign key'ler varsa, tek bir DELETE komutu yüzlerce satırı silebilir. Bu yüzden genellikle RESTRICT tercih edilir ve silme işlemi uygulama katmanında kontrollü yapılır.


IF NOT EXISTS — Güvenli Tablo Oluşturma

-- Tablo zaten varsa hata vermez
CREATE TABLE IF NOT EXISTS categories (
    category_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    category_name VARCHAR(100) NOT NULL
);

Bu özellik, script'leri tekrar tekrar çalıştırabilmen için önemlidir.


Constraint İsimlendirme

Constraint'lere isim vermek debug ve bakım açısından çok değerlidir:

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,
    discount_percent DECIMAL(5,2) DEFAULT 0,
    
    -- İsimlendirilmiş constraint'ler
    CONSTRAINT fk_products_category 
        FOREIGN KEY (category_id) REFERENCES categories(category_id)
        ON DELETE SET NULL,
    
    CONSTRAINT chk_products_price 
        CHECK (price > 0),
    
    CONSTRAINT chk_products_discount 
        CHECK (discount_percent BETWEEN 0 AND 100)
);

İsim vermenin faydaları:

  • Hata mesajlarında anlamlı isim görürsün: "Constraint 'fk_products_category' violated" vs "Constraint 'products_ibfk_1' violated"

  • ALTER TABLE ... DROP CONSTRAINT isim ile kolayca kaldırabilirsin

  • Kodun okunabilirliği artar

İsimlendirme kuralları (convention):

  • Foreign key: fk_tablo_referansfk_products_category

  • Check: chk_tablo_kuralchk_products_price

  • Unique: uq_tablo_sutunuq_customers_email


Tablo Oluşturma Sırası — Foreign Key Dikkat Noktası

Foreign key kullanan tablolarda oluşturma sırası önemlidir. Referans verilen tablo önce oluşturulmalıdır:

-- ❌ YANLIŞ SIRA — products, categories'e referans veriyor ama categories henüz yok
CREATE TABLE products (
    product_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    category_id INT UNSIGNED,
    FOREIGN KEY (category_id) REFERENCES categories(category_id)
);
-- ERROR: References a table that doesn't exist

CREATE TABLE categories (
    category_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    category_name VARCHAR(100)
);
-- ✅ DOĞRU SIRA — önce parent, sonra child
CREATE TABLE categories (
    category_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    category_name VARCHAR(100)
);

CREATE TABLE products (
    product_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    category_id INT UNSIGNED,
    FOREIGN KEY (category_id) REFERENCES categories(category_id)
);

Dairesel referans (circular reference) durumu:

Bazen iki tablo birbirine referans verir. Bu durumda foreign key'i sonradan eklersin:

-- employees ve departments birbirine referans veriyor
-- Önce FK'sız oluştur
CREATE TABLE departments (
    department_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    department_name VARCHAR(100) NOT NULL,
    manager_id INT UNSIGNED NULL
);

CREATE TABLE employees (
    employee_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,
    department_id INT UNSIGNED,
    manager_id INT UNSIGNED NULL,
    FOREIGN KEY (department_id) REFERENCES departments(department_id),
    FOREIGN KEY (manager_id) REFERENCES employees(employee_id)
);

-- Sonradan FK ekle
ALTER TABLE departments 
ADD CONSTRAINT fk_dept_manager 
FOREIGN KEY (manager_id) REFERENCES employees(employee_id);

Generated Columns (Hesaplanan Sütunlar)

MySQL 5.7+ sürümlerinde, diğer sütunlardan otomatik hesaplanan sütunlar tanımlayabilirsin:

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,
    
    -- Hesaplanan sütun: satır toplamı
    line_total DECIMAL(10,2) GENERATED ALWAYS AS 
        (quantity * unit_price * (1 - discount_percent / 100)) STORED
);

INSERT INTO order_items (order_id, product_id, quantity, unit_price, discount_percent)
VALUES (1, 5, 3, 100.00, 10);

SELECT quantity, unit_price, discount_percent, line_total FROM order_items;
-- 3, 100.00, 10.00, 270.00 (3 * 100 * 0.90 = 270)

VIRTUAL vs STORED:

  • VIRTUAL — Sorgulandığında hesaplanır, disk alanı kullanmaz

  • STORED — INSERT/UPDATE'te hesaplanıp saklanır, index oluşturulabilir


Gerçek Dünya Örneği — E-Ticaret Veritabanı Tam Kurulum

Şimdi e-ticaret veritabanımızın tüm tablolarını doğru sırada oluşturalım:

-- E-Ticaret Veritabanı — Tam Tablo Kurulumu
-- ==========================================

DROP DATABASE IF EXISTS e_commerce;
CREATE DATABASE e_commerce CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE e_commerce;

-- 1. Kategoriler (parent referansı yok — ilk oluşturulabilir)
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,
    
    CONSTRAINT fk_category_parent 
        FOREIGN KEY (parent_category_id) REFERENCES categories(category_id)
        ON DELETE SET NULL
) ENGINE=InnoDB;

-- 2. Ürünler (categories'e bağlı)
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,
    stock_quantity INT UNSIGNED DEFAULT 0,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    CONSTRAINT fk_products_category 
        FOREIGN KEY (category_id) REFERENCES categories(category_id)
        ON DELETE SET NULL,
    CONSTRAINT chk_products_price CHECK (price > 0)
) ENGINE=InnoDB;

-- 3. Müşteriler (bağımsız)
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) NOT NULL,
    phone VARCHAR(20),
    city VARCHAR(50),
    registration_date DATE DEFAULT (CURRENT_DATE),
    is_active BOOLEAN DEFAULT TRUE,
    
    CONSTRAINT uq_customers_email UNIQUE (email)
) ENGINE=InnoDB;

-- 4. Siparişler (customers'a bağlı)
CREATE TABLE orders (
    order_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    customer_id INT UNSIGNED NOT NULL,
    order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    total_amount DECIMAL(10,2) DEFAULT 0,
    status ENUM('pending','processing','shipped','delivered','cancelled') 
        DEFAULT 'pending',
    shipping_address TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    CONSTRAINT fk_orders_customer 
        FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
        ON DELETE RESTRICT
) ENGINE=InnoDB;

-- 5. Sipariş kalemleri (orders ve products'a bağlı)
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,
    
    CONSTRAINT fk_items_order 
        FOREIGN KEY (order_id) REFERENCES orders(order_id)
        ON DELETE CASCADE,
    CONSTRAINT fk_items_product 
        FOREIGN KEY (product_id) REFERENCES products(product_id)
        ON DELETE RESTRICT,
    CONSTRAINT chk_items_quantity CHECK (quantity > 0),
    CONSTRAINT chk_items_discount CHECK (discount_percent BETWEEN 0 AND 100)
) ENGINE=InnoDB;

-- 6. Departmanlar (FK sonradan eklenecek)
CREATE TABLE departments (
    department_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    department_name VARCHAR(100) NOT NULL,
    manager_id INT UNSIGNED NULL
) ENGINE=InnoDB;

-- 7. Çalışanlar (departments'a ve kendine bağlı)
CREATE TABLE employees (
    employee_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50) NOT NULL,
    last_name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE,
    department_id INT UNSIGNED,
    manager_id INT UNSIGNED NULL,
    hire_date DATE NOT NULL,
    salary DECIMAL(10,2),
    
    CONSTRAINT fk_emp_department 
        FOREIGN KEY (department_id) REFERENCES departments(department_id),
    CONSTRAINT fk_emp_manager 
        FOREIGN KEY (manager_id) REFERENCES employees(employee_id)
) ENGINE=InnoDB;

-- 8. Departman yönetici FK'sini ekle (circular reference çözümü)
ALTER TABLE departments 
ADD CONSTRAINT fk_dept_manager 
FOREIGN KEY (manager_id) REFERENCES employees(employee_id);

-- Kontrol
SHOW TABLES;
DESC products;

Bu script'te dikkat çeken noktalar:

  • Tablolar bağımlılık sırasına göre oluşturuldu

  • Her foreign key isimlendirildi (fk_products_category, fk_orders_customer vb.)

  • Her tablo ENGINE=InnoDB ile oluşturuldu (foreign key desteği için şart)

  • Circular reference ALTER TABLE ile çözüldü

  • CHECK constraint'leri iş kurallarını koruyor


Storage Engine — InnoDB vs MyISAM

MySQL'de tablolar farklı storage engine'lerle oluşturulabilir:

ÖzellikInnoDBMyISAM
Foreign Key✅ Destekler❌ Desteklemez
Transaction✅ Destekler❌ Desteklemez
Row-level locking❌ (table-level)
Crash recoveryKısıtlı
Full-text search✅ (MySQL 5.6+)
Varsayılan✅ (MySQL 5.5+)Eski sürümlerde
-- Engine belirtme
CREATE TABLE my_table (
    id INT PRIMARY KEY
) ENGINE=InnoDB;  -- Önerilen, zaten varsayılan

💡 İpucu: MySQL 5.5'ten beri varsayılan engine InnoDB'dir. Özel bir sebebin yoksa her zaman InnoDB kullan. MyISAM'ın tek avantajı çok basit okuma ağırlıklı tablolarda marginal hız farkı — ama transaction ve foreign key desteği olmaması çok büyük dezavantaj.


Temporary Table — Geçici Tablolar

Sadece mevcut oturum süresince yaşayan tablolar:

CREATE TEMPORARY TABLE temp_bestsellers (
    product_id INT UNSIGNED,
    total_sold INT UNSIGNED
);

-- Bu tablo sadece senin oturumunda görünür
-- Oturum kapandığında otomatik silinir
-- Diğer kullanıcılar bu tabloyu göremez

Geçici tablolar, karmaşık sorguların ara sonuçlarını tutmak için kullanılır. İleri derslerde daha detaylı göreceğiz.


DROP TABLE — Tabloyu Silme

-- Tabloyu sil
DROP TABLE products;

-- Güvenli silme
DROP TABLE IF EXISTS products;

-- Birden fazla tabloyu sil (sıra önemli — child önce!)
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS customers;

⚠️ Dikkat: Foreign key ilişkisi olan tabloları silerken child tabloyu önce silmelisin. Aksi halde "Cannot drop table referenced by a foreign key constraint" hatası alırsın. Veya geçici olarak foreign key kontrolünü kapat:

-- FK kontrolünü kapat (sadece geliştirme ortamında!)
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS products;
-- Sıra fark etmez artık

-- FK kontrolünü tekrar aç!
SET FOREIGN_KEY_CHECKS = 1;

Sıkça Yapılan Hatalar

  1. PRIMARY KEY koymayı unutmak: Her tabloda bir primary key olmalı. Olmadan da tablo oluşabilir ama veri bütünlüğü ve performans açısından büyük dezavantaj. Her tabloya bir ID sütunu koy.

  2. Foreign key veri tiplerinin uyuşmaması: Parent'taki INT UNSIGNED ise child'daki da INT UNSIGNED olmalı. INT ile INT UNSIGNED uyuşmaz ve foreign key oluşturamaz.

  3. CASCADE'i düşünmeden kullanmak: ON DELETE CASCADE rahat görünür ama yanlışlıkla bir parent satırı sildiğinde tüm child satırlar da gider. Özellikle production'da RESTRICT tercih et.

  4. Engine'i MyISAM bırakmak: Foreign key desteği olmaz, transaction desteği olmaz. InnoDB kullan.

  5. NOT NULL'ı her yere koymak: Her sütun NOT NULL olmak zorunda değil. Opsiyonel alanlar (telefon, ikinci adres) NULL olabilmeli. Ama zorunlu alanlar (isim, e-posta, fiyat) kesinlikle NOT NULL olmalı.

  6. Constraint isimlendirmemek: MySQL otomatik isim verir (products_ibfk_1) ama bunlar anlaşılmaz. Constraint'lere anlamlı isim ver — hata ayıklama çok kolaylaşır.


Özet

  • CREATE TABLE ile tablo oluşturulur, her sütunun veri tipi ve kısıtlamaları belirtilir

  • PRIMARY KEY her satırı benzersiz tanımlar, tabloda sadece bir tane olabilir

  • AUTO_INCREMENT otomatik artan değer üretir, ID sütunları için ideal

  • NOT NULL sütunun boş bırakılmasını engeller

  • UNIQUE aynı değerin tekrar girmesini önler

  • DEFAULT varsayılan değer atar

  • CHECK değer aralığını kontrol eder (MySQL 8.0.16+)

  • FOREIGN KEY tablolar arası ilişki kurar ve referans bütünlüğünü korur

  • ON DELETE davranışları: RESTRICT (güvenli), CASCADE (zincirleme sil), SET NULL (null yap)

  • Tablo oluşturma sırası foreign key bağımlılıklarına göre belirlenmelidir

  • Storage engine olarak InnoDB kullan — foreign key ve transaction desteği için şart

  • Constraint'lere anlamlı isim ver — bakım ve debug kolaylaşır