Mikro ERP SQL Performans Optimizasyonu: fn_Aysm Darboğazı ve Yaşlandırma Hızlandırma Teknikleri
Mikro ERP'de fn_Aysm_v2_CariHesapAnaDovizBakiye fonksiyonu cari başına 200-500ms sürüyor. 500 cari için 4 dakika bekleme. Önfiltreleme, async job, tek cari sync ve cache katmanı ile yaşlandırma sorgularını nasıl hızlandırırsınız?
Sorun: 500 Cari İçin Neden 4 Dakika Bekliyoruz?
Mikro ERP’nin bakiye hesaplama fonksiyonu fn_Aysm_v2_CariHesapAnaDovizBakiye cari başına ortalama 200-500ms sürer. Bu fonksiyon kur çevirme, döviz hesaplamaları ve birçok iç kontrolü yapar — sonucu doğrudur ama yavaştır.
-- Bu tek satir cari basina ~200-500ms suruyor
SELECT dbo.fn_Aysm_v2_CariHesapAnaDovizBakiye(
'', 0, '120.001', '', '', NULL, NULL, GETDATE(), 0, 0, 0, 0, 0
) AS Bakiye;
Şimdi bunu 500 cari için çalıştırdığınızı düşünün:
| Cari Sayısı | Fonksiyon Süresi | Toplam Süre |
|---|---|---|
| 10 | 300ms | 3 saniye |
| 100 | 300ms | 30 saniye |
| 500 | 300ms | 2,5 dakika |
| 1.000 | 300ms | 5 dakika |
Yaşlandırma raporu çekmek için önce her carinin bakiyesini hesaplamanız gerekir. Bu fonksiyon darboğazdır.
fn_Aysm Fonksiyonu Neden Bu Kadar Yavaş?
fn_Aysm_v2_CariHesapAnaDovizBakiye bir Scalar User-Defined Function (UDF)‘dir. SQL Server’da scalar UDF’ler:
- Satır satır çalışır: Her cari için ayrı bir fonksiyon çağrısı yapılır
- Paralel çalışamaz: SQL Server scalar UDF içeren sorguları paralel execution plan’a alamaz
- İç sorgular yapar: Fonksiyon içinde
CARI_HESAP_HAREKETLERItablosuna birçok alt sorgu çalıştırır - Kur çevrimi yapar: Dövizli cariler için anlık kur tablosuna erişir
Bu durum Mikro ERP’ye özgü değildir — SQL Server’da scalar UDF’ler genel olarak performans katilidir.
Çözüm 1: Önfiltreleme — Gereksiz Carileri Eleme
En basit ve en etkili çözüm: bakiye fonksiyonunu sadece hareket görmüş cariler için çalıştırın.
Birçok ERP veritabanında binlerce “pasif” cari vardır — hiç hareket görmemiş, kapanmış veya test amaçlı açılmış. Bunlar için bakiye hesaplamak tamamen gereksizdir.
-- ADIM 1: Sadece hareket gormus carileri bul (anlik)
SELECT DISTINCT cha_kod AS cari_kod
INTO #AktifCariler
FROM dbo.CARI_HESAP_HAREKETLERI WITH (NOLOCK)
WHERE cha_kod LIKE '120%' -- Cari prefix'inize gore degistirin
AND cha_tarihi <= GETDATE()
AND cha_iptal = 0;
-- Kac cari filtrelendi?
SELECT
(SELECT COUNT(*) FROM CARI_HESAPLAR
WHERE cari_kod LIKE '120%') AS [Toplam Cari],
(SELECT COUNT(*) FROM #AktifCariler) AS [Aktif Cari],
(SELECT COUNT(*) FROM CARI_HESAPLAR
WHERE cari_kod LIKE '120%') -
(SELECT COUNT(*) FROM #AktifCariler) AS [Atlanan Cari];
Beklenen Sonuç
| Toplam Cari | Aktif Cari | Atlanan Cari |
|---|---|---|
| 1.200 | 380 | 820 |
820 cari için fonksiyon çalıştırılmadı → %68 tasarruf. 1.200 × 300ms = 6 dk yerine 380 × 300ms = 1,9 dk.
-- ADIM 2: Sadece aktif carilerin bakiyesini hesapla
SELECT
a.cari_kod,
CONVERT(decimal(18,2),
dbo.fn_Aysm_v2_CariHesapAnaDovizBakiye(
'', 0, a.cari_kod, '', '', NULL, NULL, GETDATE(), 0, 0, 0, 0, 0
)
) AS HamBakiye
INTO #HamBakiye
FROM #AktifCariler a;
Çözüm 2: Sıfır Bakiyeli Carileri Erken Ele
Bakiye hesaplandıktan sonra, bakiyesi sıfır olan carileri yaşlandırma analizinden çıkarın:
-- Sadece bakiyesi olan carilerle devam et
SELECT
cari_kod,
HamBakiye AS OrjinalBakiye,
ABS(HamBakiye) AS NetBakiye,
CASE WHEN HamBakiye > 0 THEN 0 -- Borclu (bize borcu var)
WHEN HamBakiye < 0 THEN 1 -- Alacakli (bizim borcumuz var)
ELSE -1 END AS Tip
INTO #Bakiye
FROM #HamBakiye
WHERE HamBakiye <> 0; -- Sifir bakiyeleri ele!
SELECT
(SELECT COUNT(*) FROM #HamBakiye) AS [Bakiye Hesaplanan],
(SELECT COUNT(*) FROM #Bakiye) AS [Bakiyesi Olan],
(SELECT COUNT(*) FROM #HamBakiye WHERE HamBakiye = 0) AS [Sifir Bakiye (Atlandi)];
Performans Etkisi (Kümülatif)
| Adım | Cari Sayısı | Fonksiyon Çağrısı | Süre |
|---|---|---|---|
| Filtre yok | 1.200 | 1.200 | ~6 dk |
| + Aktif filtre | 380 | 380 | ~1,9 dk |
| + Sıfır bakiye eleme | 180 | 380 (zaten hesaplandı) | ~1,9 dk + anlık |
Yaşlandırma hareketleri sadece 180 cari için çekilecek → detay sorgusu çok daha hızlı.
Çözüm 3: Tek Cari On-Demand Güncelleme
Toplu sync yerine, kullanıcı bir cariye tıkladığında sadece o cariyi güncelleme stratejisi:
-- Tek cari icin hizli yaslandirma (~3 saniye)
DECLARE @TekCari VARCHAR(50) = '120.001';
-- 1. Bakiye (tek fonksiyon cagrisi = ~300ms)
DECLARE @Bakiye DECIMAL(18,2) =
dbo.fn_Aysm_v2_CariHesapAnaDovizBakiye(
'', 0, @TekCari, '', '', NULL, NULL, GETDATE(), 0, 0, 0, 0, 0
);
-- 2. Hareketleri cek + FIFO dagit (tek cari = ~2-3 saniye)
-- ... yaslandirma sorgusu burada ...
SELECT @Bakiye AS [Hesaplanan Bakiye];
Avantajı: Kullanıcı bir cariyi incelediğinde anında güncel veri görür. Toplu sync beklemesine gerek kalmaz.
Çözüm 4: Cache Katmanı (PostgreSQL/Supabase ile)
En kapsamlı çözüm: yaşlandırma verilerini bir cache tablosuna yazın. Modal/detay ekranı açıldığında cache’ten anında okuyun:
-- Ornek: Cache tablosu yapisi (PostgreSQL/Supabase)
CREATE TABLE v3_customer_aging (
account_code TEXT NOT NULL,
branch TEXT NOT NULL,
total_balance NUMERIC(18,2),
overdue_balance NUMERIC(18,2),
agirlikli_acik_gun NUMERIC(10,2),
acik_evrak_sayisi INTEGER,
gun_0_30 NUMERIC(18,2),
gun_31_60 NUMERIC(18,2),
gun_61_90 NUMERIC(18,2),
gun_91_120 NUMERIC(18,2),
gun_120_plus NUMERIC(18,2),
en_eski_acik_tarih DATE,
synced_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (account_code, branch)
);
Okuma vs Yazma Ayrımı
| İşlem | Kaynak | Süre |
|---|---|---|
| Özet kartlar (cache) | PostgreSQL | ~10ms |
| Evrak listesi (canlı) | SQL Server | ~2-3 sn |
| Toplu sync (arka plan) | SQL Server → PostgreSQL | ~3,5 dk |
Kullanıcı deneyimi: Modal açıldığında özet kartlar anında yüklenir, evrak listesi 2-3 saniyede gelir.
Çözüm 5: Evrak Filtreleme ile Hareket Sorgusunu Hızlandırma
Yaşlandırma sorgusunda CARI_HESAP_HAREKETLERI tablosundan çekilen hareketleri filtreleyerek gereksiz taramayı önleyin:
-- Optimizasyonlar:
-- 1. cha_meblag * cha_d_kur > 1 -- Cok kucuk kalemleri filtrele
-- 2. cha_evrak_tip <> 59 -- Gereksiz evrak tipini haric tut
-- 3. cha_tarihi <= @BakiyeTarih -- Gelecek tarihli evraklari alma
SELECT ch.*
FROM CARI_HESAP_HAREKETLERI ch WITH (NOLOCK)
INNER JOIN #Bakiye b
ON ch.cha_kod = b.cari_kod
WHERE ch.cha_tarihi <= @BakiyeTarih
AND ch.cha_meblag * ch.cha_d_kur > 1 -- Kucuk tutar filtresi
AND ch.cha_evrak_tip <> 59 -- Gereksiz tip filtresi
AND ch.cha_iptal = 0;
Performans Karşılaştırma Özeti
| Strateji | Uygulanabilirlik | Tasarruf | Zorluk |
|---|---|---|---|
| Önfiltreleme | Her zaman | %50-70 | Kolay |
| Sıfır bakiye eleme | Her zaman | %20-40 ek | Kolay |
| Tek cari sync | UX iyileştirme | ~3 sn/cari | Orta |
| Cache katmanı | Web uygulamaları | %99 okuma | Orta-Zor |
| Evrak filtreleme | Her zaman | %10-20 | Kolay |
Önerimiz: Önfiltreleme + sıfır bakiye eleme ile başlayın (kolay ve etkili). İhtiyaç büyürse cache katmanına geçin.
fn_Aysm Alternatifi: Manuel Bakiye Hesaplama
Eğer fn_Aysm fonksiyonları veritabanınızda yoksa veya daha hızlı bir alternatif arıyorsanız, basitleştirilmiş bir bakiye sorgusu kullanabilirsiniz:
-- fn_Aysm OLMADAN basit bakiye hesaplama
-- Not: Bu fonksiyon kadar hassas degildir (KDV, kur farki vb. eksik kalabilir)
SELECT
cha_kod AS cari_kod,
SUM(CASE WHEN cha_tip = 0 THEN cha_meblag ELSE -cha_meblag END) AS Bakiye
INTO #BasitBakiye
FROM CARI_HESAP_HAREKETLERI WITH (NOLOCK)
WHERE cha_iptal = 0
AND cha_kod LIKE '120%'
GROUP BY cha_kod
HAVING SUM(CASE WHEN cha_tip = 0 THEN cha_meblag ELSE -cha_meblag END) <> 0;
Dikkat: Bu yöntem fn_Aysm’nin yaptığı KDV, tevkifat, masraf, kur farkı hesaplamalarını atlar. Küçük farklar olabilir. Hız-doğruluk dengesini kendiniz belirlemelisiniz.
Nihai Tam Sorgu: Kopyala — Yapıştır — Çalıştır
Önfiltreleme + bakiye hesaplama + sıfır eleme pipeline’ının tamamı:
-- =============================================
-- CARI YASLANDIRMA PERFORMANS PIPELINE
-- Kullanim: Cari prefix'inizi degistirin, F5 basin
-- Not: fn_Aysm fonksiyonlari DB'nizde olmalidir
-- Uyumluluk: SQL Server 2016+
-- =============================================
SET NOCOUNT ON;
DECLARE @BakiyeTarih DATE = GETDATE();
DECLARE @CariPrefix VARCHAR(10) = '120%'; -- << CARI PREFIX'INIZI DEGISTIRIN
-- ADIM 1: Aktif cari filtresi
IF OBJECT_ID('tempdb..#AktifCariler') IS NOT NULL DROP TABLE #AktifCariler;
SELECT DISTINCT cha_kod AS cari_kod
INTO #AktifCariler
FROM CARI_HESAP_HAREKETLERI WITH (NOLOCK)
WHERE cha_kod LIKE @CariPrefix
AND cha_tarihi <= @BakiyeTarih
AND cha_iptal = 0;
PRINT 'Aktif cari sayisi: ' + CAST(@@ROWCOUNT AS VARCHAR);
-- ADIM 2: Bakiye hesapla (fn_Aysm veya basit)
IF OBJECT_ID('tempdb..#HamBakiye') IS NOT NULL DROP TABLE #HamBakiye;
-- Secim A: fn_Aysm ile (dogru ama yavas)
/*
SELECT cari_kod,
CONVERT(decimal(18,2),
dbo.fn_Aysm_v2_CariHesapAnaDovizBakiye(
'', 0, cari_kod, '', '', NULL, NULL, @BakiyeTarih, 0, 0, 0, 0, 0
)) AS HamBakiye
INTO #HamBakiye
FROM #AktifCariler;
*/
-- Secim B: Manuel hesaplama (hizli ama yaklasik)
SELECT
a.cari_kod,
SUM(CASE WHEN ch.cha_tip = 0 THEN ch.cha_meblag ELSE -ch.cha_meblag END) AS HamBakiye
INTO #HamBakiye
FROM #AktifCariler a
INNER JOIN CARI_HESAP_HAREKETLERI ch WITH (NOLOCK)
ON a.cari_kod = ch.cha_kod
WHERE ch.cha_iptal = 0
AND ch.cha_tarihi <= @BakiyeTarih
GROUP BY a.cari_kod;
-- ADIM 3: Sifir bakiye ele + sonuc
SELECT
b.cari_kod AS [Cari Kodu],
c.cari_unvan1 AS [Musteri],
b.HamBakiye AS [Bakiye],
CASE WHEN b.HamBakiye > 0 THEN 'Borclu' ELSE 'Alacakli' END AS [Tip]
FROM #HamBakiye b
INNER JOIN CARI_HESAPLAR c WITH (NOLOCK)
ON b.cari_kod = c.cari_kod
WHERE b.HamBakiye <> 0
ORDER BY ABS(b.HamBakiye) DESC;
-- ISTATISTIK
SELECT
(SELECT COUNT(*) FROM CARI_HESAPLAR WHERE cari_kod LIKE @CariPrefix) AS [Toplam Cari],
(SELECT COUNT(*) FROM #AktifCariler) AS [Aktif Cari],
(SELECT COUNT(*) FROM #HamBakiye WHERE HamBakiye <> 0) AS [Bakiyesi Olan],
(SELECT COUNT(*) FROM #HamBakiye WHERE HamBakiye = 0) AS [Sifir Bakiye];
-- TEMIZLIK
DROP TABLE #AktifCariler;
DROP TABLE #HamBakiye;
SET NOCOUNT OFF;
fn_Aysm kullanmak için: Yorum içindeki “Seçim A” bloğunu açın, “Seçim B”yi kapatın. fn_Aysm daha doğru ama cari başına ~300ms sürer.
Edge Cases
-
Çok şubeli yapılar: Her şube ayrı SQL Server veritabanıdır. Toplu sync’te şubeleri sırayla veya paralel çalıştırabilirsiniz.
-
Kilit çakışması:
WITH (NOLOCK)kullanmak okuma kilitlerini önler ama “dirty read” riski taşır. Yaşlandırma raporu için bu genellikle kabul edilebilir bir trade-off’tur. -
fn_ fonksiyonları eksikse: Bazı Mikro sürümlerinde veya eski veritabanlarında
fn_Aysmfonksiyonları bulunmayabilir. fn_ Fonksiyonlarını Şubelere Kopyalama yazısına bakın.
İlgili Yazılar
- Cari Yaşlandırma SQL Sorgusu — FIFO Bakiye Dağıtımı
- Cari Yaşlandırma Web Otomasyonu
- fn_ Fonksiyonlarını Şubelere Kopyalama
- fn_CariRiskFoyu Analizi
- PostgreSQL Materialized View ile Rapor Hızlandırma
- Ortalama Tahsilat Süresi (DSO) Hesaplama
Bu yazı AstaFlow Case Study serisinin bir parçasıdır — 8 şubeli yapıda cari yaşlandırma performans darboğazlarını çözmek için geliştirdiğimiz stratejilerin genelleştirilmiş halidir.