MariaDB ve MySQL’de CREATE TABLE ile Yeni Tablo Oluşturma

Veritabanı yönetiminin en temel işlemlerinden biri, veriyi düzenli bir şekilde saklamak için tablo oluşturmaktır. MariaDB ve MySQL’de CREATE TABLE komutu, bu işlemin merkezinde yer alır. Yeni bir proje başlarken ya da mevcut bir sisteme yeni bir modül eklerken, doğru yapılandırılmış tablolar olmadan sağlıklı bir veri yönetimi mümkün değildir. Bu yazıda CREATE TABLE komutunu tüm yönleriyle inceleyeceğiz; temel sözdiziminden başlayıp gerçek dünya senaryolarına kadar pratik örneklerle ilerleyeceğiz.

CREATE TABLE Komutunun Temel Sözdizimi

CREATE TABLE komutu, en basit haliyle bir tablo adı ve en az bir sütun tanımı gerektirir. Ancak pratikte her zaman birkaç ek parametre ile birlikte kullanılır.

CREATE TABLE tablo_adi (
    sutun_adi veri_tipi [kısıtlamalar],
    sutun_adi veri_tipi [kısıtlamalar],
    ...
);

Gerçek bir örnek üzerinden gidelim. Diyelim ki basit bir kullanıcı tablosu oluşturmak istiyorsunuz:

CREATE TABLE kullanicilar (
    id INT NOT NULL AUTO_INCREMENT,
    kullanici_adi VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    sifre_hash VARCHAR(255) NOT NULL,
    olusturma_tarihi DATETIME DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
);

Bu örnekte birkaç önemli unsur var. AUTO_INCREMENT ile id sütununun otomatik artan bir değer almasını sağlıyoruz. NOT NULL kısıtlaması, o sütunun boş bırakılamayacağını belirtir. DEFAULT CURRENT_TIMESTAMP ise kayıt eklendiği anda otomatik olarak o anki tarih ve saati atar.

IF NOT EXISTS Kullanımı

Otomasyonlarda veya script’lerde en sık yapılan hatalardan biri, zaten var olan bir tabloyu yeniden oluşturmaya çalışmaktır. Bu durumda MariaDB/MySQL bir hata fırlatır ve script’iniz durur. Bunun önüne geçmek için IF NOT EXISTS ifadesini kullanın:

CREATE TABLE IF NOT EXISTS kullanicilar (
    id INT NOT NULL AUTO_INCREMENT,
    kullanici_adi VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE,
    aktif TINYINT(1) DEFAULT 1,
    PRIMARY KEY (id)
);

Bu yaklaşım özellikle deployment script’lerinde, Ansible playbook’larında ya da uygulama başlangıcında çalışan migration dosyalarında hayat kurtarır. Tablo zaten varsa komut sessizce geçer, yoksa oluşturur.

Veri Tipleri ve Doğru Seçim

Tablo tasarımında en kritik noktalardan biri veri tiplerini doğru seçmektir. Yanlış seçimler hem disk alanı israfına hem de performans sorunlarına yol açar.

INT ve türevleri:

  • TINYINT: 1 byte, -128 ile 127 arası (ya da 0-255 unsigned)
  • SMALLINT: 2 byte, küçük sayılar için
  • MEDIUMINT: 3 byte, orta büyüklükte sayılar için
  • INT: 4 byte, en yaygın kullanılan tam sayı tipi
  • BIGINT: 8 byte, çok büyük sayılar için

Karakter ve metin tipleri:

  • CHAR(n): Sabit uzunluklu, n karaktere kadar, kalan kısım boşlukla doldurulur
  • VARCHAR(n): Değişken uzunluklu, n karaktere kadar, sadece kullanılan alan kadar yer kaplar
  • TEXT: 65.535 karaktere kadar uzun metin
  • MEDIUMTEXT: 16 MB’a kadar metin
  • LONGTEXT: 4 GB’a kadar metin

Tarih ve saat tipleri:

  • DATE: Sadece tarih, YYYY-MM-DD formatı
  • TIME: Sadece saat, HH:MM:SS formatı
  • DATETIME: Tarih ve saat birlikte, zaman diliminden bağımsız
  • TIMESTAMP: Unix timestamp, UTC’ye göre saklanır, zaman dilimine duyarlı
  • YEAR: Sadece yıl bilgisi

Sayısal veri tipleri:

  • DECIMAL(m,d): Hassas ondalıklı sayılar, finansal veriler için ideal
  • FLOAT: 4 byte, kayan noktalı sayı, hafif hassasiyet kaybı olabilir
  • DOUBLE: 8 byte, daha hassas kayan noktalı sayı

Kısıtlamalar (Constraints) ile Veri Bütünlüğü

Kısıtlamalar, tablonuza girecek verilerin belirli kurallara uymasını garantiler. Bu hem veri kalitesi hem de uygulama güvenilirliği açısından kritiktir.

CREATE TABLE urunler (
    id INT NOT NULL AUTO_INCREMENT,
    urun_kodu VARCHAR(20) NOT NULL,
    urun_adi VARCHAR(200) NOT NULL,
    fiyat DECIMAL(10,2) NOT NULL CHECK (fiyat >= 0),
    stok_miktari INT DEFAULT 0 CHECK (stok_miktari >= 0),
    kategori_id INT NOT NULL,
    olusturma_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    guncelleme_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_urun_kodu (urun_kodu),
    INDEX idx_kategori (kategori_id)
);

Bu örnekte dikkat çeken birkaç nokta var. CHECK kısıtlaması ile fiyat ve stok miktarının negatif olamayacağını belirtiyoruz. UNIQUE KEY ile ürün kodunun tekrar etmesini engelliyoruz. ON UPDATE CURRENT_TIMESTAMP ise kayıt her güncellendiğinde otomatik olarak güncelleme tarihini set eder.

Foreign Key ile Tablo İlişkilendirme

İlişkisel veritabanlarının gücü, tablolar arasında anlamlı ilişkiler kurmaktan gelir. FOREIGN KEY kısıtlaması bu ilişkiyi veritabanı seviyesinde garantiler.

-- Önce ana tabloyu oluştur
CREATE TABLE kategoriler (
    id INT NOT NULL AUTO_INCREMENT,
    kategori_adi VARCHAR(100) NOT NULL,
    ust_kategori_id INT DEFAULT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (ust_kategori_id) REFERENCES kategoriler(id) ON DELETE SET NULL
);

-- Sonra bağımlı tabloyu oluştur
CREATE TABLE urunler (
    id INT NOT NULL AUTO_INCREMENT,
    urun_adi VARCHAR(200) NOT NULL,
    fiyat DECIMAL(10,2) NOT NULL,
    kategori_id INT NOT NULL,
    PRIMARY KEY (id),
    CONSTRAINT fk_urun_kategori 
        FOREIGN KEY (kategori_id) 
        REFERENCES kategoriler(id) 
        ON DELETE RESTRICT 
        ON UPDATE CASCADE
);

ON DELETE RESTRICT: Bağlı kayıt varken üst kaydın silinmesini engeller. ON DELETE CASCADE: Üst kayıt silinince bağlı kayıtları da siler. ON DELETE SET NULL: Üst kayıt silinince bağlı kayıtlardaki foreign key alanını NULL yapar. ON UPDATE CASCADE: Üst kayıttaki primary key değişince bağlı kayıtları da günceller.

Foreign key kullanırken InnoDB storage engine kullandığınızdan emin olun. MyISAM tablolar foreign key sözdizimini kabul eder ama uygulamaz.

Storage Engine ve Charset Seçimi

Tablo oluştururken ENGINE ve CHARACTER SET parametrelerini belirtmek iyi bir alışkanlıktır. Aksi hâlde sunucu genelindeki varsayılan değerler kullanılır ki bu her zaman istediğiniz sonucu vermeyebilir.

CREATE TABLE blog_yazilari (
    id BIGINT NOT NULL AUTO_INCREMENT,
    baslik VARCHAR(500) NOT NULL,
    icerik LONGTEXT,
    yazar_id INT NOT NULL,
    yayin_tarihi DATETIME,
    goruntulenme_sayisi INT UNSIGNED DEFAULT 0,
    durum ENUM('taslak', 'yayinda', 'arsiv') DEFAULT 'taslak',
    etiketler JSON,
    PRIMARY KEY (id),
    INDEX idx_yazar (yazar_id),
    INDEX idx_yayin_tarihi (yayin_tarihi),
    FULLTEXT INDEX ft_baslik_icerik (baslik, icerik)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Bu örnekte birkaç ilginç nokta var. ENUM tipi, bir sütunun sadece belirlenmiş değerlerden birini alabilmesini sağlar. JSON tipi, MariaDB 10.2+ ve MySQL 5.7+ sürümlerinde mevcuttur; yapılandırılmamış veriyi saklamak için kullanılır. FULLTEXT INDEX ise metin içinde arama yapabilmek için gereklidir. utf8mb4 charset’i, emoji ve bazı özel karakterleri desteklemesi nedeniyle utf8‘e tercih edilmelidir.

Gerçek Dünya Senaryosu: E-Ticaret Veritabanı

Pratikte nasıl bir yaklaşım izleneceğini görmek için basit bir e-ticaret sisteminin tablo yapısını oluşturalım.

-- Müşteri tablosu
CREATE TABLE IF NOT EXISTS musteriler (
    id INT NOT NULL AUTO_INCREMENT,
    ad VARCHAR(100) NOT NULL,
    soyad VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL,
    telefon VARCHAR(20),
    dogum_tarihi DATE,
    cinsiyet ENUM('erkek', 'kadin', 'belirtilmemis') DEFAULT 'belirtilmemis',
    kayit_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    son_giris TIMESTAMP NULL,
    aktif TINYINT(1) DEFAULT 1,
    PRIMARY KEY (id),
    UNIQUE KEY uk_email (email),
    INDEX idx_aktif (aktif)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Sipariş tablosu
CREATE TABLE IF NOT EXISTS siparisler (
    id BIGINT NOT NULL AUTO_INCREMENT,
    musteri_id INT NOT NULL,
    siparis_no VARCHAR(50) NOT NULL,
    toplam_tutar DECIMAL(12,2) NOT NULL,
    kargo_tutari DECIMAL(8,2) DEFAULT 0.00,
    durum ENUM('beklemede', 'odeme_alindi', 'hazirlaniyor', 'kargoda', 'teslim_edildi', 'iptal') DEFAULT 'beklemede',
    odeme_yontemi ENUM('kredi_karti', 'havale', 'kapi_odeme') NOT NULL,
    teslimat_adresi TEXT NOT NULL,
    notlar TEXT,
    olusturma_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    guncelleme_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY uk_siparis_no (siparis_no),
    INDEX idx_musteri (musteri_id),
    INDEX idx_durum (durum),
    INDEX idx_tarih (olusturma_tarihi),
    CONSTRAINT fk_siparis_musteri 
        FOREIGN KEY (musteri_id) 
        REFERENCES musteriler(id) 
        ON DELETE RESTRICT 
        ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Bu yapıda sipariş tablosu müşteri tablosuna bağlıdır. Müşteri silinmek istendiğinde, o müşteriye ait siparişler olduğu sürece silme işlemi engellenir. Bu davranış tam da istediğimiz şeydir; müşteri verisi kaybolmamalıdır.

Geçici Tablo Kullanımı

Bazen karmaşık sorgularda ara sonuçları saklamak için geçici tablolara ihtiyaç duyarsınız. CREATE TEMPORARY TABLE ile oluşturulan tablolar, bağlantı kapandığında otomatik olarak silinir.

-- Geçici tablo oluştur
CREATE TEMPORARY TABLE IF NOT EXISTS gecici_satis_ozeti (
    kategori_id INT,
    kategori_adi VARCHAR(100),
    toplam_satis DECIMAL(15,2),
    siparis_sayisi INT
);

-- Geçici tabloya veri ekle
INSERT INTO gecici_satis_ozeti
SELECT 
    k.id,
    k.kategori_adi,
    SUM(s.toplam_tutar),
    COUNT(s.id)
FROM siparisler s
JOIN urunler u ON s.id = u.id
JOIN kategoriler k ON u.kategori_id = k.id
WHERE s.durum = 'teslim_edildi'
GROUP BY k.id, k.kategori_adi;

-- Sonuçları sorgula
SELECT * FROM gecici_satis_ozeti ORDER BY toplam_satis DESC;

Geçici tablolar, özellikle raporlama sorgularında ve stored procedure’lerde sıkça kullanılır. Her oturum kendi geçici tablolarını görür, bu da eş zamanlı kullanımda çakışma riskini ortadan kaldırır.

CREATE TABLE AS SELECT ile Mevcut Veriden Tablo Oluşturma

Bazen mevcut bir tablonun bir kısmından ya da birden fazla tablonun birleşiminden yeni bir tablo oluşturmanız gerekir.

-- Aktif müşterilerin özetini yeni bir tabloya kopyala
CREATE TABLE aktif_musteri_ozeti AS
SELECT 
    m.id,
    CONCAT(m.ad, ' ', m.soyad) AS tam_ad,
    m.email,
    COUNT(s.id) AS toplam_siparis,
    SUM(s.toplam_tutar) AS toplam_harcama,
    MAX(s.olusturma_tarihi) AS son_siparis_tarihi
FROM musteriler m
LEFT JOIN siparisler s ON m.id = s.musteri_id AND s.durum = 'teslim_edildi'
WHERE m.aktif = 1
GROUP BY m.id, m.ad, m.soyad, m.email;

-- Oluşturulan tabloya index ekle
ALTER TABLE aktif_musteri_ozeti ADD PRIMARY KEY (id);
ALTER TABLE aktif_musteri_ozeti ADD INDEX idx_harcama (toplam_harcama);

Dikkat: CREATE TABLE AS SELECT ile oluşturulan tablolara kısıtlamalar ve index’ler transfer olmaz. Bunları sonradan ALTER TABLE ile eklemeniz gerekir.

Tablo Oluşturma Sonrası Yapıyı Doğrulama

Tablo oluşturduktan sonra yapının beklediğiniz gibi olduğunu doğrulamak iyi bir alışkanlıktır. Özellikle script’lerle otomatik oluşturulan tablolarda bu adımı atlamayın.

-- Tablo yapısını görüntüle
DESCRIBE kullanicilar;

-- Ya da daha detaylı bilgi için
SHOW FULL COLUMNS FROM kullanicilar;

-- Tablo oluşturma sorgusunu gör (index ve kısıtlamalar dahil)
SHOW CREATE TABLE kullanicilar;

-- Tüm index'leri listele
SHOW INDEX FROM kullanicilar;

-- Tablo boyutu ve satır sayısı hakkında bilgi
SELECT 
    table_name,
    table_rows,
    data_length,
    index_length,
    ROUND((data_length + index_length) / 1024 / 1024, 2) AS boyut_mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'kullanicilar';

SHOW CREATE TABLE komutu özellikle kullanışlıdır çünkü tablonun tam oluşturma sorgusunu gösterir. Bu sorguyu farklı bir sunucuda ya da ortamda aynı yapıyı yeniden oluşturmak için kullanabilirsiniz.

Sık Yapılan Hatalar ve Çözümleri

PRIMARY KEY olmadan tablo oluşturmak: InnoDB her tablonun bir primary key’i olmasını bekler. Primary key yoksa InnoDB gizli bir satır ID’si oluşturur, bu da performans açısından dezavantajlıdır.

VARCHAR uzunluğunu aşırı büyük tutmak: VARCHAR(10000) yerine gerçekçi bir değer kullanın. Bu hem depolama optimizasyonu hem de query optimizer’ın daha doğru tahminler yapması için önemlidir.

NULL ve NOT NULL kararını yanlış vermek: Bir sütun gerçekten boş olabiliyorsa NULL bırakın, değilse NOT NULL ekleyin. NULL değerler bazı sorgularda beklenmedik davranışlara yol açabilir.

Foreign key için index eklemeyi unutmak: Foreign key oluşturduğunuzda MariaDB otomatik olarak ilgili sütuna index ekler. Ancak buna rağmen EXPLAIN çıktısını kontrol etmek her zaman iyi bir alışkanlıktır.

TIMESTAMP vs DATETIME karışıklığı: TIMESTAMP 2038 yılına kadar geçerlidir ve zaman dilimine göre dönüştürme yapar. DATETIME ise zaman diliminden bağımsızdır ve çok daha geniş bir tarih aralığını destekler. Uluslararası uygulamalarda genellikle DATETIME tercih edilir.

Performans İçin Index Stratejisi

Tablo oluştururken index planlaması, sonradan performans sorunuyla karşılaşmamak için kritiktir.

CREATE TABLE log_kayitlari (
    id BIGINT NOT NULL AUTO_INCREMENT,
    kullanici_id INT,
    islem_tipi VARCHAR(50) NOT NULL,
    ip_adresi VARCHAR(45),
    user_agent VARCHAR(500),
    islem_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    durum TINYINT(1) DEFAULT 1,
    detay JSON,
    PRIMARY KEY (id),
    -- Sık sorgulanan kombinasyon için composite index
    INDEX idx_kullanici_tarih (kullanici_id, islem_tarihi),
    -- Sadece tarih bazlı sorgular için
    INDEX idx_tarih (islem_tarihi),
    -- IP bazlı araştırmalar için
    INDEX idx_ip (ip_adresi)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Composite index’lerde sütun sırası önemlidir. (kullanici_id, islem_tarihi) şeklinde bir index, hem kullanici_id hem de kullanici_id + islem_tarihi sorgularında kullanılır. Ancak sadece islem_tarihi ile yapılan sorgularda bu index kullanılmaz, bu yüzden ayrı bir idx_tarih index’i ekledik.

Sonuç

CREATE TABLE komutu görünürde basit ama derinlemesine incelendiğinde pek çok nüansı olan bir yapıya sahiptir. Doğru veri tipleri seçmek, kısıtlamalar eklemek, index stratejisini planlamak ve foreign key ilişkilerini kurmak, sadece teknik detaylar değil, aynı zamanda uygulamanızın uzun vadeli başarısını etkileyen kararlar olarak düşünülmelidir.

Pratikte tablo tasarımını bir kez yapıp geçmek yerine, SHOW CREATE TABLE ve DESCRIBE komutlarıyla düzenli olarak yapıyı gözden geçirin. Uygulama büyüdükçe ihtiyaçlar değişir; ALTER TABLE ile tablolara sütun eklemek mümkün olsa da baştan iyi planlanmış bir yapı, ilerleyen dönemde çok daha az baş ağrısı yaratır. Geliştirme ortamınızda test etmeden production’a geçmeyin ve tablo değişikliklerini mutlaka migration script’leriyle yönetin.

Bir yanıt yazın

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