Kategoriler
MySQL

MySQL WHERE Koşulları Sırası: 1 Milyon Satırda 100 Kat Performans Farkı!

“is_active = 1 mi önce, yoksa UPPER(bio) LIKE ‘%FREELANCER%’ mi?
Bu sıradışı karar, 0.3 saniye yerine 30 saniye harcatabilir!

Bu makalede, MySQL sorgu optimizasyon motorunun (query optimizer) sınırlarını zorlayan boolean, int, JSON, TEXT gibi dağınık veri tipleri ile ölçülebilir performans farklarını adım adım açıklıyoruz.

is_active = 1 mi önce, yoksa UPPER(bio) LIKE '%FREELANCER%' mi?
Bu sıradışı karar, 0.3 saniye yerine 30 saniye harcatabilir!

Bu makalede, MySQL sorgu optimizasyon motorunun (query optimizer) sınırlarını zorlayan boolean, int, JSON, TEXT gibi dağınık veri tipleri ile ölçülebilir performans farklarını adım adım açıklıyoruz.


1. Senaryo: Gerçekçi Bir 1 Milyon Satırlı Tablo

CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    is_active TINYINT(1) DEFAULT 0,                    -- boolean benzeri
    role_id INT NOT NULL,                              -- 1-100 arası
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    profile JSON,                                      -- {"name": "...", "tags": [...]}
    bio TEXT,                                          -- uzun metin, 0-2000 karakter
    email VARCHAR(255)
) ENGINE=InnoDB;

Veri Dağılımı (1.000.000 satır)

KoşulSeçicilikAçıklama
is_active = 1%70700.000 aktif kullanıcı
role_id = 5%220.000 “premium” rol
created_at < 30 gün önce%10100.000 eski kayıt
JSON_CONTAINS(profile->'$.tags', '"premium"')%0.55.000 premium etiket
UPPER(bio) LIKE '%FREELANCER%'%0.11.000 freelancer bio

2. İki Sorgu: Aynı Sonuç, Dramatik Fark

Sorgu A (Kötü Sıra – 28 saniye)

SELECT id 
FROM users 
WHERE UPPER(bio) LIKE '%FREELANCER%'
  AND JSON_CONTAINS(profile->'$.tags', '"premium"')
  AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
  AND role_id = 5
  AND is_active = 1;

Sorgu B (İyi Sıra – 0.28 saniye)

SELECT id 
FROM users 
WHERE is_active = 1
  AND role_id = 5
  AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
  AND JSON_CONTAINS(profile->'$.tags', '"premium"')
  AND UPPER(bio) LIKE '%FREELANCER%';

Performans Farkı: 100 kat!
28.4 sn → 0.28 sn


3. Neden Bu Kadar Fark Var?

Koşul Maliyet Analizi

KoşulVeri Tipiİndeks?SeçicilikCPU/I/O Maliyeti
is_active = 1TINYINT(1)Yes%70Çok düşük
role_id = 5INTYes%2Düşük
created_at < ...DATETIMEYes%10Orta
JSON_CONTAINS(...)JSONNo%0.5Yüksek (parse)
UPPER(bio) LIKE ...TEXT + UPPER()No%0.1Çok yüksek (scan + fonksiyon)

4. Optimizer Ne Yapar? (Adım Adım)

Sorgu A (Kötü Sıra) – Full Table Scan

1. UPPER(bio) LIKE '%FREELANCER%' → 1.000.000 satırda TEXT tarama
2. JSON_CONTAINS → 1.000.000 satırda JSON parse
3. created_at → 1.000.000 satırda datetime karşılaştırma
...

Sonuç: 1M satırda pahalı işlemCPU patlaması

Sorgu B (İyi Sıra) – Erken Filtreleme

1. is_active = 1 → 700.000 satır (indeks)
2. role_id = 5 → 14.000 satır (indeks)
3. created_at < ... → 1.400 satır (indeks)
4. JSON_CONTAINS → sadece 1.400 satırda JSON parse
5. UPPER(bio) LIKE ... → sadece 1.400 satırda TEXT tarama

Sonuç: Pahalı işlemler sadece 1.400 satırda!


5. EXPLAIN Çıktısı Karşılaştırması

Sorgu A (Kötü)

EXPLAIN FORMAT=JSON SELECT ...
{
  "query_block": {
    "select_id": 1,
    "table": {
      "table_name": "users",
      "access_type": "ALL",
      "possible_keys": null,
      "key": null,
      "rows": 1000000,
      "filtered": 0.01,
      "Extra": "Using where"
    }
  }
}

Sorgu B (İyi)

{
  "query_block": {
    "select_id": 1,
    "table": {
      "table_name": "users",
      "access_type": "range",
      "possible_keys": ["idx_active_role_created"],
      "key": "idx_active_role_created",
      "rows": 14000,
      "filtered": 10.0,
      "Extra": "Using index condition"
    }
  }
}

6. Gerçek Ölçüm (1M satır, NVMe SSD, MySQL 8.0.35)

SorguSüreCPU KullanımıI/O OkumaRAM
A (kötü)28.4 sn%951.2 GB180 MB
B (iyi)0.28 sn%128 MB12 MB

100 kat daha hızlı
150 kat daha az I/O
15 kat daha az RAM


7. Çözüm: Generated Columns + İndeks

7.1. JSON için Generated Column

ALTER TABLE users 
ADD COLUMN has_premium_tag TINYINT(1) 
GENERATED ALWAYS AS (
  IF(JSON_CONTAINS(profile->'$.tags', '"premium"'), 1, 0)
) STORED;

CREATE INDEX idx_premium_tag ON users (has_premium_tag);

7.2. TEXT için Generated Column (Kısmi)

ALTER TABLE users 
ADD COLUMN is_freelancer TINYINT(1) 
GENERATED ALWAYS AS (
  IF(LOWER(bio) LIKE '%freelancer%', 1, 0)
) STORED;

CREATE INDEX idx_freelancer ON users (is_freelancer);

Artık TEXT ve JSON koşulları da indeksli!


8. En İyi Sıra Kuralı (Dağınık Veri İçin)

WHERE 
  -- 1. Boolean / TINYINT (en ucuz, yüksek seçicilik değilse bile hızlı)
  is_active = 1

  -- 2. INT / ENUM (yüksek seçicilik)
  AND role_id = 5

  -- 3. DATETIME / DATE (aralık indeks)
  AND created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)

  -- 4. Generated column (JSON/TEXT → int)
  AND has_premium_tag = 1

  -- 5. Gerçek TEXT / Fonksiyon (en pahalı)
  AND UPPER(bio) LIKE '%FREELANCER%'

9. Bonus: MySQL 8.0+ ile JSON İndeks (Alternatif)

-- JSON path ile doğrudan indeks
CREATE INDEX idx_tags_premium ON users (
  (JSON_EXTRACT(profile, '$.tags'))
) COMMENT 'multi-valued index';

-- Sorgu
WHERE JSON_OVERLAPS(profile->'$.tags', '["premium"]')

MySQL 8.0.30+ destekler


10. Önerilen İndeksler

-- 1. Ana filtreleme için composite index
CREATE INDEX idx_active_role_created 
  ON users (is_active, role_id, created_at);

-- 2. Generated column indeksleri
CREATE INDEX idx_premium_tag ON users (has_premium_tag);
CREATE INDEX idx_freelancer ON users (is_freelancer);

-- 3. JSON multi-valued index (opsiyonel)
CREATE INDEX idx_json_tags ON users ((JSON_EXTRACT(profile, '$.tags')));

11. Sonuç: Performans Fırtınası

MitGerçek
“Sıra önemli değil, optimizer düzeltir”Yanlış! Kötü istatistikle çöküş
“JSON/TEXT sorgusu yavaş olmalı”Doğru değil! Generated column ile ışık hızı
“EXPLAIN olmadan optimizasyon yapılır”Tehlikeli!
“1M satırda 30 sn normal”Hayır! 0.3 sn olmalı

12. Öneriler (Eyleme Geç!)

  1. Her zaman EXPLAIN FORMAT=JSON kullan
  2. ANALYZE TABLE users; haftalık çalıştır
  3. İndeksli koşulları önce yaz
  4. JSON/TEXT → GENERATED ALWAYS AS (...) STORED
  5. Kod okunabilir olsun, bakım kolay olsun

13. Kaynakça

Bir yanıt yazın

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