“Veri modelleme: yıldız şema ve ilişkiler” dersinde yıldız şemayı Power BI modelinin içinde kurdun. Fabric’te aynı tasarımı bir katman aşağıya, lakehouse ya da ambar tablolarına indirirsin: Direct Lake modelde Power Query adımı yoktur, hesaplanmış sütun ve tablo desteği de sınırlı ve yeni olduğu için (önizleme) şema, Delta tablolar yazılırken hazır olmalıdır. Bu ders o tasarımın üç zor parçasını işliyor: tarih boyunca değişen boyutlar, çok değerli boyutlar ve tabloların fiziksel bakımı.
Boyut ve olgu tablolarını tasarlamak
- Olgu tablosu (fact): bir iş olayının en ince ayrıntısı (sipariş satırı), sayısal ölçümler (adet, tutar) ve boyutlara anahtarlar. Dar ve uzun olmalı; metin sütunlarını boyuta taşı.
- Boyut tablosu (dimension): olayı betimleyen nitelikler (müşteri, ürün, tarih). Geniş ve kısa olabilir.
- Tanecik (grain) kararı ilk karardır: “Bir satır = bir sipariş satırı.” Hedef tablosu gibi farklı tanecikli olgular ayrı tablolardır (“Hedef ve gerçekleşen” dersi).
- Normalleştirmeyi kaldırma: Kar tanesi (snowflake) zincirlerini (Ürün → Alt kategori → Kategori) sunum katmanında tek boyut tablosuna düzleştir. Kaynakta normalleştirilmiş kalabilir; Power BI’a giden “altın” katman yıldız olmalı. İstisna: başka olguların da kullandığı ortak bir boyut (bizim modelde
Kategoriler, hemUrunlerhemHedeflertarafından kullanılıyor).
Vekil anahtar (surrogate key)
Kaynak sistemin anahtarı (MusteriNo) bir iş anahtarıdır. Ambarda boyuta ayrıca anlamsız, artan bir tamsayı verilir: MusteriAnahtari. Nedenleri:
- Aynı müşterinin birden çok sürümü (Tip 2) olabilir; iş anahtarı artık benzersiz değildir.
- Farklı kaynaklardan gelen çakışan numaralar ayrılır.
- Tamsayı anahtar VertiPaq’ta iyi sıkışır ve ilişkiler hızlı çalışır.
- “Bilinmeyen” üye için sabit bir satır (ör. anahtar −1) tutulur; olgu satırı hiçbir zaman boş anahtarla kalmaz.
Fabric ambarında IDENTITY sütunları destekleniyor; lakehouse’ta Spark ile row_number() ya da en büyük anahtar + 1 yöntemi kullanılır. Ambarda birincil ve yabancı anahtar kısıtları yalnızca NOT ENFORCED olarak tanımlanabilir: belgeleme ve sorgu iyileştiricisi içindir, benzersizliği yükleme kodun sağlamalıdır.
Yavaş değişen boyutlar: Tip 1 ve Tip 2
Bir müşteri 2024 başında Bireysel segmentten Premium’a geçti. 2023 satışları hangi segmentte raporlanmalı?
| Tür | Ne yapar? | Sonuç |
|---|---|---|
| Tip 1 | Eski değerin üzerine yazar | Geçmiş satışlar da bugünkü segmentte görünür. Basit; yazım düzeltmeleri için doğru. |
| Tip 2 | Eski satırı kapatır, yeni sürüm satırı ekler | Her satış, gerçekleştiği andaki segmentte görünür. Tarihsel doğruluk. |
Tip 2 boyutta her satır bir sürümdür ve şu sütunları taşır: MusteriAnahtari (vekil, sürüme özgü), MusteriNo (iş anahtarı), izlenen nitelikler, GecerliBaslangic, GecerliBitis (açık sürümde 9999-12-31) ve Guncel bayrağı (1/0).
| MusteriAnahtari | MusteriNo | Segment | GecerliBaslangic | GecerliBitis | Guncel |
|---|---|---|---|---|---|
| 104 | 4 | Bireysel | 2022-01-01 | 2023-12-31 | 0 |
| 388 | 4 | Premium | 2024-01-01 | 9999-12-31 | 1 |
Hangi niteliklerin Tip 2 olacağını iş kararı belirler: segment, bölge, temsilci evet; ad soyad yazım düzeltmesi hayır (Tip 1). Her niteliği Tip 2 yapmak boyutu gereksiz yere büyütür.
T-SQL: MERGE ile SCD2, CTAS ile olgu
Ambarda MERGE genel kullanımda. Önce değişen müşterinin açık sürümünü kapat ve yeni müşterileri ekle:
MERGE dbo.DimMusteri AS h
USING stg.Musteri AS k
ON h.MusteriNo = k.MusteriNo AND h.Guncel = 1
WHEN MATCHED AND h.Segment <> k.Segment THEN
UPDATE SET h.GecerliBitis = DATEADD(day, -1, CAST(GETDATE() AS date)),
h.Guncel = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT (MusteriNo, Musteri, Segment, GecerliBaslangic, GecerliBitis, Guncel)
VALUES (k.MusteriNo, k.Musteri, k.Segment, CAST(GETDATE() AS date), '9999-12-31', 1);
-- Kapatılan sürümlerin yerine yeni açık sürüm
INSERT INTO dbo.DimMusteri (MusteriNo, Musteri, Segment, GecerliBaslangic, GecerliBitis, Guncel)
SELECT k.MusteriNo, k.Musteri, k.Segment, CAST(GETDATE() AS date), '9999-12-31', 1
FROM stg.Musteri AS k
WHERE NOT EXISTS (SELECT 1 FROM dbo.DimMusteri AS h
WHERE h.MusteriNo = k.MusteriNo AND h.Guncel = 1);
Olgu yüklenirken her satır, satış tarihinde geçerli olan sürümün vekil anahtarını almalıdır. CREATE TABLE AS SELECT (CTAS) bunu tek adımda yapar:
CREATE TABLE dbo.FactSatis AS
SELECT s.SiparisNo, s.SatirNo, s.Tarih, s.UrunNo, s.Adet, s.Tutar,
COALESCE(m.MusteriAnahtari, -1) AS MusteriAnahtari
FROM stg.Satis AS s
LEFT JOIN dbo.DimMusteri AS m
ON m.MusteriNo = s.MusteriNo
AND s.Tarih BETWEEN m.GecerliBaslangic AND m.GecerliBitis;
Bu yapıldıysa Power BI tarafı kolaydır: FactSatis[MusteriAnahtari] ile DimMusteri[MusteriAnahtari] arasında normal bir ilişki kurarsın ve her satış kendiliğinden doğru segmentte görünür. “Bugünkü segmente göre” analiz de istenirse boyuta bir Tip 1 sütunu (GuncelSegment) eklenir.
MusteriNo) Tip 2 boyuta bağlamak. Boyutta aynı numaradan birden çok satır olduğu için ilişki çoka-çok olur ve satışlar sürüm sayısı kadar çoğalabilir.Çok değerli boyut ve köprü tablo
Bir kampanya birden çok kategoriyi kapsar, bir kategori birden çok kampanyada yer alır. Bunu olgu tablosuna sütun olarak koyamazsın. Çözüm bir köprü tablodur: KampanyaKategori(Kampanya, Kategori). Modelde köprü, iki boyut arasında durur; filtrenin Kampanya → Köprü → Kategoriler yönünde akması için köprü ile Kategoriler arasındaki ilişki çift yönlü yapılır ya da ölçüde TREATAS kullanılır. Ayrıntı “Tablo fonksiyonları, kümeler ve sanal ilişkiler” dersinde. İki önemli kural:
- Kampanyalara göre kırılımda toplamlar toplanmaz: Giyim iki kampanyada olduğu için iki satırda da görünür; toplam satırı her kategoriyi bir kez sayar.
- Köprüye ağırlık sütunu (ör. 0,5 / 0,5) eklenirse satış kampanyalara paylaştırılabilir.
Bizim modelde kampanya tablosu yok; köprü mantığını DAX içinde küçük bir tabloyla deneyebilirsin:
Okula Dönüş Satışı =
VAR Kopru =
SELECTCOLUMNS(
{ ("Okula Dönüş", "Kitap"), ("Okula Dönüş", "Elektronik"), ("Okula Dönüş", "Giyim"),
("Yaz Festivali", "Spor"), ("Yaz Festivali", "Giyim") },
"Kampanya", [Value1], "Kategori", [Value2]
)
VAR Kategoriler_ = SELECTCOLUMNS(FILTER(Kopru, [Kampanya] = "Okula Dönüş"), "Kategori", [Kategori])
RETURN CALCULATE([Net Satış], KEEPFILTERS(TREATAS(Kategoriler_, Kategoriler[Kategori])))
KEEPFILTERS olmazsa matrisin her kategori satırında kampanyanın toplamı (681.355 ₺) görünür; çünkü TREATAS aynı sütundaki dış filtrenin üzerine yazar.
Delta bakımı: OPTIMIZE, V-Order, VACUUM
Her yükleme yeni Parquet dosyaları ekler. Küçük ve çok sayıda dosya hem Spark’ı hem Direct Lake’i yavaşlatır; Direct Lake’in kapasiteye göre dosya ve satır grubu sınırları (guardrails) da aşılabilir.
- OPTIMIZE (sıkıştırma): küçük dosyaları büyük dosyalarda birleştirir.
- V-Order: Parquet’i yazarken sıralama, kodlama ve sıkıştırmayı VertiPaq tarzı okumaya göre ayarlar. Learn’e göre yazmayı ortalama yaklaşık %15 yavaşlatır, sıkıştırmayı %50’ye kadar artırabilir. Yeni çalışma alanlarında varsayılan olarak kapalıdır; Power BI’ın yoğun okuduğu tablolar için açman gerekir.
- VACUUM: artık hiçbir sürümün başvurmadığı eski dosyaları siler. Varsayılan saklama 7 gündür; daha kısa süre zaman yolculuğunu (time travel) ve eşzamanlı okuyucuları etkileyebilir.
-- Spark SQL (lakehouse not defteri)
OPTIMIZE satislar VORDER;
VACUUM satislar RETAIN 168 HOURS;
-- Tabloda V-Order'ı kalıcı aç (yalnızca bundan sonraki yazmaları etkiler)
ALTER TABLE satislar SET TBLPROPERTIES ("delta.parquet.vorder.enabled" = "true");
Aynı işlemler lakehouse gezgininde tabloya sağ tık → Bakım ile de çalıştırılır; düzenli bakım için işlem hattındaki Lakehouse bakım etkinliği (önizleme) kullanılabilir. Ambar tablolarının bakımını Fabric büyük ölçüde kendisi yapar.
DAX tarafında SCD2: “o tarihteki değer”
En iyisi, yukarıdaki gibi olgunun doğru sürümün anahtarını taşımasıdır. Ama bazen elinde yalnızca iş anahtarı olan bir olgu ve ayrı bir geçmiş tablosu olur. O zaman her satış satırı için geçerli sürümü tarih aralığına göre bulursun. Bizim modelde Musteriler Tip 1 (yalnızca bugünkü segment) olduğu için geçmişi küçük bir tabloyla canlandıralım: 4 numaralı müşteri 2024’te Premium’a geçti, 8 numaralı müşteri Nisan 2024’te Premium’dan Bireysel’e indi.
EVALUATE
SELECTCOLUMNS(
{ (4, "Bireysel", DATE(2022, 1, 1), DATE(2023, 12, 31)),
(4, "Premium", DATE(2024, 1, 1), DATE(2099, 12, 31)),
(8, "Premium", DATE(2022, 1, 1), DATE(2024, 3, 31)),
(8, "Bireysel", DATE(2024, 4, 1), DATE(2099, 12, 31)) },
"MusteriNo", [Value1], "Segment", [Value2],
"GecerliBaslangic", [Value3], "GecerliBitis", [Value4]
)
Ölçü, her satış satırında müşterinin o tarihte geçerli sürümünü arar; geçmişte kaydı olmayan müşteri için bugünkü segmenti kullanır:
Premium Satış (O Tarihte) =
VAR Gecmis =
SELECTCOLUMNS(
{ (4, "Bireysel", DATE(2022, 1, 1), DATE(2023, 12, 31)),
(4, "Premium", DATE(2024, 1, 1), DATE(2099, 12, 31)),
(8, "Premium", DATE(2022, 1, 1), DATE(2024, 3, 31)),
(8, "Bireysel", DATE(2024, 4, 1), DATE(2099, 12, 31)) },
"MusteriNo", [Value1], "Segment", [Value2],
"Bas", [Value3], "Bit", [Value4]
)
RETURN
CALCULATE(
SUMX(
Satislar,
VAR No = Satislar[MusteriNo]
VAR Gun = Satislar[Tarih]
VAR Surum = FILTER(Gecmis, [MusteriNo] = No && [Bas] <= Gun && [Bit] >= Gun)
VAR Seg = IF(ISEMPTY(Surum), RELATED(Musteriler[Segment]), MAXX(Surum, [Segment]))
RETURN IF(Seg = "Premium", Satislar[Tutar])
),
Satislar[Durum] <> "İptal"
)
Sonuç: Tip 1 görünümde (Musteriler[Segment] = "Premium") 2023 Premium satışı 164.865 ₺, tarihsel görünümde 154.847 ₺. Fark tam olarak 4 numaralı müşterinin 2023’teki Bireysel dönemi (−10.594 ₺) ve 8 numaralı müşterinin 2023’teki Premium dönemidir (+576 ₺). Bu ölçü öğreticidir ama satır satır arama yaptığı için büyük olguda yavaştır; üretimde sürüm anahtarını yükleme sırasında olguya yaz.