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üresiToplam Süre
10300ms3 saniye
100300ms30 saniye
500300ms2,5 dakika
1.000300ms5 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:

  1. Satır satır çalışır: Her cari için ayrı bir fonksiyon çağrısı yapılır
  2. Paralel çalışamaz: SQL Server scalar UDF içeren sorguları paralel execution plan’a alamaz
  3. İç sorgular yapar: Fonksiyon içinde CARI_HESAP_HAREKETLERI tablosuna birçok alt sorgu çalıştırır
  4. 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 CariAktif CariAtlanan Cari
1.200380820

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ımCari SayısıFonksiyon ÇağrısıSüre
Filtre yok1.2001.200~6 dk
+ Aktif filtre380380~1,9 dk
+ Sıfır bakiye eleme180380 (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ı

İşlemKaynakSü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

StratejiUygulanabilirlikTasarrufZorluk
ÖnfiltrelemeHer zaman%50-70Kolay
Sıfır bakiye elemeHer zaman%20-40 ekKolay
Tek cari syncUX iyileştirme~3 sn/cariOrta
Cache katmanıWeb uygulamaları%99 okumaOrta-Zor
Evrak filtrelemeHer zaman%10-20Kolay

Ö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

  1. Çok şubeli yapılar: Her şube ayrı SQL Server veritabanıdır. Toplu sync’te şubeleri sırayla veya paralel çalıştırabilirsiniz.

  2. 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.

  3. fn_ fonksiyonları eksikse: Bazı Mikro sürümlerinde veya eski veritabanlarında fn_Aysm fonksiyonları bulunmayabilir. fn_ Fonksiyonlarını Şubelere Kopyalama yazısına bakın.

İlgili Yazılar


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.

📚 İlgili Yazılar