İki motor
| Depolama motoru (SE, VertiPaq) | Formül motoru (FE) | |
|---|---|---|
| Ne yapar? | Sıkıştırılmış sütunları tarar, filtreler, toplar | DAX mantığını yürütür: IF, iç içe yineleyiciler, karmaşık ifadeler |
| Paralel mi? | Çok çekirdekli | Tek iş parçacığı |
| Önbellek | Var (aynı veri isteği tekrar hesaplanmaz) | Yok |
| Hedef | İşin çoğu burada yapılsın | Az ve küçük ara sonuçlarla çalışsın |
DAX Studio → Server Timings: toplam süre, SE süresi, FE süresi, SE sorgu sayısı ve her SE sorgusunun döndürdüğü satır sayısı. Yavaş bir ölçüde genellikle iki desen görürsün: (1) çok sayıda küçük SE sorgusu (yineleyicide bağlam geçişi), (2) milyonlarca satırlık bir ara sonucun FE'ye taşınması (materyalizasyon).
Geri çağırma (callback)
SE basit aritmetiği kendi yapabilir; ama IF, DIVIDE gibi bazı ifadeler satır satır FE'ye “geri çağırma” ister (CallbackDataID). SUMX(Satislar, IF(Satislar[Indirim] > 0, Satislar[Tutar])) yerine CALCULATE(SUM(Satislar[Tutar]), Satislar[Indirim] > 0) yazmak işi tamamen SE'ye verir.
Reçeteler
| Belirti | Neden | Çözüm |
|---|---|---|
| Ölçü tek başına hızlı, matriste yavaş | Her hücrede tekrar eden pahalı alt ifade | VAR ile bir kez hesapla; mümkünse dışa taşı |
| Çok sayıda SE sorgusu | Büyük tabloda yineleyici + ölçü çağrısı | Boyutun anahtarı üzerinde dolaş: SUMX(VALUES(Urunler[UrunNo]), …) |
| FE süresi yüksek | Büyük materyalizasyon (FILTER(Satislar…)) | Sütun filtresi kullan; KEEPFILTERS ile kesişim |
| Yüksek kardinaliteli sütun filtresi yavaş | Milyonlarca farklı değer | Modelde sütunu böl (tarih/saat), gruplanmış sütun ekle |
| IFERROR / ISERROR yaygın | Hata yakalama satır satır FE'de | DIVIDE ve doğru veri türleri |
| Çift yönlü ilişkiler | Filtre zincirleri ve belirsizlik | Tek yön + gerektiğinde CROSSFILTER |
| Görsel çok satırlı | Görselde 10.000+ satır | İlk N filtresi, sayfalandırma, detay sayfası |
Ölçme yöntemi
- Performans Analizi'nde yavaş görseli bul → Sorguyu kopyala.
- DAX Studio'da (ya da DAX sorgu görünümünde) sorguyu çalıştır; önbelleği temizleyerek (Clear Cache) ölç.
- Ölçüyü değiştir, aynı koşulda tekrar ölç. Tek değişiklik, tek ölçüm.
- Sonuçların aynı olduğunu doğrula (performans için doğruluktan vazgeçme).
Formula Engine ve Storage Engine
Bir DAX sorgusu iki motorda çalışır. Storage Engine (VertiPaq), sıkıştırılmış sütunları çok çekirdekli ve çok hızlı tarar; ama yalnızca basit işler bilir: filtrele, grupla, topla, say. Formula Engine geri kalan her şeyi yapar: koşullar, iç içe iteratörler, karmaşık mantık. Formula Engine tek çekirdekte çalışır.
Optimizasyonun özü, işi Storage Engine’e bırakmaktır:
- Sütun filtresi tablo filtresinden hızlıdır.
Urunler[Fiyat] > 1000doğrudan Storage Engine sorgusuna dönüşür;FILTER(Urunler, ...)çoğu zaman Formula Engine’de satır satır değerlendirilir. - İteratör içinde IF “geri çağırma” (callback) üretir.
SUMX(Satislar, IF(...))her satır için Formula Engine’e döner. Koşulu CALCULATE filtresine taşıyabiliyorsan taşı. - Değişkenler tekrar hesaplamayı önler. Aynı ölçüyü bir formülde üç kez çağırmak, üç ayrı hesaplama demektir; VAR ile bir kez hesaplanır.
- Kardinalite her şeyden önemlidir. Bir sütundaki farklı değer sayısı sıkıştırmayı ve tarama hızını belirler. Tarih-saat sütununu tarih ve saat diye ikiye bölmek, modeli birkaç kat küçültebilir.
Pahalı Satırlar Satışı = CALCULATE([Net Satış], Satislar[Tutar] > 2000)
Bu yazım, aynı sonucu veren SUMX(Satislar, IF(Satislar[Tutar] > 2000, Satislar[Tutar])) gibi bir iteratörden daha ölçeklenebilirdir. Gerçek modelde farkı görmek için Performans Analizi ile sorguyu kopyalayıp DAX Studio’da “Server Timings” ile SE ve FE sürelerine bak.
Server Timings'i okumak: bir ekranı adım adım yorumla
Bir matris 3 saniyede açılıyor. Performans Analizi'nden sorguyu kopyaladın, DAX Studio'da Clear Cache sonrası Server Timings açıkken çalıştırdın. Aşağıdaki değerler örnek bir ekrandır (bu modelden ölçülmüş değil); amaç okumayı öğrenmek:
| Alan | Örnek değer | Nasıl okunur? |
|---|---|---|
| Total | 3.050 ms | Sorgunun toplam süresi |
| FE | 2.700 ms (%89) | Formül motoru tek iş parçacığında çalışıyor; asıl yük burada |
| SE | 350 ms (%11) | Depolama motorunda geçen süre |
| SE CPU | 1.400 ms | SE CPU / SE ≈ 4: tarama paralel çalışmış, SE sağlıklı |
| SE Queries | 612 | Çok sayıda küçük SE sorgusu: yineleyici içinde bağlam geçişi |
| SE Cache | 540 | Sorguların çoğu önbellekten geldi; aynı veri tekrar tekrar isteniyor |
Bu tablo tek başına bir teşhis koyar: işin çoğu FE'de, SE sorguları çok ve küçük. Neden genellikle bir yineleyicinin her satırında ölçü çağrılmasıdır. Sonraki adım sorgu listesine bakmaktır: her xmSQL satırında döndürülen satır sayısı (Rows) ve boyut (KB) yazar. Tek bir sorgu 1,6 milyon satır döndürüyorsa sorun materyalizasyondur: SE büyük bir ara tabloyu FE'ye taşımıştır. xmSQL metninde CallbackDataID görüyorsan SE satır satır FE'ye geri çağırma yapıyordur.
Query Plan sekmesinde mantıksal ve fiziksel plan vardır. Fiziksel planda büyük “Records” sayılı Spool satırları, FE'nin bellekte tuttuğu ara sonuçlardır. Hedef: az sayıda SE sorgusu, küçük ara sonuçlar, callback yok.
Sütun filtresi, KEEPFILTERS ve FILTER(tablo): hız ve anlam birlikte
Pazarlama bir “Web Satışı” ölçüsü istiyor ve onu Kanal satırlı bir matrise koyacak. Üç yazımın hızı ve sonucu farklıdır:
EVALUATE
SUMMARIZECOLUMNS(
Satislar[Kanal],
"Net Satış", [Net Satış],
"Sütun filtresi", CALCULATE([Net Satış], Satislar[Kanal] = "Web"),
"KEEPFILTERS", CALCULATE([Net Satış], KEEPFILTERS(Satislar[Kanal] = "Web"))
)
Web satırında üçü de 457.330,90 ₺ verir. Mobil satırında ise sütun filtresi yine 457.330,90 ₺ gösterir: Satislar[Kanal] = "Web", satırdan gelen “Kanal = Mobil” filtresinin üzerine yazar. KEEPFILTERS iki filtreyi kesiştirir; Mobil ve Mağaza satırları boş kalır. İkisi de SE'de çözülür, hız aynıdır; fark tamamen anlamdadır.
Üçüncü yol FILTER(Satislar, Satislar[Kanal] = "Web") yazmaktır. Bu, olgu tablosunu satır satır dolaşıp tüm sütunlarıyla bir tablo filtresi üretir. Büyük tabloda materyalizasyon demektir ve filtre genişletilmiş tablonun bütün sütunlarına uygulanır; sonucu tahmin etmek zorlaşır. Kural: tek sütun koşulu → sütun filtresi; mevcut seçime saygı → KEEPFILTERS; FILTER yalnızca ölçüye dayalı koşulda ve küçük bir tablo üzerinde (ör. FILTER(VALUES(Urunler[UrunNo]), [Net Satış] > 5000)).
[Net Satış] ölçüsü Satislar[Durum] <> "İptal" sütun filtresiyle yazılmıştır. Bu yüzden Durum satırlı bir tabloda her satırda 1.250.772,45 ₺ görünür; İptal satırı dahil. Aynı ölçü KEEPFILTERS ile yazılsaydı İptal satırı boş, Teslim Edildi 1.049.627,00 ₺ olurdu. Ölçüyü hangi sütunlarla kesileceğini düşünerek yaz.IF, SWITCH ve kısa devre
Motor, IF ve SWITCH'te çoğu zaman yalnızca gereken dalı hesaplar. Ama koşul satıra bağlıysa ve bir yineleyicinin içindeyse, SE her iki dalın verisini önceden toplayabilir. Bu bir hata değildir; bazen tek bir geniş SE sorgusu, satır başına küçük sorgulardan hızlıdır. IF.EAGER bu davranışı bilerek ister. Kendi tarafında yapabileceğin iki şey var:
- Ucuz kontrolü öne al:
IF(ISBLANK([Net Satış]), BLANK(), <pahalı ifade>). Satışı olmayan hücrelerde pahalı kısım hesaplanmaz; ayrıca boş satırlar matristen düşer. - SWITCH(TRUE(), …) sırası: En sık doğru olan koşulu ilk sıraya koy; koşullarda ölçü değil, değişken kullan.
Tersi de bir anti-kalıptır: IF([Net Satış] > 0, [Net Satış], 0) gibi “boş yerine 0” yazımları, satışı olmayan her ürün-gün bileşimini görünür kılar. Matris 40 ürün × 1.096 gün = 43.840 satıra çıkabilir ve yavaşlık DAX'tan değil, sıfırların kendisinden gelir.
DISTINCTCOUNT'un bedeli
[Müşteri Sayısı] ve [Sipariş Sayısı] DISTINCTCOUNT kullanır. Farklı sayım toplanamaz: ayların müşteri sayılarını toplayıp yıl bulamazsın, çünkü aynı müşteri birden çok ayda alışveriş yapar. Bu yüzden motor her hücre için sütunun sözlüğünü yeniden tarar; kardinalite yükseldikçe maliyet artar. Toplama tabloları da çoğu zaman farklı sayımı karşılayamaz.
COUNTROWS(VALUES(Satislar[MusteriNo]))aynı sonucu (bu modelde 136) verir ve çoğu zaman aynı planı üretir; tek başına hızlandırmaz.- Asıl kazanç kardinaliteden gelir: birleştirilmiş metin anahtar (“Musteri-Urun”) üzerinde farklı sayım yerine iki tam sayı sütun ya da önceden hesaplanmış bir anahtar kullan.
- DirectQuery'de yaklaşık sayım (
APPROXIMATEDISTINCTCOUNT) kaynağa itilebilir; “yaklaşık” olduğu raporda belirtilmeli. Bu oyun alanının motorunda yoktur.
Değişken: bir kez hesapla, ama doğru yerde
Değişken tanımlandığı yerdeki bağlamda bir kez hesaplanır ve sonra sabittir. Bu hem hız kazandırır hem de sık bir hataya yol açar:
Yanlış 2023 Satışı = VAR s = [Net Satış] RETURN CALCULATE(s, Tarih[Yil] = 2023) Doğru 2023 Satışı = CALCULATE([Net Satış], Tarih[Yil] = 2023)
Toplamda yanlış ölçü 1.250.772,45 ₺ (bütün yıllar), doğru ölçü 400.869,55 ₺ verir; 2024 satırında yanlış ölçü 747.065,70 ₺ gösterir. CALCULATE bir sabitin bağlamını değiştiremez. Kural: değişkeni, değerini istediğin bağlamda tanımla; CALCULATE ile değiştirilecek bir şeyse değişkene ölçünün kendisini değil, sonucu kullanılacak tabloyu ya da filtreyi koy.
Karar ağacı
- Önce ölç: Clear Cache + Server Timings. FE mi SE mi baskın?
- SE baskın ve sorgu az ama büyükse: model sorunu (kardinalite, ilişki, sütun sayısı) → Performans ve VertiPaq dersi.
- FE baskın ve SE sorgusu çoksa: yineleyiciyi boyut anahtarına taşı, bağlam geçişini azalt.
- CallbackDataID varsa: IF/koşulu CALCULATE filtresine taşı.
- Büyük materyalizasyon varsa: FILTER(olgu) yerine sütun filtresi ya da KEEPFILTERS.
- Aynı ölçü bir formülde birden çok kez geçiyorsa: VAR.
- Her değişiklikten sonra sonucu karşılaştır; hızlı ama yanlış ölçü, yavaş ve doğru ölçüden kötüdür.