SQL Server'da FIFO Fatura-Tahsilat Eşleştirme: Kümülatif Aralık Kesişim Algoritması
Bir fatura 3 farklı tahsilatla kapanırsa her parçanın ödeme süresini nasıl hesaplarsınız? Window Function ile kümülatif toplam, aralık kesişim formülü ve Cursor'a gerek kalmadan set-based FIFO eşleştirme algoritması.
Problem: Bir Fatura Üç Farklı Tahsilatla Kapanırsa Ne Olur?
Bir müşterinize 100.000 TL’lik fatura kestiniz. Müşteri şu şekilde ödedi:
| Ödeme | Tarih | Tutar | Fatura Sonrası |
|---|---|---|---|
| 1. Ödeme | 15 gün sonra | 30.000 TL | 15 gün |
| 2. Ödeme | 45 gün sonra | 50.000 TL | 45 gün |
| 3. Ödeme | 80 gün sonra | 20.000 TL | 80 gün |
Bu fatura ortalama kaç günde ödendi?
Yanlış hesap: (15 + 45 + 80) / 3 = 46,7 gün ❌
Doğru hesap (ağırlıklı): (30.000 × 15 + 50.000 × 45 + 20.000 × 80) / 100.000 = 35,5 gün ✅
Peki ya bu tahsilatlar başka faturaları da kapsıyorsa? Yani 2. ödeme (50.000 TL) hem bu faturanın kalan 20.000 TL’sini hem de bir sonraki faturanın 30.000 TL’sini kapatıyorsa?
İşte bu sorunun çözümü FIFO Kümülatif Aralık Kesişim Algoritmasıdır.
Naif Yaklaşım: Cursor (Neden Kötü?)
-- ❌ CURSOR ile naive yaklasim (YAVAS ve karmasik)
DECLARE @KalanOdeme DECIMAL(18,2)
DECLARE @OdemeTarihi DATE
DECLARE @OdemeTutar DECIMAL(18,2)
DECLARE odeme_cursor CURSOR FOR
SELECT OdemeTarihi, Tutar FROM #Odemeler ORDER BY OdemeTarihi
OPEN odeme_cursor
FETCH NEXT FROM odeme_cursor INTO @OdemeTarihi, @OdemeTutar
WHILE @@FETCH_STATUS = 0
BEGIN
SET @KalanOdeme = @OdemeTutar
-- Her odeme icin tum acik faturalari tara...
-- En eski faturadan baslayarak kalanini dus...
-- N odeme x M fatura = N*M islem!
FETCH NEXT FROM odeme_cursor INTO @OdemeTarihi, @OdemeTutar
END
CLOSE odeme_cursor
DEALLOCATE odeme_cursor
Sorunları:
- O(N × M) karmaşıklık: 500 ödeme × 300 fatura = 150.000 işlem
- Satır satır işlem: SQL Server’ın set-based gücünden yararlanmaz
- Bakımı zor: Hata ayıklama, değişiklik yapmak kabus
- Paralel çalışamaz: Cursor seri çalışır, birden fazla CPU kullanamaz
Set-Based Çözüm: Window Functions ile FIFO
Aynı işi tek bir SELECT ile yapabiliriz. Anahtar fikir: fatura ve tahsilatları kümülatif toplamlarla birer sayı doğrusu aralığına dönüştürmek, sonra aralıkların kesişimini bulmak.
Adım 0: Test Verisi Oluştur
Aşağıdaki test verisini kendi SQL Server’ınızda çalıştırabilirsiniz — Mikro ERP gerekmez:
-- =============================================
-- TEST VERISI - Kendi DB'nizde calistirabilirsiniz
-- =============================================
IF OBJECT_ID('tempdb..#TestFaturalar') IS NOT NULL DROP TABLE #TestFaturalar;
IF OBJECT_ID('tempdb..#TestOdemeler') IS NOT NULL DROP TABLE #TestOdemeler;
CREATE TABLE #TestFaturalar (
FaturaNo INT,
Tarih DATE,
Tutar DECIMAL(18,2)
);
INSERT INTO #TestFaturalar VALUES
(1, '2026-01-15', 50000), -- 50.000 TL
(2, '2026-02-10', 80000), -- 80.000 TL
(3, '2026-03-05', 30000); -- 30.000 TL
-- Toplam: 160.000 TL
CREATE TABLE #TestOdemeler (
OdemeNo INT,
Tarih DATE,
Tutar DECIMAL(18,2)
);
INSERT INTO #TestOdemeler VALUES
(1, '2026-02-01', 40000), -- 40.000 TL
(2, '2026-03-15', 60000), -- 60.000 TL
(3, '2026-04-20', 35000); -- 35.000 TL
-- Toplam: 135.000 TL
-- Acik kalan: 25.000 TL
Adım 1: Kümülatif Toplamları Hesapla
;WITH FaturaCum AS (
SELECT
FaturaNo,
Tarih AS FaturaTarihi,
Tutar,
SUM(Tutar) OVER (
ORDER BY Tarih, FaturaNo
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS CumTutar
FROM #TestFaturalar
),
OdemeCum AS (
SELECT
OdemeNo,
Tarih AS OdemeTarihi,
Tutar,
SUM(Tutar) OVER (
ORDER BY Tarih, OdemeNo
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS CumTutar
FROM #TestOdemeler
),
Bu adımdan sonra veriler şöyle görünür:
Faturalar (kümülatif):
| FaturaNo | Tutar | CumTutar | Aralık |
|---|---|---|---|
| 1 | 50.000 | 50.000 | [0 — 50.000] |
| 2 | 80.000 | 130.000 | [50.000 — 130.000] |
| 3 | 30.000 | 160.000 | [130.000 — 160.000] |
Ödemeler (kümülatif):
| OdemeNo | Tutar | CumTutar | Aralık |
|---|---|---|---|
| 1 | 40.000 | 40.000 | [0 — 40.000] |
| 2 | 60.000 | 100.000 | [40.000 — 100.000] |
| 3 | 35.000 | 135.000 | [100.000 — 135.000] |
Adım 2: Aralık Kesişimini Hesapla (Çekirdek Formül)
İki aralığın kesişim uzunluğu:
Kesişim = min(ÜstSınır_A, ÜstSınır_B) - max(AltSınır_A, AltSınır_B)
Pozitifse kesişim var, negatif veya sıfırsa yok.
Kesisim AS (
SELECT
f.FaturaNo,
o.OdemeNo,
f.FaturaTarihi,
o.OdemeTarihi,
f.Tutar AS FaturaTutar,
o.Tutar AS OdemeTutar,
-- Kesisim formulu
CASE
WHEN (CASE WHEN f.CumTutar < o.CumTutar
THEN f.CumTutar ELSE o.CumTutar END)
- (CASE WHEN f.CumTutar - f.Tutar > o.CumTutar - o.Tutar
THEN f.CumTutar - f.Tutar
ELSE o.CumTutar - o.Tutar END) > 0
THEN (CASE WHEN f.CumTutar < o.CumTutar
THEN f.CumTutar ELSE o.CumTutar END)
- (CASE WHEN f.CumTutar - f.Tutar > o.CumTutar - o.Tutar
THEN f.CumTutar - f.Tutar
ELSE o.CumTutar - o.Tutar END)
ELSE 0
END AS KesisimTutar
FROM FaturaCum f
INNER JOIN OdemeCum o
ON o.CumTutar > f.CumTutar - f.Tutar -- Odeme aralik sonu > Fatura aralik basi
AND o.CumTutar - o.Tutar < f.CumTutar -- Odeme aralik basi < Fatura aralik sonu
)
SQL Server 2022 ve üzeri kullananlar iç içe CASE yerine
LEAST()veGREATEST()kullanabilir:LEAST(f.CumTutar, o.CumTutar) - GREATEST(f.CumTutar - f.Tutar, o.CumTutar - o.Tutar)
Görsel Açıklama: Sayı Doğrusu
Kümülatif aralıkları bir sayı doğrusu üzerinde düşünün:
Sayı Doğrusu (TL)
0 40K 50K 100K 130K 135K 160K
| | | | | | |
├── F1 ────┤─────────┤ | | | |
| [0—50K] | | | | | |
| | ├── F2 ─────┤─────────┤ | |
| | | [50K—130K] | | |
| | | | ├─ F3 ┤──────┤
| | | | | [130K—160K]|
| | | | | | |
├── O1 ────┤ | | | | |
| [0—40K] | | | | | |
| ├── O2 ───┤───────────┤ | | |
| | [40K—100K] | | | |
| | | ├── O3 ───┤─────┤ |
| | | |[100K—135K] | |
Kesişimler:
F1 ∩ O1 = min(50K, 40K) - max(0, 0) = 40.000 TL ✓
F1 ∩ O2 = min(50K, 100K) - max(0, 40K) = 10.000 TL ✓
F2 ∩ O2 = min(130K, 100K) - max(50K, 40K) = 50.000 TL ✓
F2 ∩ O3 = min(130K, 135K) - max(50K, 100K) = 30.000 TL ✓
F3 ∩ O3 = min(160K, 135K) - max(130K, 100K) = 5.000 TL ✓
Kontrol: 40K + 10K + 50K + 30K + 5K = 135.000 TL = Toplam ödeme ✓
Adım 3: Fatura Bazlı Ağırlıklı Ödeme Süresi
SELECT
k.FaturaNo,
k.FaturaTarihi,
SUM(k.KesisimTutar) AS KapananTutar,
(SELECT Tutar FROM #TestFaturalar
WHERE FaturaNo = k.FaturaNo) AS FaturaTutar,
(SELECT Tutar FROM #TestFaturalar
WHERE FaturaNo = k.FaturaNo)
- SUM(k.KesisimTutar) AS AcikKalan,
-- Agirlikli ortalama odeme gunu
SUM(k.KesisimTutar * DATEDIFF(DAY, k.FaturaTarihi, k.OdemeTarihi))
/ NULLIF(SUM(k.KesisimTutar), 0) AS AgirlikliOdemeGun
FROM Kesisim k
WHERE k.KesisimTutar > 0
GROUP BY k.FaturaNo, k.FaturaTarihi
ORDER BY k.FaturaNo;
Beklenen Output
| FaturaNo | Fatura Tarihi | Kapanan | Fatura Tutar | Açık Kalan | Ağırlıklı Gün |
|---|---|---|---|---|---|
| 1 | 2026-01-15 | 50.000 | 50.000 | 0 | 25 |
| 2 | 2026-02-10 | 80.000 | 80.000 | 0 | 47 |
| 3 | 2026-03-05 | 5.000 | 30.000 | 25.000 | 46 |
Fatura 1: 40K’sı 17 günde (O1), 10K’sı 59 günde (O2) kapandı → (40K×17 + 10K×59)/50K = 25,4 gün
Fatura 2: 50K’sı 33 günde (O2), 30K’sı 69 günde (O3) kapandı → (50K×33 + 30K×69)/80K = 46,5 gün
Fatura 3: Sadece 5K’sı 46 günde (O3) kapandı, 25K hâlâ açık.
Mikro ERP Uyarlaması
Test verisinde konsepti anladıysanız, Mikro ERP tablolarına uyarlamak kolaydır:
-- Mikro ERP icin: #TestFaturalar yerine gercek faturalar
DECLARE @CariKod VARCHAR(50) = '120.001';
-- Faturalar (Borc tarafi)
SELECT
ROW_NUMBER() OVER (ORDER BY cha_tarihi ASC, cha_create_date ASC) AS FaturaNo,
CAST(cha_tarihi AS date) AS Tarih,
cha_meblag AS Tutar
INTO #MikroFaturalar
FROM CARI_HESAP_HAREKETLERI WITH (NOLOCK)
WHERE cha_kod = @CariKod
AND cha_tip = 0 -- Borc
AND cha_iptal = 0
AND cha_meblag > 1; -- Cok kucuk tutarlari filtrele
-- Tahsilatlar (Alacak tarafi)
SELECT
ROW_NUMBER() OVER (ORDER BY cha_tarihi ASC, cha_create_date ASC) AS OdemeNo,
CAST(cha_tarihi AS date) AS Tarih,
cha_meblag AS Tutar
INTO #MikroOdemeler
FROM CARI_HESAP_HAREKETLERI WITH (NOLOCK)
WHERE cha_kod = @CariKod
AND cha_tip = 1 -- Alacak
AND cha_iptal = 0
AND cha_meblag > 1;
-- Sonra ayni CTE yapisini #MikroFaturalar ve #MikroOdemeler ile kullanin
Dikkat edilecekler:
cha_tip = 0→ Borç (fatura),cha_tip = 1→ Alacak (tahsilat)cha_ciro_cari_kodu— Ciro hareketleri için ek filtre gerekebilircha_evrak_tip = 59hariç tutulmalı (gereksiz evrak tipi)- Daha hassas meblağ hesabı için
fn_Aysm_v2_CariHesapMeblagfonksiyonu kullanılabilir
Performans Karşılaştırması
| Yöntem | 300 Fatura × 200 Ödeme | 1.000 × 800 | Karmaşıklık |
|---|---|---|---|
| Cursor | ~2,4 sn | ~32 sn | O(N × M) |
| Window Function | ~0,1 sn | ~0,8 sn | O(N log N) |
| Fark | 24x hızlı | 40x hızlı | — |
Window Function yaklaşımı SQL Server’ın sıralama ve pencere işlemlerini paralel çalıştırabilmesi sayesinde büyük veri setlerinde dramatik fark yaratır.
Nihai Tam Sorgu: Kopyala — Yapıştır — Çalıştır
Test verisi dahil, herhangi bir veritabanında çalışır. Kendi Mikro DB’nizde kullanmak için #Faturalar ve #Odemeler kısımlarını gerçek tabloyla değiştirin:
-- =============================================
-- FIFO FATURA-TAHSILAT ESLESTIRME (TEST VERISI DAHIL)
-- Kullanim: Direkt F5 basin, sonuc gelir
-- Uyumluluk: SQL Server 2016+
-- =============================================
-- TEST VERISI
IF OBJECT_ID('tempdb..#Faturalar') IS NOT NULL DROP TABLE #Faturalar;
IF OBJECT_ID('tempdb..#Odemeler') IS NOT NULL DROP TABLE #Odemeler;
CREATE TABLE #Faturalar (FaturaNo VARCHAR(10), Tarih DATE, Tutar DECIMAL(18,2));
CREATE TABLE #Odemeler (OdemeNo VARCHAR(10), Tarih DATE, Tutar DECIMAL(18,2));
INSERT INTO #Faturalar VALUES
('F001', '2026-01-10', 50000),
('F002', '2026-01-20', 80000),
('F003', '2026-02-05', 30000);
INSERT INTO #Odemeler VALUES
('T001', '2026-02-15', 60000),
('T002', '2026-03-10', 70000);
-- FIFO ESLESTIRME
;WITH BorcCum AS (
SELECT
FaturaNo, Tarih AS FaturaTarihi, Tutar,
SUM(Tutar) OVER (ORDER BY Tarih, FaturaNo
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS CumTutar
FROM #Faturalar
),
AlacakCum AS (
SELECT
OdemeNo, Tarih AS OdemeTarihi, Tutar,
SUM(Tutar) OVER (ORDER BY Tarih, OdemeNo
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS CumTutar
FROM #Odemeler
),
Kesisim AS (
SELECT
b.FaturaNo, b.FaturaTarihi, b.Tutar AS FaturaTutar,
a.OdemeNo, a.OdemeTarihi, a.Tutar AS OdemeTutar,
CASE
WHEN (CASE WHEN b.CumTutar < a.CumTutar
THEN b.CumTutar ELSE a.CumTutar END)
- (CASE WHEN b.CumTutar - b.Tutar > a.CumTutar - a.Tutar
THEN b.CumTutar - b.Tutar
ELSE a.CumTutar - a.Tutar END) > 0
THEN (CASE WHEN b.CumTutar < a.CumTutar
THEN b.CumTutar ELSE a.CumTutar END)
- (CASE WHEN b.CumTutar - b.Tutar > a.CumTutar - a.Tutar
THEN b.CumTutar - b.Tutar
ELSE a.CumTutar - a.Tutar END)
ELSE 0
END AS KapananParca,
DATEDIFF(DAY, b.FaturaTarihi, a.OdemeTarihi) AS GunFarki
FROM BorcCum b
INNER JOIN AlacakCum a
ON a.CumTutar > b.CumTutar - b.Tutar
AND a.CumTutar - a.Tutar < b.CumTutar
)
SELECT
FaturaNo, FaturaTarihi, FaturaTutar,
OdemeNo, OdemeTarihi, OdemeTutar,
CONVERT(decimal(18,2), KapananParca) AS [Kapanan Parca],
GunFarki AS [Gun Farki]
FROM Kesisim
WHERE KapananParca > 0
ORDER BY FaturaNo, OdemeNo;
-- TEMIZLIK
DROP TABLE #Faturalar;
DROP TABLE #Odemeler;
Mikro ERP’de kullanmak için:
#FaturalaryerineCARI_HESAP_HAREKETLERI WHERE cha_tip = 0,#OdemeleryerineWHERE cha_tip = 1kullanın. Detaylar yukarıdaki “Mikro ERP Uyarlaması” bölümünde.
Edge Cases (İstisnai Durumlar)
-
Negatif tutarlar: İade faturaları veya düzeltme kayıtları negatif olabilir. Bunları ana analizden hariç tutun (
cha_meblag > 0). -
Aynı tutarlı aynı tarihli evraklar:
ORDER BYsıralamasında çakışma olabilir.cha_create_datevecha_Guidile kırılım sağlayın. -
Çek/Senet gerçek tahsilat tarihi: Bu sorguda
cha_tarihikullanılıyor. Çek vadesini baz almak için Çek ve Senet Gerçek Tahsilat Tarihi yazısına bakın. -
Toplam ödeme > toplam fatura: Fazla ödeme varsa avans kaydı oluşur. Kesişim algoritması bunu otomatik ele alır — fazla kısım hiçbir faturaya eşleşmez.
İlgili Yazılar
- Ortalama Tahsilat Süresi (DSO) Hesaplama
- Cari Yaşlandırma SQL Sorgusu — FIFO Bakiye Dağıtımı
- Stok Maliyet Katman Hesabı — FIFO Benzeri Mantık
- Çek ve Senet Gerçek Tahsilat Tarihi
- Vadesinde Ödeme Yüzdesi Analizi
- Cari Hesap Tablo İlişkileri JOIN Rehberi
Bu yazı AstaFlow Case Study serisinin bir parçasıdır — parça ağırlıklı FIFO eşleştirme algoritması cari yaşlandırma ve ortalama tahsilat süresi modüllerinin temelini oluşturur.