WordPress'te TTFB'yi yükselten asıl etken çoğunlukla veritabanı sorgularıdır. Query Monitor ile yavaş ve tekrar eden sorguları bulmayı, N+1 problemini çözmeyi, transient API ile object cache'i doğru kullanmayı ve her iyileştirmeyi TTFB üzerinden doğrulamayı anlatan pratik rehber.

WordPress'te SQL Sorgu Optimizasyonu ile TTFB Nasıl Düşürülür?

TTFB (Time to First Byte), sunucunun bir isteği alıp ilk byte'ı gönderdiği süreyi ölçer. WordPress sitelerinde bu değer çoğunlukla PHP işleme ve veritabanı sorguları tarafından şekillenir. Sayfa başına düşen sorgu sayısı arttıkça, her bir sorgunun süresi uzadıkça TTFB de yükselir; ancak sorunun tam olarak nerede olduğunu görmeden sunucu planını yükseltmek kalıcı bir çözüm değil, geçici bir örtbastırmadır.

WordPress çekirdeği verimli çalışır. Eklentiler, temalar ve özel WP_Query kullanan kod parçaları bu dengeyi bozabilir. Bir sayfa yüklenirken onlarca, bazen yüzü aşkın SQL sorgusu tetiklenebilir; bir kısmı tekrarlıdır, bir kısmı indeks kullanmaz, bir kısmı gereğinden büyük veri setleri çeker. Tüm bu yük TTFB'ye doğrudan yansır ve sonuç ölçümlere - özellikle Core Web Vitals raporlarına - girer.

Tahmin üzerinden optimizasyon yapmak zaman kaybettirir. Gereksiz yerlere cache eklemek, bazen davranışı karmaşıklaştırır ve sorunun gerçek kaynağını gizler. Neyin ne kadar sürdüğünü görmek için doğru araç Query Monitor'dür ve başlangıç her zaman ölçümle olmalıdır.

Query Monitor ile yavaş sorguları tespit etmek

Query Monitor, WordPress için ücretsiz bir geliştirici eklentisidir. Her sayfa yüklemesinde çalışan SQL sorgularını, sürelerini, sorguyu tetikleyen fonksiyon ve dosya bilgisini, ayrıca EXPLAIN çıktısını yönetici çubuğu üzerinden gösterir. Kurulum standart bir WordPress eklentisi kadardır; eklenti etkinleştirildikten sonra herhangi bir sayfayı yönetici olarak ziyaret edin ve üst çubukta beliren "Queries" bağlantısına tıklayın.

Sorgular süreye göre sıralanabilir. En yavaş olanlar hemen öne çıkar. "Caller" sütunu sorguyu tetikleyen fonksiyon ve dosyayı gösterir; bu bilgi, sorunun hangi eklenti veya tema bileşeninden kaynaklandığını anında görünür kılar ve genel bir "sorgular yavaş" tespitini somut bir kaynağa bağlar.

EXPLAIN çıktısındaki "type" sütununa bakın. Bu sütunda ALL yazan sorgular tablonun tamamını tarar; tablo büyüdükçe süre de artar. key sütunu boşsa sorgu hiçbir indeks kullanmıyor demektir. Bu iki işaret bir arada göründüğünde, söz konusu sorgu büyük ihtimalle TTFB'ye en büyük katkıyı yapandır.

"Duplicate queries" sekmesi ayrıca incelemeye değer. Aynı sorgunun defalarca çalışması, N+1 probleminin ya da yanlış yapılandırılmış bir cache katmanının işaretidir. Hangi sayfada kaç sorgu çalıştığını not edin - bu sayı, sonraki iyileştirmelerin etkisini ölçmek için referans noktanız olacak.

N+1 sorgu problemi: WordPress'te nasıl ortaya çıkar?

N+1, her öğe için ayrı bir veritabanı sorgusu çalıştırıldığında oluşan bir örüntüdür. Döngüde 20 yazı listeliyorsanız ve her yazı için ayrıca yazar adını, taksonomileri veya özel meta alanlarını çekiyorsanız, 1 listeleme sorgusu artı 20 ek sorgu tetiklenir. Liste büyüdükçe sorgu sayısı da büyür; bunu Query Monitor'de görmek çoğunlukla şaşırtıcıdır.

WordPress bu durumu kısmen önler. WP_Query ile gelen yazılar için terim ve meta verileri varsayılan olarak toplu önceden yüklenir; update_post_meta_cache ve update_post_term_cache parametreleri varsayılan olarak true'dur. Sorun, eklentilerin kendi döngülerinde veya doğrudan $wpdb üzerinden yazı ID listesi alıp her ID için ayrı sorgu çalıştırdığında ortaya çıkar.

// Sorunlu: her döngüde ayrı sorgu
foreach ($post_ids as $id) {
    $meta = get_post_meta($id, 'price', true);
}

// Düzeltilmiş: meta önce toplu yüklenir
update_postmeta_cache($post_ids);
foreach ($post_ids as $id) {
    $meta = get_post_meta($id, 'price', true); // önbellekten gelir
}

Özel WP_Query sorgularında 'no_found_rows' => true eklemek, sayfalama kullanılmadığı durumlar için SQL_CALC_FOUND_ROWS'u devre dışı bırakır. Büyük tablolarda bu ek işlem gözle görülür gecikme üretir. Çoğu widget sorgusunda, menü oluşturma işleminde ve ana döngü dışındaki listeleme bloklarında sayfalama gerekmez; parametreyi atlamak yaygın bir hatadır ve sorgu başına düşen maliyeti sessizce artırır.

Bir diğer kaynaklık noktası: get_the_terms() veya wp_get_post_terms() döngü içinde çağrıldığında, her yazı için ayrı bir sorgu tetikler. WP_Query içinde yazılar yüklendiyse terimler zaten cache'e alınmış olabilir; ama doğrudan ID listesiyle çalışırken bu otomatik yükleme çalışmaz. update_object_term_cache() ile toplu yükleme burada da aynı mantıkla uygulanır.

Kontrolsüz posts_per_page ve karmaşık sorgu argümanları

posts_per_page => -1 tüm eşleşen yazıları tek sorguda çeker. Birkaç yüz yazılık küçük sitelerde bu fark edilmez. Yazı sayısı binlere ulaştığında, aynı argüman hem sorgu süresini hem de PHP bellek kullanımını ciddi biçimde artırır. Kod yazılırken bu değer makul görünür; içerik büyüdükçe sorun da büyür, ama kaynak hep aynı satırda kalır.

Benzer bir etki, meta_query ile yapılan değer tabanlı aramalarda görülür. LIKE '%değer%' biçimindeki koşullar indeks kullanamaz; tablo büyüdükçe yavaşlar. wp_postmeta üzerinde değer araması yapmanın verimli bir yolu yoktur. Bu tür aramaları ayrı bir tabloya taşımak ya da tam metin arama altyapısı kullanmak gerekir.

Taksonomi ile meta sorgusu bir arada kullanıldığında da dikkat gerekir. relation => AND ile çalışan iki meta_query koşulunu tax_query ile birleştirdiğinizde, WordPress arka planda birden fazla JOIN üretir. Üretilen gerçek SQL'i görmek için:

$query = new WP_Query($args);
echo $query->request;

Bu çıktı, EXPLAIN ile birlikte hangi indekslerin kullanıldığını görmek için başlangıç noktasıdır. Gereksiz JOIN'leri azaltmak, argümanları sadeleştirmek veya sorguyu parçalara ayırarak önbelleğe almak, bu aşamada değerlendirilen yollardır. Ampirik kural basittir: her eklenen meta_query koşulu sorguyu daha uzağa götürür; yalnızca gerçekten gerekli olanlar eklenir.

Transient API: ne zaman işe yarar, ne zaman gereksiz kalır?

Transient API, sorgu sonuçlarını belirli bir süre saklamak için tasarlanmıştır. Yavaş ama seyrek değişen veriler için uygun bir katmandır. Sözdizimi açıktır:

$result = get_transient('my_expensive_query');
if (false === $result) {
    $result = $wpdb->get_results($sql);
    set_transient('my_expensive_query', $result, HOUR_IN_SECONDS);
}

Ters etki vardır. Persistent cache kurulu değilken transient verisi wp_options tablosuna yazılır. Bu tabloya her erişim de bir veritabanı sorgusudur. Yani yavaş sorgu yerine daha hızlı bir opsiyon okuma koymuş olursunuz - sorgu sayısı azalmaz, yalnızca sorgunun tipi değişir. Transient, persistent cache olmadan asıl sorunu çözmez.

Redis veya Memcached kuruluysa durum değişir. set_transient ile saklanan değer RAM'den okunur ve veritabanına hiç gidilmez. Bu durumda transient gerçek anlamını kazanır ve pahalı bir sorgunun önüne ciddi bir katman koyar.

Stok durumu, güncel fiyat veya yorum sayısı gibi sık değişen verileri uzun süreli transient ile saklamak görünür tutarsızlıklara yol açar. Transient süresi verideki değişim sıklığıyla orantılı olmalıdır. Sık değişen veriler için kısa TTL ya da kayıt güncellendiğinde transient'ı silen event tabanlı invalidation tercih edilir; delete_transient() bunu sağlar.

Object cache ve Redis ile persistent önbellekleme

WordPress'in yerleşik object cache, istek süresince tekrar eden veritabanı sorgularını bellekte tutar. Aynı istek içinde ikinci kez çalışan özdeş sorgu, veritabanına gitmek yerine bellekten sonucu alır. Bu yararlıdır. Ama bu cache tek bir PHP isteğiyle sınırlıdır; sayfa yenilendiğinde sıfırlanır.

Persistent object cache bu sınırı ortadan kaldırır. Redis veya Memcached kullanıldığında, bir istek sırasında hesaplanan değer bir sonraki istekte de RAM'den okunur. Yüksek trafikli sayfalarda aynı sorguyu her istek için tekrar çalıştırmak yerine sonucu paylaşmak, ölçekle birlikte artan bir tasarruf sağlar.

WordPress'te Redis entegrasyonu için wp-content/object-cache.php dosyasının yerleştirilmesi gerekir. WP Redis ve Redis Object Cache eklentileri bu dosyayı yönetir ve bağlantı ayarlarını wp-config.php üzerinden alır. Entegrasyon sonrası Query Monitor'de toplam sorgu sayısı ve toplam süreye bakın; sık tekrar eden sorgular azaldıysa cache çalışıyordur.

Kodda doğrudan kullanmak da mümkündür:

$cache_key = 'my_query_result';
$data = wp_cache_get($cache_key);
if (false === $data) {
    $data = $wpdb->get_results($sql);
    wp_cache_set($cache_key, $data, '', 300);
}

Cache anahtarlarını özenle seçin. Aynı sorgu farklı parametrelerle çalışıyorsa, anahtar bu parametreleri yansıtmalıdır. Aksi hâlde farklı filtre kombinasyonları aynı önbellek bloğunu paylaşır ve yanlış veri sunulur; bu tür hatalar izlemesi en zor davranış bozukluklarından biridir.

wp_postmeta tablosu ve indeks sınırları

WordPress meta tablosu dikey bir veri modeliyle çalışır: her özellik ayrı bir satır olarak saklanır. Bu yapı esneklik sağlar; eklenti kurulur, meta alanları eklenir, tablo büyür. Ama büyük sitelerde sorgulama maliyeti de büyür. Her yazıya ait onlarca meta alanı olduğunda tablo boyutu hızla şişer ve filtreleme yavaşlar.

Tablo varsayılan olarak post_id ve meta_key üzerinde indekslidir. meta_value üzerinde indeks yoktur. Değer üzerinden yapılan her arama tam tablo taramasına girer ve bu, tablonun büyüklüğüyle doğrusal biçimde kötüleşir. WooCommerce bu sorunu kısmen çözmek için wc_product_meta_lookup tablosunu kullanır; kritik alan değerleri bu tabloda indekslenmiş sütunlarda tutulur ve ürün listesi sorguları buraya yönlendirilir.

Özel veri modelleri kuruyorsanız ve belirli bir meta anahtarını yoğun sorguluyorsanız, bu veriyi wp_postmeta yerine özel bir tabloda tutmayı düşünmek mantıklıdır. Ek tablo karmaşıklık getirir; ama indeks esnekliği, değer tabanlı aramaların gerçekten hızlanmasını sağlar. Karar, sorgunun ne sıklıkta çalıştığına ve tablonun ne kadar büyüdüğüne bağlıdır.

Sorgu optimizasyonunu TTFB üzerinden doğrulamak

Değişiklik yapmadan önce ve sonra ölçüm almak zorunludur. Ölçümsüz optimizasyon, neyin işe yaradığını gizler ve öncelik sıralamasını rastgele bırakır.

Lab ölçümü için Chrome DevTools'un Network sekmesi açılır, sayfa yüklenir, listedeki ilk istek seçilir ve "Waiting for server response" değeri okunur. Bu değer TTFB'dir. İlk istek genellikle daha yavaştır; birkaç kez tekrarlayın ve ısınmış değerleri kaydedin. Ölçümler arasındaki tutarlılık, sonuçları anlamlı kılar.

JavaScript ile alan verisi toplamak da mümkündür:

const [nav] = performance.getEntriesByType('navigation');
console.log('TTFB ms:', nav.responseStart - nav.requestStart);

Query Monitor her sayfa yüklemesinde toplam sorgu sayısı ve toplam sorgu süresini özetler. Bir iyileştirme uyguladıktan sonra bu iki sayıya bakın. Sorgu süresi düştüyse TTFB'de de iyileşme beklenir; ancak her zaman tam orantılı bir düşüş olmaz, çünkü PHP işleme ve diğer etkenler de bu toplam içindedir. Değişkenleri izole etmek için tek bir iyileştirmeyi uygulayın, ölçün, sonra bir sonrakine geçin.

Cache devreye alındıktan sonra doğrulamak için cache'i bypass eden bir istek gönderin. Çoğu cache sistemi, yetkili kullanıcı oturumlarında veya belirli bir sorgu parametresi varlığında sayfayı cache'lemez; bu durum, gerçek sorgu yükünü yeniden çalıştırır ve iyileştirmenin temel ölçümünü verir. Cache hit ve cache miss arasındaki TTFB farkı ne kadar büyükse, sorgu optimizasyonunun katkısı da o kadar önemlidir.

WordPress veritabanı optimizasyonu doğrusal bir süreç değildir. Bir sorgu düzeltildiğinde başka bir sorgu öne çıkabilir, cache katmanı eklendikten sonra yanlış anahtar stratejisi yeni sorunlar yaratabilir. Ölçüm-değişiklik-ölçüm döngüsünü kısa tutmak ve her değişikliği ayrı ayrı test etmek, ilerlemeyi görünür kılar ve geri adım atmayı kolaylaştırır.

TTFB üzerindeki etkiyi görme konusunda en güvenilir yol, Query Monitor çıktısını kaydetmek ve zamanla karşılaştırmaktır. Toplam sorgu sayısı ve toplam süre, soyut iyileştirme iddialarını somut sayılara bağlar. Persistent cache kuruluysa her yeni dağıtım sonrasında bu sayıları tekrar kontrol etmek, cache'in beklediği gibi çalıştığını doğrular ve sessizce bozulan bir entegrasyonu erkenden yakalar.