VARCHAR ve TEXT Alan Tipleri Arasındaki Fark

Veritabanı tasarımı yaparken en sık karşılaşılan sorulardan biri şudur: “Bu alana VARCHAR mı koysam, TEXT mi?” Yüzeysel bakıldığında ikisi de metin tutuyor gibi görünür ama aralarındaki farklar performansı, depolama maliyetini ve uygulamanın genel davranışını doğrudan etkiler. Bu yazıda MySQL ve MariaDB özelinde bu iki tipi derinlemesine inceleyeceğiz.

VARCHAR ve TEXT’in Temel Yapısı

VARCHAR (Variable Character), tanımladığınız maksimum uzunluğa kadar değişken boyutlu karakter verisi saklar. Örneğin VARCHAR(255) dediğinizde, bu sütun 0 ile 255 karakter arasında herhangi bir uzunlukta veri tutabilir ve yalnızca gerçek veri uzunluğu kadar yer kaplar (artı 1 veya 2 byte’lık uzunluk bilgisi).

TEXT ise büyük metin bloklarını saklamak için tasarlanmış bir tiptir. TINYTEXT, TEXT, MEDIUMTEXT ve LONGTEXT olmak üzere dört farklı boyutu vardır. TEXT alanları satır içinde değil, ayrı bir depolama alanında tutulur; bu durum bazı senaryolarda ciddi performans farklılıklarına yol açar.

Önce temel boyut sınırlarına bakalım:

  • VARCHAR: Maksimum 65.535 karakter (ancak satır boyutu limiti nedeniyle pratikte daha az)
  • TINYTEXT: Maksimum 255 karakter
  • TEXT: Maksimum 65.535 karakter
  • MEDIUMTEXT: Maksimum 16.777.215 karakter (yaklaşık 16 MB)
  • LONGTEXT: Maksimum 4.294.967.295 karakter (yaklaşık 4 GB)

Depolama Mekanizması Farkları

Bu iki tip arasındaki en kritik teknik fark, verinin fiziksel olarak nerede tutulduğudur.

VARCHAR, verisi satırın kendisiyle birlikte (in-row) depolanır. Bu, bir sorgu çalıştırdığınızda MySQL’in tüm satırı tek seferde belleğe alabildiği anlamına gelir. TEXT ise off-page storage kullanır; veri asıl satır sayfasının dışında, ayrı bir alanda tutulur. Bu durum her TEXT alanı için ekstra bir I/O operasyonu anlamına gelebilir.

-- Örnek tablo yapısı oluşturalım
CREATE TABLE urun_katalogu (
    id          INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    urun_adi    VARCHAR(150)    NOT NULL,
    kisa_aciklama VARCHAR(500)  NOT NULL,
    uzun_aciklama TEXT,
    slug        VARCHAR(200)    UNIQUE NOT NULL,
    created_at  TIMESTAMP       DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Yukarıdaki örnekte urun_adi, kisa_aciklama ve slug alanları satırla birlikte tutulurken, uzun_aciklama ayrı bir alanda depolanır.

Index Davranışları

Bu konuda iki tip arasında önemli farklar vardır ve yanlış seçim indeksleme stratejinizi mahvedebilir.

VARCHAR alanları doğrudan indekslenebilir. Eğer VARCHAR(150) bir alana index koyarsanız, MySQL bu alanın tamamını indekse dahil eder.

TEXT alanlarına index koyarken ise prefix index kullanmak zorundasınızdır. Yani kaç karakterin indeksleneceğini açıkça belirtmeniz gerekir.

-- VARCHAR alana direkt index
CREATE INDEX idx_urun_adi ON urun_katalogu(urun_adi);

-- TEXT alana prefix index (zorunlu)
CREATE INDEX idx_uzun_aciklama ON urun_katalogu(uzun_aciklama(100));

-- Full-text search için TEXT alanları daha uygundur
ALTER TABLE urun_katalogu ADD FULLTEXT INDEX ft_aciklama(uzun_aciklama);

Prefix index’in dezavantajı, yalnızca belirlenen karakter sayısına kadar sorgu optimizasyonu yapabilmesidir. Eğer aradığınız değer prefix’in ötesine geçiyorsa, MySQL tam tablo taramasına düşebilir.

Bellek ve Geçici Tablo Davranışı

Bence bu başlık, iki tip arasındaki en gözden kaçan farktır. Sysadminler genellikle disk kullanımına odaklanır ama bellek kullanımı çok daha kritik olabilir.

MySQL, karmaşık sorgularda geçici tablolar oluşturur. Bu geçici tablolar önce bellekte (MEMORY engine kullanarak) oluşturulur; eğer yeterli bellek yoksa diske yazılır. İşte burada kilit nokta: MEMORY engine TEXT tipini desteklemez. Bu nedenle TEXT alanı içeren sorgular her zaman disk üzerinde geçici tablo oluşturur.

-- Bu sorgu TEXT alanı nedeniyle disk geçici tablosu kullanacak
SELECT u.urun_adi, u.uzun_aciklama
FROM urun_katalogu u
JOIN siparis_detay sd ON u.id = sd.urun_id
GROUP BY u.id
ORDER BY u.urun_adi;

-- EXPLAIN ile geçici tablo kullanımını görebilirsiniz
EXPLAIN SELECT u.urun_adi, u.uzun_aciklama
FROM urun_katalogu u
JOIN siparis_detay sd ON u.id = sd.urun_id
GROUP BY u.id
ORDER BY u.urun_adi;

EXPLAIN çıktısında Using temporary; Using filesort görüyorsanız ve sorguda TEXT alanı varsa, bu bir uyarı işaretidir.

Geçici tablo boyutunu kontrol eden parametreleri de bilmek gerekir:

  • tmp_table_size: Bellek içi geçici tabloların maksimum boyutu
  • max_heap_table_size: MEMORY tablolarının maksimum boyutu
  • tmpdir: Disk geçici tablolarının yazıldığı dizin
# Mevcut geçici tablo ayarlarını kontrol edin
mysql -u root -p -e "SHOW VARIABLES LIKE 'tmp_table_size'; SHOW VARIABLES LIKE 'max_heap_table_size';"

# Kaç kez diske yazıldığını görmek için
mysql -u root -p -e "SHOW STATUS LIKE 'Created_tmp_disk_tables'; SHOW STATUS LIKE 'Created_tmp_tables';"

Eğer Created_tmp_disk_tables / Created_tmp_tables oranı yüksekse ve sorgularınızda TEXT alanları varsa, bu alanları gözden geçirmeniz gerekiyor demektir.

Gerçek Dünya Senaryosu: Blog Platformu

Bir blog platformu tasarlıyorsunuz diyelim. Makaleler, yazarlar, kategoriler var. İşte bu senaryoda alan tiplerini nasıl belirlemeliyiz?

CREATE TABLE yazilar (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    baslik          VARCHAR(300)    NOT NULL,
    slug            VARCHAR(320)    NOT NULL UNIQUE,
    ozet            VARCHAR(600),
    icerik          MEDIUMTEXT      NOT NULL,
    meta_description VARCHAR(160),
    meta_keywords   VARCHAR(255),
    yazar_id        INT UNSIGNED    NOT NULL,
    kategori_id     INT UNSIGNED,
    durum           ENUM('taslak', 'yayinda', 'arsiv') DEFAULT 'taslak',
    yayinlama_tarihi DATETIME,
    created_at      TIMESTAMP       DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP       DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_yazar (yazar_id),
    INDEX idx_kategori (kategori_id),
    INDEX idx_durum_tarih (durum, yayinlama_tarihi),
    FULLTEXT INDEX ft_baslik_icerik (baslik, icerik)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_turkish_ci;

Bu tasarımda dikkat ettiğimiz noktalar:

  • baslik: Makale başlıkları 300 karakteri nadiren geçer, sık filtrelenir ve sıralanır. VARCHAR tercih.
  • slug: URL dostu isimler, indekslenmesi gerekiyor. VARCHAR tercih.
  • ozet: Liste sayfalarında gösterilecek kısa özet, VARCHAR yeterli.
  • icerik: Gerçek makale içeriği, binlerce karakter olabilir. MEDIUMTEXT doğru seçim.
  • meta_description: SEO standartları 160 karakteri geçmez, VARCHAR yeterli.

Gerçek Dünya Senaryosu: Log Yönetimi

Uygulama loglarını veritabanında saklayan bir sistem düşünelim. Bu senaryo farklı kararlar almamızı gerektirir.

CREATE TABLE uygulama_loglari (
    id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    log_seviye  ENUM('DEBUG','INFO','WARNING','ERROR','CRITICAL') NOT NULL,
    kaynak      VARCHAR(100)    NOT NULL,
    mesaj       VARCHAR(1000)   NOT NULL,
    stack_trace TEXT,
    istek_url   VARCHAR(2048),
    kullanici_id INT UNSIGNED,
    ip_adresi   VARCHAR(45),
    log_tarihi  DATETIME(3)     NOT NULL,
    INDEX idx_seviye_tarih (log_seviye, log_tarihi),
    INDEX idx_kullanici (kullanici_id),
    INDEX idx_kaynak (kaynak)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (YEAR(log_tarihi)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION pfuture VALUES LESS THAN MAXVALUE
);

Burada mesaj için VARCHAR(1000) seçtik çünkü log mesajları genellikle kısa ve tahmin edilebilir uzunluktadır. stack_trace ise uzun olabilir ve sık sorgulanan bir alan değil, bu yüzden TEXT mantıklı.

VARCHAR vs TEXT Kararını Etkileyen Faktörler

Hangi tipi seçeceğinize karar verirken şu soruları kendinize sorun:

  • Bu alanı WHERE koşulunda sık kullanacak mısınız? Evet ise, mümkünse VARCHAR tercih edin ve index koyun.
  • Bu alan ORDER BY veya GROUP BY’da yer alacak mı? Evet ise, VARCHAR çok daha performanslı çalışır.
  • Veri uzunluğu tutarlı mı ve tahmin edilebilir mi? Evet ise, VARCHAR uygun.
  • Veri 65.535 karakteri geçebilir mi? Evet ise, TEXT ailesi zorunlu.
  • Bu alan sadece depolanıp nadiren sorgulanacak mı? Evet ise, TEXT daha az satır overhead’i demektir.
-- Veri boyutunu analiz eden yardımcı sorgu
SELECT
    column_name,
    data_type,
    character_maximum_length,
    AVG(LENGTH(column_value)) as ort_uzunluk,
    MAX(LENGTH(column_value)) as max_uzunluk,
    MIN(LENGTH(column_value)) as min_uzunluk
FROM information_schema.columns c
JOIN (
    SELECT 'baslik' as column_name, baslik as column_value FROM yazilar
    UNION ALL
    SELECT 'ozet', ozet FROM yazilar WHERE ozet IS NOT NULL
) data ON c.column_name = data.column_name
WHERE c.table_name = 'yazilar'
GROUP BY column_name;

Bu sorgu gerçek veri dağılımınızı analiz ederek alan tipi kararlarınızı destekler.

Charset ve Collation Etkisi

Türkçe veri saklıyorsanız utf8mb4 kullanmanız zorunludur. Bu durumda VARCHAR boyut hesaplaması değişir çünkü her karakter 1 ila 4 byte arasında yer kaplayabilir.

InnoDB’de bir indeks kaydının maksimum boyutu 767 byte’tır (Barracuda format ve innodb_large_prefix açıksa 3072 byte’a çıkar). utf8mb4 ile VARCHAR(255) bir alan 255 * 4 = 1020 byte olabilir; bu 767 byte sınırını aşar.

# innodb_large_prefix durumunu kontrol edin
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_large_prefix';"
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_file_format';"

# MariaDB 10.2+ ve MySQL 5.7+ için bu ayarları my.cnf'e ekleyin
grep -E "innodb_large_prefix|innodb_file_format|innodb_file_per_table" /etc/mysql/my.cnf
# /etc/mysql/my.cnf veya /etc/mysql/mariadb.conf.d/50-server.cnf
# [mysqld] bölümüne ekleyin
cat >> /etc/mysql/mariadb.conf.d/50-server.cnf << 'EOF'
innodb_file_format = Barracuda
innodb_file_per_table = 1
innodb_large_prefix = 1
EOF

Mevcut Tabloları Analiz Etme

Üretim ortamında çalışan bir sistemde hangi TEXT alanlarının gereksiz yere kullanıldığını bulmak için information_schema’yı kullanabilirsiniz:

-- Veritabanındaki tüm TEXT alanlarını listele
SELECT
    table_schema,
    table_name,
    column_name,
    data_type,
    character_maximum_length,
    is_nullable
FROM information_schema.columns
WHERE table_schema = 'blog_db'
  AND data_type IN ('tinytext', 'text', 'mediumtext', 'longtext')
ORDER BY table_name, column_name;

-- TEXT alanlarının gerçek kullanımını ölç
SELECT
    COUNT(*) as toplam_kayit,
    AVG(LENGTH(icerik)) as ort_byte,
    MAX(LENGTH(icerik)) as max_byte,
    MIN(LENGTH(icerik)) as min_byte,
    SUM(LENGTH(icerik)) / 1024 / 1024 as toplam_mb
FROM yazilar
WHERE icerik IS NOT NULL;

Bu analizin sonucuna göre belki bazı TEXT alanlarınızın aslında VARCHAR ile karşılanabileceğini, ya da tam tersi bazı uzun VARCHAR alanların TEXT’e alınması gerektiğini görebilirsiniz.

Tip Dönüşümü Yaparken Dikkat Edilecekler

Mevcut bir VARCHAR alanı TEXT’e veya TEXT’i VARCHAR’a dönüştürürken dikkatli olmak gerekir. Bu işlem büyük tablolarda tablo kilidine (veya online DDL ile metadata kilidine) yol açabilir.

-- Mevcut TEXT alanını VARCHAR'a dönüştürme
-- Önce maksimum uzunluğu kontrol edin
SELECT MAX(CHAR_LENGTH(uzun_aciklama)) FROM urun_katalogu;

-- Güvenli dönüşüm (pt-online-schema-change veya gh-ost olmadan)
ALTER TABLE urun_katalogu
    MODIFY COLUMN uzun_aciklama VARCHAR(2000);

-- Büyük tablolarda pt-online-schema-change kullanın
-- pt-online-schema-change --alter "MODIFY COLUMN uzun_aciklama VARCHAR(2000)" 
--   D=mydb,t=urun_katalogu --execute
# Percona Toolkit ile güvenli ALTER işlemi
# Önce pt-online-schema-change kurulumu
apt-get install percona-toolkit  # Debian/Ubuntu
yum install percona-toolkit      # RHEL/CentOS

# Büyük tablolarda tip değişikliği
pt-online-schema-change 
    --host=localhost 
    --user=root 
    --password=gizli_sifre 
    --alter "MODIFY COLUMN uzun_aciklama VARCHAR(2000)" 
    D=blog_db,t=urun_katalogu 
    --no-drop-old-table 
    --execute

Sık Yapılan Hatalar

Yıllar içinde gördüğüm en yaygın hatalar şunlar:

  • Her metin alanına TEXT koymak: “Büyük olsun zararlı olmaz” mantığı yanlış. Gereksiz TEXT kullanımı geçici tablo performansını doğrudan etkiler.
  • VARCHAR(255)’i evrensel standart olarak kullanmak: Bazı alanlara 50 yeterken 255 koymak kaynak israfı değil ama indeks verimliliğini düşürür.
  • DEFAULT değer atamayı unutmak: TEXT alanları DEFAULT değer alamaz (MySQL 5.7 öncesinde), bunu göz ardı etmek uygulama hatalarına yol açar.
  • Gereksiz yere büyük VARCHAR kullanmak: VARCHAR(65535) yerine TEXT kullanmak hem daha semantik hem de aynı işlevi görür.
-- Bu hata verir (MySQL 5.7 öncesi)
CREATE TABLE test_tablo (
    aciklama TEXT DEFAULT 'Varsayılan metin'  -- Hata!
);

-- VARCHAR'da DEFAULT çalışır
CREATE TABLE test_tablo (
    aciklama VARCHAR(500) DEFAULT 'Varsayılan metin'  -- OK
);

-- TEXT için default null veya uygulama katmanında default kullanın
CREATE TABLE test_tablo (
    aciklama TEXT NULL
);

Performans Testi Yapma

Kendi ortamınızda iki tip arasındaki performans farkını ölçmek için basit bir test yapabilirsiniz:

-- Test verisi oluştur
CREATE TABLE varchar_test (
    id INT AUTO_INCREMENT PRIMARY KEY,
    metin VARCHAR(1000)
) ENGINE=InnoDB;

CREATE TABLE text_test (
    id INT AUTO_INCREMENT PRIMARY KEY,
    metin TEXT
) ENGINE=InnoDB;

-- Her iki tabloya aynı veriyi yükle
INSERT INTO varchar_test (metin)
SELECT REPEAT('x', 500) FROM information_schema.columns LIMIT 10000;

INSERT INTO text_test (metin)
SELECT REPEAT('x', 500) FROM information_schema.columns LIMIT 10000;

-- Sorgu sürelerini karşılaştır
SET profiling = 1;

SELECT COUNT(*), AVG(LENGTH(metin)) FROM varchar_test WHERE metin LIKE 'xx%';
SELECT COUNT(*), AVG(LENGTH(metin)) FROM text_test WHERE metin LIKE 'xx%';

SHOW PROFILES;

Sonuç

VARCHAR ve TEXT arasındaki seçim, “hangisi daha büyük veri tutar” sorusunun çok ötesine geçer. Depolama mekanizması, indeksleme davranışı, bellek içi geçici tablo kullanımı ve sorgu optimizasyonu bu kararı doğrudan etkiler.

Pratik bir özet geçmek gerekirse:

  • VARCHAR kullanın: Alan sık sorgulanıyorsa, WHERE/ORDER BY/GROUP BY’da yer alıyorsa, uzunluk 500-1000 karakterin altındaysa ve tutarlıysa.
  • TEXT kullanın: Veri çok uzun ve değişken olabiliyorsa, alan nadiren sorgulanıyorsa, sadece saklanıp görüntüleniyorsa.
  • MEDIUMTEXT veya LONGTEXT kullanın: Blog içerikleri, makale metinleri, HTML şablonları, JSON veri blokları gibi potansiyel olarak çok büyük veriler için.

En önemli nokta: Kararı sezgiyle değil, gerçek veri profilinizi analiz ederek verin. information_schema sorgularını ve EXPLAIN çıktılarını düzenli olarak gözden geçirme alışkanlığı edinin. Üretim sisteminde yaşanan yavaş sorgular çoğunlukla yanlış alan tipi seçiminden değil ama doğru alan tipiyle yanlış indeks stratejisinden kaynaklanır; dolayısıyla bu iki konuyu birlikte değerlendirmek şart.

Bir yanıt yazın

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