Veritabanı Sorgu Optimizasyonu İçin Temel Teknikler

Veritabanı sorgu optimizasyonunda indeksleme, execution plan analizi ve N+1 probleminden kaçınma gibi temel tekniklerle performans artırmayı öğrenin.

Yazı boyutu
Veritabanı Sorgu Optimizasyonu İçin Temel Teknikler

Fotoğraf: AMORIE SAM / Pexels

Bir bakışta

  • Sorgu performansı erken ele alınmazsa üretimde veri büyüdükçe ciddi sorunlara dönüşür.
  • Optimizasyondan önce sorgu gerçek veri hacmiyle ölçülmeli ve execution plan incelenmelidir.
  • Doğru indeksleme, SELECT * kaçınma ve N+1 problemine dikkat performansı artırır.

Yavaş Sorgular Neden Önemli Bir Sorun?

Bir uygulamada tek bir yavaş sorgu, ilk bakışta önemsiz görünebilir. Ancak bu sorgu bir sayfa yüklenirken çalışıyorsa, kullanıcı o sayfanın açılmasını beklerken geçen her ek yüz milisaniye doğrudan deneyimi etkiler. Daha da önemlisi, o sorgu veritabanı sunucusunda kaynak (CPU, bellek, disk G/Ç) tüketirken, aynı anda gelen başka isteklerin de bu kaynaklardan pay almasını zorlaştırır.

Uygulama küçükken, az veri ve az kullanıcıyla çalışırken bu sorunlar genellikle fark edilmez. Sorgu 50 milisaniyede de, 500 milisaniyede de dönse kullanıcı bir fark hissetmeyebilir. Sorun, veri hacmi ve eşzamanlı kullanıcı sayısı arttıkça büyür. Test ortamında sorunsuz çalışan bir sorgu, üretimde milyonlarca satırlık bir tabloya karşı çalıştırıldığında dakikalar sürebilir. Bu yüzden performans sorunları çoğunlukla geliştirme sırasında değil, üretimde veri büyüdükçe ortaya çıkar — ve genellikle en kötü zamanda, yani sistem en çok yük altındayken fark edilir.

Küçük performans sorunları da tek başına önemsiz kalmaz; bir sayfada birkaç yavaş sorgu, bir API isteğinde birden fazla gereksiz veritabanı çağrısı birleştiğinde toplam etki katlanarak büyür. Bu yüzden sorgu performansını erken düşünmek, sonradan büyük bir "performans yangını" söndürmekten çok daha az maliyetlidir.

Optimizasyona Başlamadan Önce: Ölçmeden Dokunma

Optimizasyona girişmeden önce atılması gereken en önemli adım, sorunu doğru tespit etmektir. Sezgiyle "bu sorgu yavaş olmalı" diyerek kod değiştirmek, çoğu zaman yanlış yere odaklanmaya ve gerçek sorunu gözden kaçırmaya yol açar.

Sağlıklı bir yaklaşım şudur: değişiklik yapmadan önce sorgunun ne kadar sürdüğünü ölç, değişikliği yap, sonra tekrar ölç. Bu karşılaştırma olmadan bir iyileştirmenin gerçekten işe yarayıp yaramadığını bilemezsiniz. Bazen "iyileştirme" olarak yapılan bir değişiklik, farklı bir senaryoda performansı kötüleştirebilir.

Bir diğer kritik nokta, testin gerçekçi veri hacmiyle yapılması gerektiğidir. Yüz satırlık bir test tablosunda hangi sorgu yazılırsa yazılsın hızlı çalışır; bu yüzden küçük veri setleriyle yapılan testler yanıltıcı olabilir. Üretimdeki veri hacmine yakın bir ortamda (veya en azından benzer bir dağılım ve büyüklükte) test etmek, gerçek darboğazları görmenizi sağlar.

Execution Plan Mantığını Anlamak

Bir sorgu çalıştırıldığında veritabanı, bu sorguyu en verimli şekilde nasıl çalıştıracağına dair bir plan oluşturur. Hangi tabloya önce bakacağını, hangi indeksi kullanacağını, verileri nasıl birleştireceğini bu plana göre belirler. Çoğu veritabanı sistemi, bu planı geliştiricinin görebilmesi için bir mekanizma sunar; genellikle sorgunun başına özel bir anahtar kelime eklenerek çalıştırılır ve sonucunda veritabanının "içeride ne yaptığı" adım adım gösterilir.

Bu planı okumak ilk bakışta karmaşık görünebilir, ancak temel mantık basittir: plan size sorgunun hangi adımda ne kadar zaman veya kaynak harcadığını gösterir. Özellikle dikkat edilmesi gereken nokta, veritabanının bir tablodaki tüm satırları tek tek tarayıp taradığı (tam tarama) mı, yoksa bir indeks üzerinden doğrudan ilgili satırlara mı ulaştığıdır. Büyük bir tabloda tam tarama, genellikle performans sorununun en açık işaretidir.

Buradaki hedef, veritabanı motorunun iç mimarisini derinlemesine öğrenmek değil; sorgunun "nerede zaman kaybettiğini" görebilecek kadar bu planı okuyabilmektir. Bu, hangi tekniği uygulamanız gerektiğine dair en güvenilir ipucunu verir.

İndeksleme: Doğru Kullanıldığında Güçlü, Aşırıya Kaçıldığında Riskli

İndeksi basitçe bir kitabın sonundaki dizin gibi düşünebilirsiniz: dizin olmadan belirli bir konuyu bulmak için kitabın tüm sayfalarını taramanız gerekir; dizin varsa doğrudan ilgili sayfaya gidersiniz. Veritabanında da indeks, belirli sütunlardaki değerlere hızlıca ulaşmayı sağlayan bir yapıdır.

İndeks eklemek özellikle şu durumlarda mantıklıdır: bir sütun sık sık filtreleme (WHERE koşulunda) kullanılıyorsa, sıralama (ORDER BY) için kullanılıyorsa veya tablolar arası birleştirme (JOIN) işlemlerinde kullanılıyorsa. Bu sütunlara indeks eklemek, sorguların tam tarama yapmak zorunda kalmadan doğrudan ilgili satırlara ulaşmasını sağlar.

Ancak indeksleme bedelsiz değildir. Her yeni indeks, veri her eklendiğinde, güncellendiğinde veya silindiğinde ayrıca güncellenmesi gereken bir yapı demektir — bu da yazma işlemlerini yavaşlatır. Ayrıca indeksler disk alanı kaplar. Bu yüzden "her sütuna indeks eklemek" güvenli bir strateji değildir; zamanla hiç kullanılmayan veya nadiren işe yarayan indeksler birikir, sistemi yavaşlatır ve bakımını karmaşıklaştırır. Doğru yaklaşım, gerçekten sık kullanılan ve sorgu planında tam tarama olarak görünen sütunları hedeflemektir.

SELECT * Kullanmaktan Kaçınmak

SELECT * yazmak pratik görünür çünkü hangi sütunları seçeceğinizi düşünmenize gerek kalmaz. Ancak bu rahatlığın gizli bir maliyeti vardır: veritabanı ihtiyaç duyulmayan sütunları da diskten okur, ağ üzerinden taşır ve uygulamanın belleğine yükler. Tablo çok sütunlu veya bazı sütunlar büyük veri içeriyorsa (örneğin metin veya ikili veri alanları), bu fark ciddi bir performans kaybına dönüşebilir.

Sadece gerçekten kullanılan sütunları seçmek, hem veri transferini azaltır hem de kodun okunabilirliğini artırır — sorguya bakan biri hangi verinin kullanıldığını doğrudan görebilir. Ayrıca şema değiştiğinde (örneğin tabloya yeni bir sütun eklendiğinde), SELECT * kullanan sorgular öngörülemeyen şekillerde davranış değiştirebilir; uygulama kodu beklemediği bir sütunla karşılaşabilir veya gereksiz veri taşımaya başlar. Sütunları açıkça belirtmek bu riski ortadan kaldırır.

N+1 Sorgu Problemi ve ORM Tuzağı

N+1 problemi, bir listeyi çekmek için 1 sorgu çalıştırdıktan sonra, o listedeki her öğe için ilişkili veriyi ayrı ayrı sorgulamak zorunda kalmaktan doğar. Örneğin 50 blog yazısı çekildiğinde, her yazının yazarını ayrı ayrı sorgulamak toplamda 51 sorguya yol açar. Veri az olduğunda bu fark edilmeyebilir, ama liste büyüdüğünde sorgu sayısı da doğrusal olarak artar ve performans hızla kötüleşir.

Bu problem özellikle ORM (nesne-ilişkisel eşleme) araçları kullanılırken kolayca ortaya çıkar, çünkü ORM'ler ilişkili verilere erişimi çok basit hale getirir; bir nesnenin ilişkili alanına erişmek, arka planda otomatik olarak ayrı bir sorgu tetikleyebilir. Kod yazan kişi bunun farkında olmayabilir, çünkü görünürde tek bir döngü vardır.

Çözüm genellikle "eager loading" (istekli yükleme) denilen yaklaşımdır: ilişkili verinin ayrı ayrı değil, başta tek bir sorguyla (veya sınırlı sayıda sorguyla) birlikte çekilmesi. Bu, ORM'lerin çoğunda ilişkiyi önceden belirtme veya birleştirme talebiyle yapılır. Önemli olan, bu davranışı fark edebilmek; çoğu zaman execution plan yerine, bir işlem sırasında kaç sorgu çalıştığını gözlemlemek bu problemi ortaya çıkarır.

JOIN Kullanımında Dikkat Edilmesi Gerekenler

JOIN, ilişkili veriyi tek sorguda birleştirmenin doğal yoludur ve genellikle N+1 probleminin çözümüdür. Ancak JOIN de dikkatsiz kullanıldığında performans sorununa dönüşebilir.

Öncelikle, gerçekten ihtiyaç duyulmayan tabloları sorguya dahil etmek gereksiz iştir. Bir sorguda kullanılmayan bir tabloyla JOIN yapmak, veritabanına ekstra iş yükler ve sonuç kümesini büyütür. İkinci olarak, çok sayıda tabloyu zincirleme birleştiren karmaşık sorgular, her adımda daha fazla veri işlenmesine yol açabilir; bazen sorguyu birkaç basit adıma bölmek, tek bir aşırı karmaşık JOIN zincirinden daha performanslı olabilir.

Son ve kritik nokta: JOIN yapılan sütunların indekslenmiş olması gerekir. İndekssiz bir sütun üzerinden JOIN yapmak, veritabanının eşleşen satırları bulmak için büyük taramalar yapmasına neden olur. JOIN performansı incelenirken execution plan'a bakmak, hangi birleştirmenin tam tarama gerektirdiğini görmek için iyi bir başlangıç noktasıdır.

Büyük Veri Setlerinde Sayfalama Stratejileri

Kullanıcıya veri listelerini sayfa sayfa göstermek yaygın bir ihtiyaçtır ve genellikle "offset" tabanlı bir yaklaşımla yapılır: belirli bir sayıda satırı atla, sonra istenen sayıda satırı getir. Bu yöntem küçük veri setlerinde sorunsuz çalışır, ancak veri büyüdükçe ciddi bir sorun ortaya çıkar: veritabanı, atlanacak satırları da önce bulmak zorundadır. Yani 100.000. sayfayı istediğinizde, veritabanı önce önceki 99.999 sayfalık veriyi bir şekilde işlemek zorunda kalabilir — bu da sayfa numarası büyüdükçe sorguyu yavaşlatır.

Alternatif bir yaklaşım, "bir önceki sonuçtan devam et" mantığıdır: her sayfa, önceki sayfanın son kaydının bir referans noktası (örneğin bir kimlik veya tarih değeri) alınarak, "bundan sonraki N kayıt" şeklinde sorgulanır. Bu yaklaşım, indekslenmiş bir sütun üzerinden ilerlediği için veri hacmi büyüse de performansı nispeten sabit tutar. Her senaryoda offset tabanlı sayfalamanın yerini tutmasa da (örneğin belirli bir sayfaya doğrudan atlamak istendiğinde), büyük veri setlerinde tercih edilen bir yöntemdir.

Bunun yanında basit ama etkili bir prensip de: kullanıcıya gerçekten göstereceğiniz kadar veri çekmek. "Belki daha sonra gerekir" diyerek gereğinden fazla satır veya sütun çekmek, sayfalamanın getirdiği avantajı baştan zayıflatır.

Önbellekleme ile Veritabanı Yükünü Azaltmak

Bazı veriler sık sık istenir ama nadiren değişir — bir ürün kataloğu, bir ayarlar listesi, sık ziyaret edilen bir profil sayfası gibi. Bu tür veriler için her istekte veritabanına gitmek gereksiz bir yüktür; aynı sorgu, aynı sonucu üretmek için tekrar tekrar çalıştırılır.

Önbellekleme katmanı, bu tür verilerin bir kopyasını daha hızlı erişilebilen bir yerde (örneğin bellek içi bir depoda) tutarak, her istekte veritabanına gitme ihtiyacını ortadan kaldırır. Bu, veritabanı üzerindeki toplam yükü önemli ölçüde azaltabilir, çünkü sık istenen sorguların çoğu artık veritabanına hiç ulaşmaz.

Ancak önbellekleme bedelsiz bir çözüm değildir; asıl zorluk, verinin değiştiği anda önbellekteki kopyanın da güncellenmesi veya geçersiz kılınmasıdır (invalidation). Eğer bu doğru yönetilmezse, kullanıcılara güncel olmayan veri gösterilebilir. Bu yüzden önbellekleme kararı, hangi verinin ne sıklıkla değiştiği ve güncelliğin ne kadar kritik olduğu düşünülerek verilmelidir; her veri önbelleklemeye uygun değildir.

Şema Tasarımı: Normalizasyon ve Denormalizasyon Dengesi

Veritabanı şeması tasarlarken genellikle "normalizasyon" ilkesi izlenir: veriyi tekrarsız, mantıksal parçalara bölerek tutarlılığı korumak. Örneğin bir müşterinin adresini her siparişte tekrar tekrar yazmak yerine, ayrı bir müşteri tablosunda bir kez tutup siparişlerden buna referans vermek. Bu yaklaşım veri tutarsızlıklarını önler ve güncellemeleri kolaylaştırır.

Ancak aşırı normalizasyon, bir bilgiyi elde etmek için çok sayıda tabloyu JOIN ile birleştirmeyi gerektirebilir. Her JOIN bir maliyet getirdiğinden, çok parçalı bir şema, okuma ağırlıklı sorgularda performansı olumsuz etkileyebilir.

Bu noktada bazı durumlarda kontrollü bir "denormalizasyon" tercih edilir: belirli bir veriyi, tekrar tekrar hesaplamak veya JOIN ile birleştirmek yerine, doğrudan ilgili tabloda bir kopya olarak tutmak. Bu, veri tutarlılığı açısından ek bir dikkat gerektirir (kopyanın güncel tutulması gerekir), ama okuma performansını belirgin şekilde artırabilir.

Hangi dengenin doğru olduğu projeye bağlıdır. Çok fazla yazma işlemi yapan ve tutarlılığın kritik olduğu bir sistemde normalizasyona yakın kalmak mantıklıyken, okuma ağırlıklı ve performansın öncelikli olduğu bir sistemde belirli noktalarda denormalizasyon faydalı olabilir. Bu bir "ya hep ya hiç" kararı değil, ihtiyaca göre ayarlanan bir spektrumdur.

Sonuç: Küçük Adımlarla, Ölçerek İlerlemek

Burada anlatılan tekniklerin — doğru indeksleme, gereksiz sütunları çekmemek, N+1 problemini önlemek, JOIN'leri dikkatli kullanmak, sayfalama stratejisini seçmek, önbellekleme ve şema tasarımı — hepsini aynı anda uygulamaya çalışmak gerekmez. Aksine böyle bir yaklaşım, hangi değişikliğin gerçekten fayda sağladığını anlamayı zorlaştırır.

Daha sağlıklı bir yol, önce en büyük etkiyi yaratacak noktayı bulmaktır. Bu genellikle execution plan'a bakarak veya en sık çalışan ve en yavaş sorguları tespit ederek başlar. Bir sorunu çözdükten sonra ölçüm yapıp gerçekten iyileşme olduğunu doğrulamak, sonra sıradaki en büyük darboğaza geçmek, zamanla kod tabanının tamamını daha sağlıklı bir performans seviyesine taşır.

Sonuçta bu yazıda geçen her teknik bir araçtır, kesin bir kural değil. Hangi tekniğin ne zaman işe yaradığı, gerçek veriyle yapılan ölçümle anlaşılır — sezgiyle değil.

Kod Katmanı Editör Ekibi

Kodun özünü keşfetmek

Tüm yazıları

Sen ne düşünüyorsun?

Yorumlar onaylandıktan sonra yayınlanır. E-posta adresin yayınlanmaz.

Okumaya devam et

Tüm yazılar
Bülten

Haftanın fikirleri, gelen kutunda.

Teknoloji ve daha fazlası için hazırladığımız haftalık özeti kaçırma.