---
title: "Mikro ERP SQL Performans Optimizasyonu: fn_Aysm Darboğazı ve Yaşlandırma Hızlandırma Teknikleri"
description: "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?"
date: 2026-06-27
category: mikro-erp
tags: ["mikro-erp", "sql-server", "performans", "fn-aysm", "optimizasyon", "cache", "cari-yaslandirma", "astaflow"]
url: https://mikroerp.dev/blog/mikro-erp-cari-yaslandirma-performans-optimizasyon/
---

## 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**.

```sql
-- 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:

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.

```sql
-- 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**.

```sql
-- 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:

```sql
-- 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:

```sql
-- 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:

```sql
-- 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:

```sql
-- 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:

```sql
-- 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ı:

```sql
-- =============================================
-- 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](/blog/mikro-erp-fn-fonksiyonlari-sube-kopyalama/) yazısına bakın.

## İlgili Yazılar

- [Cari Yaşlandırma SQL Sorgusu — FIFO Bakiye Dağıtımı](/blog/mikro-erp-cari-yaslandirma-sql-sorgusu/)
- [Cari Yaşlandırma Web Otomasyonu](/blog/mikro-erp-cari-yaslandirma-otomasyonu/)
- [fn_ Fonksiyonlarını Şubelere Kopyalama](/blog/mikro-erp-fn-fonksiyonlari-sube-kopyalama/)
- [fn_CariRiskFoyu Analizi](/blog/mikro-erp-cari-risk-raporu-fn-caririsklfoyu-analizi/)
- [PostgreSQL Materialized View ile Rapor Hızlandırma](/blog/postgresql-materialized-view-erp-rapor-hizlandirma/)
- [Ortalama Tahsilat Süresi (DSO) Hesaplama](/blog/mikro-erp-ortalama-tahsilat-suresi-dso-sql/)

---

*Bu yazı [AstaFlow Case Study](/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.*