---
title: SQL Server MARS Nedir? EF Core Transaction ve Savepoint Tuzağı
description: "MARS, SQL Server’da aynı bağlantı üzerinde birden fazla aktif sonuç kümesiyle çalışmaya izin veren bir özellik. İlk bakışta masum görünen bu ayarın EF Core tarafında önemli bir yan etkisi var: MARS etkinse otomatik savepoint mekanizması devre dışı kalıyor. Bu da özellikle açık transaction içinde SaveChanges hatası yaşandığında işin rengini değiştirebiliyor. Bu yazıda MARS’ın gerçekten ne yaptığını, nerelerde gerekli olduğunu ve connection string’den kaldırmadan önce nelere bakılması gerektiğini anlatıyorum."
author: Burak Kaşıkcı
date: 2026-10-02
updated: 2026-10-02
tags:
  - sql server
  - ef core
  - .net
  - database
  - transaction
  - mars
  - savepoint
  - backend
canonical: https://burakkasikci.com/blog/sql-server-mars-nedir-ef-core-transaction-ve-savepoint-tuzagi
slug: sql-server-mars-nedir-ef-core-transaction-ve-savepoint-tuzagi
---

# SQL Server MARS Nedir? EF Core Transaction ve Savepoint Tuzağı

Geçenlerde EF Core tarafında bir uyarının peşine düştüm. Başta klasik, çok da önemli olmayan bir log mesajı gibi görünüyordu. Sonra konu connection string’e, oradan da yıllardır birçok projede gördüğüm şu ayara geldi:

```
MultipleActiveResultSets=True
```

Açıkçası bu ayarı daha önce defalarca gördüm. Hatta eski projelerde connection string hazırlarken benim de çok düşünmeden kullandığım olmuştur.
Adı da insanı biraz yanıltıyor:
**Multiple Active Result Sets.**
İlk çağrışım şu oluyor: Birden fazla sonucu aynı anda çalıştırıyor, demek ki paralellik veya performansla ilgili bir şey.
Aslında hikâye pek öyle değil.
Daha ilginç olanı ise bu küçücük ayarın EF Core’un transaction yönetimini ve savepoint davranışını değiştirebilmesi.
Benim konuyu araştırmaya devam etmemin asıl sebebi de bu oldu.

## Önce MARS tam olarak ne yapıyor?

MARS, yani **Multiple Active Result Sets**, SQL Server’a açılmış aynı fiziksel bağlantı üzerinde birden fazla aktif sonuç kümesinin bulunabilmesine izin veriyor.
Daha sade söyleyeyim.
Bir sorgudan veri okumaya başladınız ama henüz okumayı bitirmediniz. O sırada aynı bağlantıyı kullanarak başka bir SQL komutu çalıştırmak istiyorsunuz.
Normalde problem çıkarabilecek senaryo tam olarak bu.
Örneğin:

```
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var command = new SqlCommand(
    "SELECT Id, Name FROM Users",
    connection);

await using var reader = await command.ExecuteReaderAsync();

while (await reader.ReadAsync())
{
    var userId = reader.GetInt32(0);

    // İlk reader hâlâ açık.
}
```

Şu anda `reader` veriyi okumaya devam ediyor.
Döngünün içinde aynı connection üzerinden bir sorgu daha göndermeye çalışalım:

```
while (await reader.ReadAsync())
{
    var userId = reader.GetInt32(0);

    await using var orderCommand = new SqlCommand(
        "SELECT * FROM Orders WHERE UserId = @UserId",
        connection);

    orderCommand.Parameters.AddWithValue("@UserId", userId);

    await using var orderReader =
        await orderCommand.ExecuteReaderAsync();
}
```

İlk sonuç kümesi kapanmadan ikinci bir tane açmaya çalışıyoruz.
MARS’ın çözdüğü mesele bu.
`MultipleActiveResultSets=True` ise aynı bağlantı üzerinde birden fazla aktif sonuç tutulabiliyor. Kapalıysa önce mevcut okumayı tamamlamak, reader’ı kapatmak veya başka bir connection kullanmak gerekiyor.
Burada önemli olan şu: MARS, “bir sürü SQL sorgusunu paralel çalıştır” özelliği değil.

## MARS bir performans seçeneği değil

Benim burada ilk düzeltmek istediğim yanlış algı tam olarak buydu.
Connection string’de şu satırı görünce:

```
MultipleActiveResultSets=True
```

insanın kafasında ister istemez şöyle bir şey oluşuyor:

```
Multiple = Daha fazla
Active = Aynı anda
Result Sets = Sorgu

Sonuç: Daha hızlı?
```

Hayır.
MARS gerçek anlamda bir paralel sorgu motoru değil.
SQL Server tarafında komutların çalışması bazı noktalarda iç içe geçebiliyor ama buradan otomatik bir performans kazancı çıkmıyor.
Zaten bir uygulamanın birden fazla database connection açabilmesi MARS’tan tamamen ayrı bir konu.
Connection pool da öyle.
Örneğin uygulamada iki farklı `DbContext` varsa bunlar gerektiğinde havuzdan iki ayrı bağlantı alabilir:

```
await using var userDb =
    await contextFactory.CreateDbContextAsync();

await using var orderDb =
    await contextFactory.CreateDbContextAsync();

var usersTask = userDb.Users.ToListAsync();
var ordersTask = orderDb.Orders.ToListAsync();

await Task.WhenAll(usersTask, ordersTask);
```

Bu senaryoda MARS’a ihtiyacımız yok.
İki context kendi bağlantısını yönetiyor.
Ben artık MARS’ı kafamda şu şekilde konumlandırıyorum:

```
Tek SQL bağlantısı

    ↓

Bir result set hâlâ okunuyor

    ↓

Aynı bağlantıda ikinci komut çalıştırılmak isteniyor
```

MARS burada devreye giriyor.
Bu ayrım önemli. Çünkü sadece adına bakarak “performans için açalım” demek yanlış bir karar olabilir.

## Peki EF Core neden uyarı veriyor?

İşin benim için daha ilginç olan tarafı burada başladı.
EF Core açık bir transaction içinde `SaveChanges` çalıştırırken **savepoint** kullanabiliyor.
Savepoint’i transaction’ın içindeki küçük bir geri dönüş noktası olarak düşünebiliriz.
Kabaca şöyle:

```
BEGIN TRANSACTION

    İşlem A
    İşlem B

    SAVEPOINT X

    İşlem C
    İşlem D
```

`İşlem D` sırasında problem yaşandığını düşünelim.
Her şeyi çöpe atmak yerine:

```
ROLLBACK TO SAVEPOINT X
```

yapılabilir.
Bu durumda A ve B korunurken savepoint’ten sonra yapılan işlemler geri alınır.
Ben savepoint’i en basit hâliyle şöyle düşünüyorum:

> Transaction içindeki checkpoint.

Teknik olarak birebir aynı şey değil tabii ama zihinde oturtmak için güzel bir benzetme.

## EF Core bunu bizim yerimize yapabiliyor

Burada güzel olan nokta şu: çoğu durumda savepoint’i bizim elle oluşturmamız gerekmiyor.
Eğer `DbContext` zaten açık bir transaction içindeyse ve:

```
await dbContext.SaveChangesAsync();
```

çalıştırırsak EF Core, kaydetme işleminden önce otomatik olarak bir savepoint oluşturabiliyor.
Örneğin:

```
await using var transaction =
    await dbContext.Database.BeginTransactionAsync();

user.Name = "Burak";

await dbContext.SaveChangesAsync();

order.Status = OrderStatus.Completed;

await dbContext.SaveChangesAsync();

await transaction.CommitAsync();
```

İkinci `SaveChangesAsync()` sırasında hata çıktığını düşünelim.
Normal şartlarda EF Core, çağrıdan hemen önce oluşturduğu noktaya geri dönebilir.
Yani transaction tamamen iptal olmak zorunda değildir.
Bu özellikle concurrency hataları gibi bazı senaryolarda oldukça işe yarıyor. Hatanın sebebini düzelttikten sonra aynı transaction içinde yeniden deneme şansınız olabilir.
Buraya kadar gayet güzel.
Sonra devreye MARS giriyor.

## MARS açıksa savepoint yok

EF Core dokümantasyonundaki kritik detay şu:
SQL Server connection’da MARS açıksa EF Core savepoint oluşturmuyor.
Üstelik kodun gerçekten MARS kullanıyor olması gerekmiyor.
Connection string’de şu ayarın bulunması yeterli:

```
MultipleActiveResultSets=True
```

Bunu ilk öğrendiğimde benim de dikkatimi çeken nokta buydu.
Çünkü insan doğal olarak şunu düşünüyor:

> “Ben açık reader üzerinde ikinci sorgu çalıştırmıyorum ki. Bana ne MARS’tan?”

Ama EF Core açısından durum öyle değil.
Özellik connection seviyesinde aktifse savepoint mekanizması devre dışı kalıyor.
Normal durumda kabaca şu akış var:

```
Transaction açık

    ↓

SaveChanges

    ↓

Savepoint

    ↓

SQL komutları

    ↓

Hata

    ↓

Savepoint'e geri dön
```

MARS açık olduğunda ise:

```
Transaction açık

    ↓

SaveChanges

    ↓

Savepoint yok

    ↓

SQL komutları

    ↓

Hata

    ↓

EF Core önceki güvenli noktaya
otomatik olarak dönemiyor
```

Bence meselenin en kritik kısmı burası.

## Bu, transaction bozuldu demek mi?

Tam olarak değil.
Burada ifadeyi dikkatli kullanmak lazım.
MARS açıkken `SaveChanges` hata verdi diye:

```
Transaction kesin bozuldu.
```

diyemeyiz.
Aynı şekilde:

```
Veri kaybı oldu.
```

demek de yanlış olur.
Asıl problem şu:
EF Core artık transaction’ı otomatik olarak bilinen güvenli bir savepoint’e döndüremiyor.
Yani framework size:

> “Başarısız SaveChanges öncesindeki duruma geri döndüm, devam edebilirsin.”

garantisini veremiyor.
Ben böyle bir durumda transaction üzerinde hiçbir şey olmamış gibi devam etmeyi tercih etmem.
Daha güvenli yaklaşım transaction’ın tamamını geri almak olur:

```
await using var transaction =
    await dbContext.Database.BeginTransactionAsync();

try
{
    await DoSomethingAsync();

    await dbContext.SaveChangesAsync();

    await DoSomethingElseAsync();

    await dbContext.SaveChangesAsync();

    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}
```

Zaten çoğu uygulamada transaction yönetimi bu şekilde yapılıyor.
Ama MARS ve savepoint ilişkisini bilmek, özellikle aynı transaction içinde hata sonrası devam etmeyi planlayan kodlarda ciddi fark yaratıyor.

## Asıl soru: MARS gerçekten gerekli mi?

Benim burada kendime sorduğum soru buydu.
Connection string’de:

```
MultipleActiveResultSets=True
```

var.
Peki gerçekten neden var?
Bazı eski projelerde bu sorunun cevabı maalesef şu olabiliyor:

> Çünkü hep vardı.

Bir projeden diğerine connection string kopyalanmış. Sonra başka bir servis oradan örnek almış. Birkaç yıl sonra kimse bu parametrenin neden eklendiğini bilmiyor.
Ben bunu yalnızca MARS için değil connection string’deki birçok ayarda gördüm.
O yüzden artık:

```
Orada duruyor → kesin gereklidir
```

varsayımına pek güvenmiyorum.
Ama diğer uç da yanlış.
“Gereksiz gibi duruyor” deyip production’dan doğrudan silmek de doğru değil.
Önce kodun davranışına bakmak gerekiyor.

## Nerelere bakmak lazım?

İlk kontrol edeceğim yer manuel ADO.NET veya Dapper kullanımları olur.
Örneğin:

```
await using var reader =
    await command.ExecuteReaderAsync();

while (await reader.ReadAsync())
{
    // Reader açıkken aynı connection ile
    // başka query çalıştırılıyor mu?
}
```

Aynı `SqlConnection` ile iç içe sorgular varsa MARS gerçekten gerekli olabilir.
EF Core tarafında ise streaming yapan yapılar daha ilginç.
Mesela:

```
await foreach (var user in dbContext.Users.AsAsyncEnumerable())
{
    var orders = await dbContext.Orders
        .Where(x => x.UserId == user.Id)
        .ToListAsync();
}
```

Burada dış sorgu henüz tamamlanmamış olabilir.
`Users` sonuçları stream edilirken içeride yeniden SQL çalıştırmaya çalışıyoruz.
Bu tür kodlara özellikle dikkat etmek gerekir.
Öte yandan önce sonucu tamamen materialize edersek:

```
var users = await dbContext.Users
    .ToListAsync();

foreach (var user in users)
{
    var orders = await dbContext.Orders
        .Where(x => x.UserId == user.Id)
        .ToListAsync();
}
```

ilk sorgu tamamlanmış olur.
Aktif reader kalmaz.
Tabii bu defa karşımıza başka bir arkadaş çıkar:
**N+1 query problemi.**
Bir problemi çözerken başka birini üretmemek lazım.
Bu nedenle çoğu durumda daha düzgün çözüm veriyi baştan doğru şekilde çekmek olabilir:

```
var users = await dbContext.Users
    .Include(x => x.Orders)
    .ToListAsync();
```

veya gerçekten ihtiyaç duyulan alanları projection ile almak:

```
var users = await dbContext.Users
    .Select(x => new
    {
        x.Id,
        x.Name,
        Orders = x.Orders.Select(o => new
        {
            o.Id,
            o.Total
        })
    })
    .ToListAsync();
```

Açıkçası ben ikinci yaklaşımı daha çok seviyorum.
Özellikle büyük tablolarda entity graph’ın tamamını belleğe taşımak yerine gerçekten kullanılacak kolonları çekmek hem daha kontrollü hem de daha okunabilir oluyor.

## MARS açıp DbContext’i paralel kullanabilir miyiz?

Burada da kolayca yanlış bir yere gidilebilir.
Şu kodu düşünelim:

```
var usersTask = dbContext.Users.ToListAsync();
var ordersTask = dbContext.Orders.ToListAsync();

await Task.WhenAll(usersTask, ordersTask);
```

“Nasıl olsa MARS açık, çalışır” demek doğru değil.
`DbContext` aynı instance üzerinden paralel operasyon çalıştırmak için tasarlanmış değil.
Ben böyle bir ihtiyaç varsa MARS’a değil, context yaşam döngüsüne bakarım.
Mesela `IDbContextFactory` ile iki ayrı instance:

```
await using var usersDb =
    await contextFactory.CreateDbContextAsync();

await using var ordersDb =
    await contextFactory.CreateDbContextAsync();

var usersTask = usersDb.Users.ToListAsync();
var ordersTask = ordersDb.Orders.ToListAsync();

await Task.WhenAll(usersTask, ordersTask);
```

Böylece iki işlem birbirinden bağımsız ilerler.
Buradaki ayrımı net tutmak lazım:

```
MARS
=
Aynı connection üzerinde
birden fazla aktif result set
```

şu değil:

```
MARS
=
DbContext paralel çalışabilir
```

İkisi farklı problemler.

## Peki MARS’ı kapatırsak ne değişir?

MARS zaten varsayılan olarak kapalı.
Yani connection string’de hiç belirtmezsek:

```
MultipleActiveResultSets=False
```

gibi davranır.
Dolayısıyla:

```
Server=localhost;
Database=AppDb;
Trusted_Connection=True;
MultipleActiveResultSets=True;
```

yerine:

```
Server=localhost;
Database=AppDb;
Trusted_Connection=True;
```

kullanmak yeterli.
İsterseniz açıkça:

```
MultipleActiveResultSets=False
```

da yazabilirsiniz.
Ben genelde gerekmeyen parametreyi connection string’den tamamen çıkarmayı tercih ediyorum.
Çünkü birkaç yıl sonra o ayara bakan kişi:

> “Bu özellikle false yapılmış, acaba neden?”

diye başka bir gizemle uğraşmasın.

## Production’da direkt kaldırır mıyım?

Ben kaldırmazdım.
Önce bir arama yapardım.
Özellikle şunlara bakardım:

* `ExecuteReader` kullanımları
* Dapper ile açık reader üzerinde yapılan işlemler
* Aynı `SqlConnection` nesnesinin tekrar kullanıldığı yerler
* `AsAsyncEnumerable()` kullanan sorgular
* `await foreach` içinde database erişimleri
* Uzun süre açık tutulan manuel connection’lar
* Aynı `DbContext` üzerinden stream sırasında yapılan yeni sorgular

Sonra integration testleri çalıştırırdım.
Mümkünse gerçek sorgu akışlarını da gözlemlerdim.
Çünkü MARS’ı kapatınca ortaya çıkacak problem genellikle oldukça net olur: hâlâ açık bir reader varken başka komut çalıştırmaya çalışan kod patlamaya başlar.
Bu aslında iyi bir şey de olabilir.
Yıllardır gizli kalmış bir sorgu tasarımını ortaya çıkarabilirsiniz.

## Connection string de kodun bir parçası

Bu konudan benim çıkardığım asıl ders biraz daha genel oldu.
Biz genellikle code review yaparken şuralara bakıyoruz:

```
Service
Repository
DbContext
LINQ
SQL
Transaction
```

Connection string ise çoğu zaman altyapı detayı gibi kalıyor.
Halbuki:

```
MultipleActiveResultSets=True
```

gibi tek bir parametre framework’ün çalışma biçimini değiştirebiliyor.
Üstelik bunu kod tarafında hiçbir yerde görmüyorsunuz.
Benzer şekilde retry, timeout, pooling veya encryption ayarları da uygulamanın davranışında ciddi fark yaratabiliyor.
Bu nedenle özellikle eski sistemlerde connection string’i de uygulama kodunun bir parçası gibi okumak gerektiğini düşünüyorum.
“Sistem çalışıyor, dokunmayalım” bazen doğru karar olabilir.
Ama “bu ayar neden burada?” sorusunu sormamak da teknik borcun sessizce büyümesine yol açıyor.

## Sonuç

MARS kötü bir özellik değil. Belirli bir problemi çözmek için var ve gerçekten ihtiyacınız olduğunda işini yapıyor.
Ama genel bir performans optimizasyonu da değil.
Daha önemlisi, EF Core kullanıyorsanız yan etkisini bilmek gerekiyor: MARS aktifken otomatik savepoint mekanizması kullanılamıyor.
Benim için konunun özeti şu oldu:

```
MARS gerekiyorsa kullan.

Ama neden gerektiğini bil.
```

Connection string’de yıllardır duran:

```
MultipleActiveResultSets=True
```

satırına bir daha denk gelirsem artık gözüm otomatik olarak üzerinden geçmeyecek.
Önce kod tarafına bakacağım.
Gerçekten açık bir result set üzerinde ikinci sorguya ihtiyaç var mı? Yoksa bu ayar yalnızca geçmişten taşınan bir alışkanlık mı?
Çünkü bazen uygulamanın davranışını değiştiren detay, yüzlerce satırlık servis kodunda değil; connection string’in sonunda unutulmuş tek bir parametrede çıkıyor.