Yıldız şemada her ilişki çoka-birdir: bir satış satırı tek bir ürüne, tek bir müşteriye bağlanır. Gerçek iş hayatı bu kadar düzenli değildir. “Model tasarımı II” dersinde ayrıntı düzeyini (grain), “Rol oynayan boyutlar” dersinde CROSSFILTER’ı, “Tablo fonksiyonları” dersinde köprü tablonun kısa tanımını gördün. Bu ders, çoka-çok (many-to-many) durumları tanımayı, modelde ve ölçüde çözmeyi ve toplam satırını kullanıcıya doğru anlatmayı öğretiyor.
Anadolu Market’te bu hafta gelen üç talep:
- Proje yönetim ofisi: “Fabric veri platformu geçişi, Veri Analitiği ile Yazılım’ın ortak projesi. Departman raporunda bütçe iki departmanda da görünmeli, ama şirket toplamı şişmemeli.”
- Pazarlama: “Elektronik alan müşterilerimizin toplam cirosu, yani bütün kategorilerdeki harcaması ne?”
- Satış planlama: “Hedefler kategori ve ay düzeyinde. Ürün tablosundaki kategori sütunuyla rapor yapınca hedef her satırda aynı.”
1 · Çoka-çok’un üç yüzü
| Tür | Örnek | Tipik çözüm |
|---|---|---|
| İki boyut arasında çok değerli ilişki | Bir proje birden çok departmanda, bir departmanın birden çok projesi var | Köprü tablo (ProjeDepartman), gerekirse ağırlık sütunu |
| Olgu üzerinden iki boyut | Bir müşteri birden çok kategoriden alır, bir kategoriden birçok müşteri alır | Olgu tablosu zaten köprüdür; ölçüde küme filtresi ya da CROSSFILTER |
| Farklı taneciklikte olgu | Hedefler kategori × ay, satış ürün × gün | Ortak boyut (Kategoriler); olmazsa TREATAS ya da çoka-çok kardinalite |
Hepsinin ortak sonucu aynıdır: satırların toplamı, toplam satırına eşit değildir. Bir proje iki departmanda görünür, toplamda bir kez sayılır. Bu bir hata değil, çoka-çok’un doğasıdır; hata, bunu kullanıcıya söylememektir.
2 · Köprü tablo: proje ↔ departman
Modelde Projeler[DepartmanNo] tek bir departman tutuyor. Ortak projeleri göstermek için bir köprü tablo gerekir: her satırı “bu proje, bu departmana, şu payla aittir” cümlesidir. Modelde şöyle kurulur:
Departmanlar[DepartmanNo] 1 ──> * ProjeDepartman[DepartmanNo]
Projeler[ProjeNo] 1 <─> * ProjeDepartman[ProjeNo] (çift yönlü)
ProjeDepartman: ProjeNo | DepartmanNo | Pay
5 | 2 (Pazarlama) | 0,7
5 | 1 (Satış) | 0,3
7 | 3 (Veri Analitiği) | 0,6
7 | 4 (Yazılım) | 0,4
12 | 3 (Veri Analitiği) | 0,5
12 | 1 (Satış) | 0,3
12 | 2 (Pazarlama) | 0,2
Filtrenin Departmanlar’dan Projeler’e ulaşması için köprü ile Projeler arasındaki ilişkinin çift yönlü olması gerekir: filtre Departmanlar → köprü yönünde doğal akar, köprü → Projeler yönünde ise “çoktan bire” geri gitmesi gerekir. Bu uygulamanın modelinde köprü tablo yok; aynı mantığı ölçünün içinde, sanal bir köprüyle deneyebilirsin. Önce ağırlıklı (paylaştırılmış) bütçe:
Paylaştırılmış Bütçe =
VAR Kopru =
DATATABLE(
"ProjeNo", INTEGER, "DepartmanNo", INTEGER, "Pay", DOUBLE,
{
{5, 2, 0.7}, {5, 1, 0.3},
{7, 3, 0.6}, {7, 4, 0.4},
{12, 3, 0.5}, {12, 1, 0.3}, {12, 2, 0.2}
}
)
VAR Gorunen = FILTER(Kopru, [DepartmanNo] IN VALUES(Departmanlar[DepartmanNo]))
RETURN
SUMX(Gorunen, [Pay] * LOOKUPVALUE(Projeler[Butce], Projeler[ProjeNo], [ProjeNo]))
Sonra ağırlıksız, “dokunduğu projelerin tam bütçesi”:
Katıldığı Projelerin Bütçesi =
VAR Kopru =
DATATABLE(
"ProjeNo", INTEGER, "DepartmanNo", INTEGER, "Pay", DOUBLE,
{
{5, 2, 0.7}, {5, 1, 0.3},
{7, 3, 0.6}, {7, 4, 0.4},
{12, 3, 0.5}, {12, 1, 0.3}, {12, 2, 0.2}
}
)
VAR Projeler_ = SELECTCOLUMNS(FILTER(Kopru, [DepartmanNo] IN VALUES(Departmanlar[DepartmanNo])), "P", [ProjeNo])
RETURN
CALCULATE([Proje Bütçesi], REMOVEFILTERS(Departmanlar), TREATAS(Projeler_, Projeler[ProjeNo]))
| Departman | Paylaştırılmış | Tam bütçe |
|---|---|---|
| Pazarlama | 165.000 ₺ | 450.000 ₺ |
| Satış | 135.000 ₺ | 450.000 ₺ |
| Veri Analitiği | 402.000 ₺ | 720.000 ₺ |
| Yazılım | 168.000 ₺ | 420.000 ₺ |
| Satırların toplamı | 870.000 ₺ | 2.040.000 ₺ |
| Toplam satırı | 870.000 ₺ | 870.000 ₺ |
Üç projenin gerçek bütçesi 870.000 ₺. Paylaştırılmış ölçü toplanabilir: satırlar toplama eşittir. Tam bütçe ölçüsü toplanamaz: her departman “benim projem” der, satırlar 2.040.000 ₺ eder. İkisi de doğrudur, ama farklı soruları cevaplar. Departman müdürü “sorumlu olduğum işlerin büyüklüğü”nü, finans “maliyetin kime yazılacağı”nı sorar. Raporda ikisini aynı ada koyma.
3 · Olgu tablosu bir köprüdür: müşteri ↔ kategori
Pazarlamanın sorusu: “Elektronik alanların toplam cirosu.” Burada köprü tablo kurmana gerek yok; Satislar zaten her müşteriyi aldığı kategorilere bağlıyor. Yapılacak iş, iki adımlı bir filtre:
- Görünen kategoride alışveriş yapan müşterilerin kümesini bul.
- Kategori filtresini kaldır, bu müşterilerin tüm satışını hesapla.
Alıcıların Toplam Satışı =
VAR Alicilar = CALCULATETABLE(VALUES(Satislar[MusteriNo]), Satislar[Durum] <> "İptal")
RETURN
CALCULATE([Net Satış], Alicilar, REMOVEFILTERS(Kategoriler), REMOVEFILTERS(Urunler))
| Kategori | Alıcı sayısı | Kategorideki satış | Alıcıların toplam satışı |
|---|---|---|---|
| Elektronik | 95 | 339.304 ₺ | 1.104.856 ₺ |
| Kitap | 113 | 151.882 ₺ | 1.153.184 ₺ |
| Hediye | 30 | 17.370 ₺ | 328.027 ₺ |
| Toplam satırı | 136 | 1.250.772 ₺ | 1.250.772 ₺ |
Kitap alıcıları, şirket cirosunun %92’sini yapıyor: Kitap küçük bir kategori ama çok geniş bir müşteri kitlesine dokunuyor. Bu bilgi kategori satışından görünmez. Yedi kategorinin alıcı sayılarını toplarsan 628 eder, oysa tekil müşteri 136’dır. Bir müşteri ortalama 4,6 kategoride görünür.
Tek yönlü filtrenin tuzağı
“En az 4 kategoriden alan müşteri” sorusunda çoğu kişi CALCULATE(DISTINCTCOUNT(Urunler[Kategori])) yazar ve her müşteride 7 bulur. Sebep: filtre Satislar’dan Urunler’e akmaz. Müşteri filtresi Satislar’ı süzer ama Urunler tablosunun bütün satırları görünür kalır. Çözüm ya ölçü içinde CROSSFILTER(Satislar[UrunNo], Urunler[UrunNo], BOTH) ya da olgu üzerinden saymak: COUNTROWS(SUMMARIZE(Satislar, Urunler[Kategori])). İkisiyle de dağılım şöyle çıkar: 7 kategoride 13, 6’da 40, 5’te 22, 4’te 28 müşteri; yani 136 müşterinin 103’ü en az 4 kategoriden almış.
4 · Çift yönlü filtre: ne zaman, neden riskli?
Çift yönlü (both) ilişki, filtrenin bir taraftan çok tarafa olduğu gibi çok taraftan bir tarafa da akmasını sağlar. Kolay görünür; üç bedeli vardır.
Bedel 1: anlam sessizce değişir
Kanal kırılımında CALCULATE([Kayıtlı Müşteri], CROSSFILTER(Satislar[MusteriNo], Musteriler[MusteriNo], BOTH)) Mağaza’da 73 verir. [Müşteri Sayısı] ise 70. Fark, bu kanaldaki siparişlerinin hepsi iptal olan 3 müşteri: çift yönlü filtre “bu kanalda satırı olan” müşteriyi sayar, iş kuralını (iptal hariç) bilmez. Kullanıcı modelde ilişkiyi çift yönlü görünce bütün müşteri sayılarının bu yolla geldiğini varsayar.
Bedel 2: belirsiz yollar
Modelde Talepler hem Tarih’e hem Musteriler’e bağlı. Satislar–Musteriler ilişkisini çift yönlü yaparsan, Tarih’ten Talepler’e iki yol oluşur: doğrudan Tarih → Talepler ve Tarih → Satislar → Musteriler → Talepler.
Yol 1: Tarih ──> Talepler Yol 2: Tarih ──> Satislar ──(çift yönlü)──> Musteriler ──> Talepler
Microsoft Learn’e göre Power BI bu durumda yolları önceliğe göre sıralar (önce doğrudan bire-çok yollar), eşit öncelik ve ağırlıkta birden çok yol kalırsa “belirsiz yol” hatası verir. Desktop çift yönlü değişikliği kaydetmeyi reddedebilir ya da belirsizliğe izin verip bir yolu seçebilir. Her iki durumda da [Talep Sayısı] gibi masum bir ölçünün anlamı, modeli kimin ne zaman değiştirdiğine bağlı hâle gelir.
Bedel 3: performans ve güvenlik
Çift yönlü ilişki her sorguda ek filtre yayılımı demektir ve büyük modellerde belirgin biçimde yavaştır. Satır düzeyi güvenlikte (RLS) güvenlik filtresi varsayılan olarak tek yönde akar; ilişki özelliklerindeki “Güvenlik filtresini her iki yönde de uygula” seçeneği açılmadıkça çift yönlü ilişki RLS’yi taşımaz. Bu seçeneği açmak ise rol tasarımını karmaşıklaştırır.
CROSSFILTER(…, BOTH) ile, açıkça ve yalnızca kendi içinde istesin. İstisna: köprü tablo ile ona bağlı boyut arasındaki ilişki; orada çift yön desenin parçasıdır ve belgelenmelidir.5 · Farklı taneciklik: hedef kategori düzeyinde, rapor ürün tablosundan
Satış planlamanın sorunu: görselde kategoriyi Urunler[Kategori]’den kullanınca [Hedef] her satırda 1.299.300 ₺ gösteriyor. Hedefler, Urunler’e bağlı değil; Kategoriler üzerinden bağlı. Üç çözüm var:
| Yol | Nasıl? | Artı | Eksi |
|---|---|---|---|
| Ortak boyut | Görselde Kategoriler[Kategori]; Urunler[Kategori] gizlenir | Hızlı, düzenli ilişki, ölçü değişmez | Kullanıcıyı doğru alana yönlendirmek gerekir |
| TREATAS (sanal ilişki) | Ölçü içinde Urunler[Kategori] değerleri Hedefler[Kategori]’ye uygulanır | Modele dokunmaz, hangi ölçüde geçerli olduğu açık | Her ölçüye ayrı yazılır, bakım yükü |
| Çoka-çok kardinalite | Hedefler[Kategori] *—* Urunler[Kategori] ilişkisi | Kodsuz, sürükle-bırak | Sınırlı ilişki: yavaş, RELATED yok, boş satır yok |
Hedef (Ürün Kategorisinden) = CALCULATE([Hedef], TREATAS(VALUES(Urunler[Kategori]), Hedefler[Kategori]))
Urunler[Kategori] kırılımında artık Elektronik 351.800 ₺, Kitap 156.200 ₺ gösterir; toplam 1.299.300 ₺.
Sınırlı (limited) ilişki ne demek?
Learn’e göre bir ilişki, motor “bir” tarafını garanti edemediğinde sınırlıdır: çoka-çok kardinalite (sütunlar benzersiz olsa bile) ve bileşik modellerde farklı kaynak grupları arasındaki ilişkiler. Sınırlı ilişkinin davranışı düzenli ilişkiden farklıdır:
- Birleşme iç birleşme (inner join) mantığıyla yapılır; eşleşmeyen satırlar için boş sanal satır eklenmez. Hedefte olup ürün tablosunda olmayan bir kategori raporda “(Boş)” satırı olarak görünmez, sessizce kaybolur.
- RELATED ile “bir” taraftaki değer alınamaz.
- Tablo genişlemesi olmaz; birinci derste gördüğün “ALL(Hedefler) Kategoriler filtresini de kaldırır” kuralı burada işlemez.
- İçe aktarma modelinde ilişki için yapı önceden oluşturulmaz, birleşme sorgu anında çözülür; daha yavaştır.
-- Model görünümünde (bu uygulamanın motorunda kurulamaz) Hedefler[Kategori] * ── * Urunler[Kategori] Çapraz filtre yönü: Tek (Urunler filtreler Hedefler’i)
6 · Ortak boyut mu, çift yönlü mü, TREATAS mı? Toplam satırındaki fark
Gider türüne göre proje bütçesini görmek isteyelim. Giderler ve Projeler, Departmanlar üzerinden birbirine dokunuyor; ama gider türü filtresinin projelere ulaşması için filtrenin Giderler’den Departmanlar’a geri akması gerekir.
Gider Türü Proje Bütçesi (CROSSFILTER) = CALCULATE([Proje Bütçesi], CROSSFILTER(Giderler[DepartmanNo], Departmanlar[DepartmanNo], BOTH)) Gider Türü Proje Bütçesi (TREATAS) = CALCULATE([Proje Bütçesi], TREATAS(VALUES(Giderler[DepartmanNo]), Departmanlar[DepartmanNo]))
| Gider türü | Gideri yapan departman | İki ölçü de |
|---|---|---|
| Personel | Satış | 45.000 ₺ |
| Lojistik ve kargo | Satış | 45.000 ₺ |
| Pazarlama ve reklam | Pazarlama | 240.000 ₺ |
| Teknoloji | Yazılım | 1.140.000 ₺ |
| Kira ve tesis, Genel yönetim | Genel Müdürlük (projesi yok) | BLANK |
| Toplam satırı | CROSSFILTER: 3.040.000 ₺ · TREATAS: 1.425.000 ₺ |
Satırlarda iki ölçü aynı. Toplamda ayrılıyorlar: CROSSFILTER’da Giderler hiç filtrelenmemişse Departmanlar’a yayılacak bir filtre yoktur ve bütün projeler (3.040.000 ₺) gelir. TREATAS ise her zaman “gideri olan departmanlar” kümesini uygular ve Veri Analitiği, Finans, İK, Hukuk projelerini dışarıda bırakır (1.425.000 ₺). Satırların toplamı ise 1.470.000 ₺’dir, çünkü Satış departmanının 45.000 ₺’lik projesi iki gider türünde görünür. Üç farklı “toplam” var; hangisinin doğru olduğunu iş sorusu belirler, DAX değil.
Karar listesi ve test
- Gerçekten çoka-çok mu? Önce ortak bir boyut ara. Hedef–satış sorununun %90’ı ortak boyutla çözülür.
- Köprü gerekiyorsa payların toplamını doğrula, köprü–boyut ilişkisini çift yönlü yap ve bunu ilişki açıklamasına yaz.
- Ölçü içinde çöz: Model tek yönlü kalsın; CROSSFILTER ya da TREATAS yalnızca gereken ölçüde.
- Toplanabilir mi? Paylaştırılmış ölçü toplanır, tam değer ölçüsü toplanmaz. Ölçü adına ve araç ipucuna bunu yaz: “Toplam satırı tekil projeleri bir kez sayar.”
- Test: DAX sorgu görünümünde
SUMX(VALUES(Boyut[Anahtar]), [Ölçü])ile toplam satırını yan yana koy. Fark beklediğin fark mı? - RLS: Köprü ya da çift yönlü ilişki varsa, güvenlik filtresinin hangi yönde aktığını rol görünümüyle (Farklı görüntüle) kontrol et.
- Performans: Performans Analizi’nde çift yönlü ilişkili görselin süresini, CROSSFILTER’lı ölçü sürümüyle karşılaştır.