Merhaba
Standalone bir SQL Server’da yedekleme tek bir sorunun cevabıdır: bu instance üzerindeki veritabanlarını kim, ne zaman, nereye yazacak? Yedeği alan node ile veritabanının bulunduğu node aynıdır, yedek zincirinin sahibi tektir, msdb içindeki yedek geçmişi tam ve tutarlıdır.
Always On Availability Group devreye girdiğinde bu varsayımların tamamı bozulur. Bir veritabanı artık tek bir instance’a ait değildir; iki, üç ya da daha fazla replikada eşzamanlı olarak mevcuttur. Yedek alma yetkisi replikalar arasında çalışma zamanında belirlenen bir tercih mekanizmasıyla dağıtılır. Log zinciri AG (Availability Group) genelinde tektir ama yedek geçmişi her node’un kendi msdb veritabanında ayrı ayrı tutulur. Secondary replikada bazı yedek türleri desteklenirken bazıları motor seviyesinde reddedilir.
Bunun pratik sonucu şudur: Standalone bir ortam için doğru yazılmış bir yedekleme script’i, AG (Availability Group)’li bir ortama olduğu gibi taşındığında çalışmaya devam ediyormuş gibi görünür. Job yeşil yanar, hata log’u temizdir, fakat ya hiç yedek alınmamıştır ya da alınan yedekler geri dönüş anında zincir tutarsızlığı nedeniyle kullanılamaz durumdadır. Her iki hata da sessizdir ve genellikle ilk gerçek restore denemesinde ortaya çıkar.
Bu yazıda yaygın bir standalone yedekleme script’ini ele alıyor, AG mimarisinde nerede ve neden kırıldığını motor davranışı seviyesinde inceliyor, AG farkındalıklı bir sürümünü kuruyor ve son bölümde iki node’lu gerçek bir SQL Server 2025 AG ortamında çıktılarla doğruluyorum.
AG (Availability Group)’de Yedeklemenin Mimari Farkları
Script’e geçmeden önce, düzeltmelerin dayandığı üç mekanizmayı netleştirmek gerekiyor.
Yedekleme tercihi replika değil, AG (Availability Group) seviyesinde tanımlanır
Her Availability Group’un automated_backup_preference adında bir ayarı vardır.
Bu ayar tinyint tipindedir ve dört değerden birini alır:
| Sayısal değer | _desc karşılığı |
SSMS’teki adı | Davranış |
|---|---|---|---|
0 |
primary |
Primary | Yedek yalnızca primary replikada alınır |
1 |
secondary_only |
Secondary only | Yedek yalnızca secondary’de alınır, primary hiçbir zaman seçilmez |
2 |
secondary |
Prefer Secondary | Secondary tercih edilir; online tek replika primary ise primary’ye düşer |
3 |
none |
Any Replica | Tercih yok, önceliğe göre herhangi bir replika seçilebilir |
AG (Availability Group)’yi SSMS (SQL Server Management Studio) sihirbazıyla kurduğunuzda varsayılan 2 (Prefer Secondary) gelir.
Mevcut ayarı şöyle görürsünüz:
SELECT name,
automated_backup_preference,
automated_backup_preference_desc
FROM sys.availability_groups;
Sütunların anlamı:
| Sütun | Ne gösterir |
|---|---|
name |
Availability Group’un adı |
automated_backup_preference |
Tercihin sayısal değeri (yukarıdaki tablo) |
automated_backup_preference_desc |
Aynı değerin metin karşılığı |
Tercihi değiştirmek için:
ALTER AVAILABILITY GROUP [AG_ADI]
SET (AUTOMATED_BACKUP_PREFERENCE = SECONDARY_ONLY);
Dikkat: 1 (secondary_only) seçiliyken AG (Availability Group)’de online secondary kalmazsa yedek hiç alınmaz, primary devreye girmez. Bu ayarla çalışıyorsanız replika sağlığını izlemek yedeklemenin bir parçası haline gelir.
Tercihin yanında her replikanın 0 ile 100 arasında bir backup_priority değeri vardır. Yüksek olan tercih edilir; 0 değeri o replikanın yedekleme için hiç seçilmemesi anlamına gelir.
SELECT ar.replica_server_name,
ar.backup_priority,
ars.role_desc,
ars.synchronization_health_desc
FROM sys.availability_replicas AS ar
JOIN sys.dm_hadr_availability_replica_states AS ars
ON ar.replica_id = ars.replica_id;
| Sütun | Ne gösterir |
|---|---|
replica_server_name |
Replikanın sunucu adı. Listener adı değil, gerçek node adıdır |
backup_priority |
0-100 arası yedekleme önceliği. 0 ise bu replika hiç seçilmez |
role_desc |
O anki rol: PRIMARY veya SECONDARY |
synchronization_health_desc |
HEALTHY, PARTIALLY_HEALTHY veya NOT_HEALTHY |
BACKUP DATABASE çalıştırmanızı engellemez. Bu ayarlar yalnızca bir tavsiye kaydıdır ve bu tavsiyeyi okuyup uygulamak yedekleme çözümünün, yani bizim script’imizin sorumluluğudur. Bu tavsiyeyi okuyan fonksiyon sys.fn_hadr_backup_is_preferred_replica’dır ve çalıştığı node için 1 ya da 0 döner.Secondary replikada her yedek türü desteklenmez
Secondary replika salt okunur bir kopyadır ve yedekleme motoru bu durumu dikkate alır. Desteklenen ve desteklenmeyen işlemler şunlardır:
| İşlem | Primary | Secondary |
|---|---|---|
| Full backup | Evet | Yalnızca COPY_ONLY ile |
| Differential backup | Evet | Desteklenmez |
| Log backup | Evet | Evet, zincire dahil olarak |
COPY_ONLY log backup |
Evet | Desteklenmez |
| Dosya / filegroup backup | Evet | Desteklenmez |
Secondary’de COPY_ONLY olmadan full backup almaya çalışırsanız motor hatayla reddeder. Differential backup ise secondary’de hiçbir şekilde alınamaz; bu, differential kullanan bir stratejide yedekleme tercihini zorunlu olarak PRIMARY yapar.
Log backup tarafındaki davranış özellikle dikkat çekicidir: secondary’de alınan log backup COPY_ONLY değildir ve log zincirini ilerletir. Yani log zinciri AG (Availability Group) genelinde tek bir zincirdir ve hangi replikada alınırsa alınsın aynı zinciri devam ettirir. Bu, log yedeklerinin replikalar arasında dolaşmasını güvenli kılar.
Yedek geçmişi her node’da ayrıdır
msdb.dbo.backupset ve backupmediafamily tabloları instance bazındadır. Yedeği secondary node aldıysa, primary’nin msdb veritabanında o yedeğin kaydı bulunmaz.
Bunun iki sonucu vardır:
- SSMS (SQL Server Management Studio)’in Restore Database ekranındaki otomatik yedek geçmişi listesi, AG (Availability Group) ortamında eksik görünür. Restore planlaması yaparken tüm replikaların
msdbkayıtlarını birleştirmek gerekir. - Yedeklerin varlığını
msdbüzerinden izleyen monitoring sorguları, yedeğin başka bir node’da alınmış olması nedeniyle yanlış alarm üretebilir.
Bu üç mekanizma anlaşıldığında, standalone script’in nerede kırıldığı netleşir.
Sorun 1: is_read_only = 0 Filtresi AG (Availability Group)’de Hiçbir Şeyi Yedeklemez
Standalone script’lerde veritabanı listesi tipik olarak şöyle oluşturulur:
SELECT name
FROM sys.databases
WHERE name IN ('BAKICUBUK_DB1', 'BAKICUBUK_DB2', 'BAKICUBUK_DB3')
AND state = 0
AND is_read_only = 0;
Bu filtrenin standalone’daki amacı açıktır: ALTER DATABASE ... SET READ_ONLY ile salt okunur moda alınmış veritabanlarını atlamak. Mantıklı bir savunmadır.
AG (Availability Group)’de ise yanlış bir sinyali okur. Bir AG (Availability Group)’un secondary replikasındaki veritabanları sys.databases içinde her zaman is_read_only = 1 olarak görünür. Bu değer, veritabanının salt okunur moda alınmış olmasından değil, secondary rolünün doğasından kaynaklanır: secondary’deki veritabanı yazma kabul etmez. Readable secondary özelliğinin açık ya da kapalı olması bu değeri değiştirmez; kapalıysa veritabanı zaten hiç erişilebilir değildir, açıksa salt okunur erişilebilir.
Sonuç, senaryoya göre iki farklı biçimde ortaya çıkar:
- Yedekleme tercihi
SECONDARY_ONLYveyaSECONDARYise ve o gece secondary seçilmişse, script secondary’de çalışır, filtre AG (Availability Group) üyesi tüm veritabanlarını eler, cursor boş döner ve hiçbir şey yedeklenmez. - Job
THROWile yalnızca yedekleme sırasında hata olursa fail ettiği için, boş cursor bir hata değildir. Job başarılı olarak kapanır.
Yani failure modu sessizdir. Job geçmişinde yeşil tik, job_fullbackup.log dosyasında hata yok, yedek klasöründe ise o geceye ait hiçbir dosya yok. Bu durum genellikle aylar sonra, retention penceresi çoktan kaydığında fark edilir.
Çözüm, AG (Availability Group) üyeliğini filtrenin dışında tutmaktır:
SELECT name
FROM sys.databases
WHERE name IN ('BAKICUBUK_DB1', 'BAKICUBUK_DB2', 'BAKICUBUK_DB3')
AND state = 0
AND (is_read_only = 0 OR replica_id IS NOT NULL);
Bu sorgularda kullanılan sütunlar:
| Sütun | Ne gösterir |
|---|---|
state |
Veritabanının durumu. 0 = ONLINE. Metin karşılığı state_desc sütunundadır |
is_read_only |
1 ise veritabanı yazma kabul etmiyor. Secondary replikada AG (Availability Group) üyeleri için daima 1 |
replica_id |
Veritabanı bir AG (Availability Group)’ye üyeyse ilgili replikanın GUID değeri, değilse NULL |
replica_id IS NOT NULL koşuluyla AG üyesi veritabanları is_read_only durumundan bağımsız olarak listeye girer; AG dışındaki veritabanları ise eski korumayı aynen korur.
state = 0 kontrolü (ONLINE) yerinde duruyor ve AG (Availability Group)’de de anlamlıdır. Secondary’de RESTORING veya NOT SYNCHRONIZING durumundaki bir veritabanı bu kontrolle zaten elenir.
Sorun 2: COPY_ONLY Olmadan Alınan Full Backup Differential Zincirini Kırar
Yedekleme tercihi SECONDARY veya NONE olduğunda, yedeği alan node çalışma zamanında belirlenir. Bir gece primary, ertesi gece bir secondary, sonraki gece başka bir secondary seçilebilir. Replika sağlık durumu değiştikçe bu dağılım kendiliğinden kayar.
Normal, yani COPY_ONLY olmayan bir full backup çalıştığında iki şey yapar: veritabanının o anki kopyasını yazar ve differential base LSN değerini sıfırlar. Bu değer, sonraki differential yedeklerin hangi noktadan itibaren değişiklikleri toplayacağını belirleyen referanstır ve veritabanına ait tek bir değerdir.
AG (Availability Group) ortamında bu davranış şu soruna yol açar: farklı gecelerde farklı replikalardan COPY_ONLY olmadan alınan full yedekler, differential base’i her seferinde yeniden konumlandırır. Elinizde bir full ve ona bağlı differential yedekler olduğunu düşünürsünüz, oysa aradaki bir başka full backup base’i kaydırmıştır ve differential artık beklediğiniz full’e değil, başka bir node’da duran bir full’e bağlıdır. Restore zinciri, gerekli dosya o node’un diskinde kaldığı için tamamlanamaz.
Ayrıca motor seviyesinde bir kısıt daha vardır: secondary replikada COPY_ONLY olmayan full backup zaten çalışmaz, hata döner. Yani SECONDARY_ONLY tercihiyle çalışan bir ortamda COPY_ONLY eksikliği sessiz değil, gürültülü bir hataya dönüşür ve her gece job fail eder.
Bu yüzden AG (Availability Group) üyesi veritabanlarında tam yedek her zaman COPY_ONLY ile alınmalıdır:
@name ve @fileName değişkenleri job içindeki cursor döngüsünde tanımlanır. Bu bloğu doğrudan yeni bir sorgu penceresine yapıştırırsanız Msg 137, Must declare the scalar variable "@name" hatası alırsınız. Hemen altında tek başına çalışan test sürümü var.BACKUP DATABASE @name
TO DISK = @fileName
WITH COPY_ONLY, INIT, COMPRESSION, CHECKSUM, STATS = 10;
Tek başına denemek isterseniz değişkenleri kendiniz tanımlayın:
DECLARE @name SYSNAME = N'BAKICUBUK_DB1';
DECLARE @fileName NVARCHAR(400) = N'H:\BACKUP\BAKICUBUK_DB1_copyonly_test.bak';
BACKUP DATABASE @name
TO DISK = @fileName
WITH COPY_ONLY, INIT, COMPRESSION, CHECKSUM, STATS = 10;
BACKUP DATABASE hem veritabanı adını hem de hedef dosya yolunu değişken olarak kabul eder, bu yüzden bu blok olduğu gibi çalışır. COPY_ONLY kullanıldığı için test yedeği differential base’e dokunmaz, üretimdeki zinciri bozmaz.
AG (Availability Group) üyesi olmayan veritabanları için COPY_ONLY gereksizdir ve eklenmemelidir; aksi halde o veritabanları için differential stratejisi kurma imkanı kapanır. Script bu ayrımı veritabanı bazında yapmalıdır.
Sorun 3: Job Hangi Node’da Ne Yapacağını Bilmiyor
Standalone mantıkta job tek bir sunucuya kurulur ve her çalıştığında yedek alır. AG (Availability Group)’de bu model iki yönden de başarısızdır:
- Job yalnızca primary’ye kurulursa, failover sonrası eski primary secondary rolüne düşer ve yedekleme durur. Yeni primary’de job yoktur.
- Job tüm node’lara kurulur ama tercih kontrolü yapılmazsa, her node her gece kendi kopyasını alır. Aynı veritabanının üç ayrı yedeği üç ayrı diske yazılır, disk ve CPU gereksiz tüketilir, hangi dosyanın otoriter olduğu belirsizleşir.
sys.fn_hadr_backup_is_preferred_replica’dır:IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @name AND replica_id IS NOT NULL)
BEGIN
SET @isAgDb = 1;
SET @isPreferred = sys.fn_hadr_backup_is_preferred_replica(@name);
END
Tek başına çalışan hali:
DECLARE @name SYSNAME = N'BAKICUBUK_DB1';
DECLARE @isAgDb BIT = 0;
DECLARE @isPreferred BIT = 1;
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @name AND replica_id IS NOT NULL)
BEGIN
SET @isAgDb = 1;
SET @isPreferred = sys.fn_hadr_backup_is_preferred_replica(@name);
END
SELECT @@SERVERNAME AS bu_node,
@name AS veritabani,
@isAgDb AS ag_uyesi_mi,
@isPreferred AS tercih_edilen_mi;
Fonksiyon, AG (Availability Group)’nin automated_backup_preference ayarını, replikaların backup_priority değerlerini ve replikaların o anki bağlantı ve senkronizasyon sağlığını birlikte değerlendirir. Çalıştığı node bu veritabanı için tercih edilen replika ise 1, değilse 0 döner. Birden fazla replika aynı önceliğe sahipse seçim deterministik biçimde çözülür, yani aynı anda iki node’un birden 1 alması beklenmez.
@isPreferred = 0 döndüğünde o veritabanı bu node’da atlanır ve durum log’a yazılır. Kritik olan, bu atlamanın hata sayılmamasıdır; asıl yedeği tercih edilen replika zaten alacaktır. Atlamayı hata olarak işaretlemek, üç node’lu bir AG (Availability Group)’de her gece iki node’un job’ını kırmızıya boyar ve gerçek hataları görünmez kılar.
AG (Availability Group) üyesi olmayan veritabanları için fonksiyon çağrılmaz ve davranış standalone mantığıyla aynı kalır.
Sorun 4: Retention Penceresi AG (Availability Group)’de Göründüğünden Kısadır
Bu, AG (Availability Group)’ye geçişte en sık gözden kaçan noktadır ve script’in kodunda değil, çıktısının dağılımında ortaya çıkar.
@retentionDays = 5 ayarı standalone bir sunucuda diskte son 5 günün yedeği durur anlamına gelir. Her gece bir yedek yazılır, 5 günden eskiler silinir, pencerede 5 dosya bulunur.
AG (Availability Group)’de yedek, tercih mekanizmasına göre node’lar arasında dolaşır. Her node kendi yerel diskine yazar ve xp_delete_file ile yalnızca kendi yerel diskindeki dosyaları tarih bazlı siler. Üç node’lu bir AG (Availability Group)’de yedekler node’lar arasında kabaca eşit dağılırsa, her node’un diskinde son 5 gün içinden yalnızca 1 ya da 2 dosya bulunur. Toplamda 5 günün yedeği mevcuttur ama üç ayrı sunucunun diskine dağılmış durumdadır.
Bunun operasyonel sonuçları şunlardır:
- Bir restore sırasında aradığınız tarihteki dosya, bağlandığınız node’da olmayabilir.
- Bir node’u yeniden kurar ya da diskini temizlerseniz, o node’un tuttuğu yedek dilimleri kaybolur ve retention penceresinde delikler oluşur.
- Retention’ı 5 gün diye raporlarsanız, tek bir node üzerinden bakan biri pencereyi 1 ya da 2 gün olarak görür.
Üretim ortamı için doğru çözüm, tüm replikaların ortak bir UNC paylaşımına yazmasıdır:
DECLARE @path NVARCHAR(256) = N'\\backupsrv\SQLBackup\PROD\';
Bu durumda retention mantığı tek bir dizin üzerinde çalışır ve pencere gerçekten 5 gün olur. Bunun gereksinimi, tüm replikaların SQL Server servis hesaplarının bu paylaşıma yazma ve silme yetkisine sahip olmasıdır. Domain ortamında en temiz yöntem, servis hesaplarını bir AD grubuna alıp paylaşım ve NTFS yetkisini o gruba vermektir.
UNC kullanılamıyorsa, yerel diskler korunup retention süresi replika sayısıyla çarpılarak telafi edilebilir: üç node’lu bir AG (Availability Group)’de gerçek 5 günlük pencere için @retentionDays değerini 15 civarına çıkarmak gerekir. Bu bir çözüm değil, bir telafidir; dosyaların hangi node’da olduğu belirsizliği devam eder.
Ayrıca xp_delete_file hakkında iki not: prosedür Microsoft tarafından resmi olarak belgelenmemiştir ve sildiği dosyaların yedek başlığını okuyarak geçerli bir yedek olup olmadığını kontrol eder. Bozuk ya da yarım yazılmış bir .bak dosyası bu kontrolden geçemez ve silinmez, diskte birikir. Buna karşılık xp_cmdshell gerektirmediği için güvenlik açısından tercih edilir. Disk doluluğunu bu yüzden ayrıca izlemek gerekir.
Tam Script
Aşağıdaki script yukarıdaki düzeltmeleri içerir. AG (Availability Group) üyesi olmayan veritabanları için davranış standalone sürümle aynıdır; AG üyesi veritabanları için tercih kontrolü ve COPY_ONLY devreye girer.
USE msdb;
GO
-- 0. VARSA ESKI JOB SIL
IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N'FullDBBackup_AG')
EXEC msdb.dbo.sp_delete_job @job_name = N'FullDBBackup_AG';
GO
-- KATEGORI OLUSTUR (yoksa)
IF NOT EXISTS (SELECT 1 FROM msdb.dbo.syscategories WHERE name = N'Database Maintenance')
EXEC msdb.dbo.sp_add_category @class = N'JOB', @type = N'LOCAL', @name = N'Database Maintenance';
GO
-- 1. JOB OLUSTUR
EXEC msdb.dbo.sp_add_job
@job_name = N'FullDBBackup_AG',
@enabled = 1,
@description = N'AG-farkindalikli tam yedek + dogrulama + retention. Bu job TUM replikalara ayni sekilde kurulmalidir; her calismada hangi node''un yedegi alacagina AG''nin backup preference ayari karar verir.',
@owner_login_name = N'sa',
@category_name = N'Database Maintenance';
GO
-- 2. STEP EKLE
EXEC msdb.dbo.sp_add_jobstep
@job_name = N'FullDBBackup_AG',
@step_name = N'Backup',
@subsystem = N'TSQL',
@database_name = N'master',
@retry_attempts = 2,
@retry_interval = 5,
@output_file_name = N'H:\BACKUP\job_fullbackup.log',
@flags = 2,
@command = N'
SET NOCOUNT ON;
DECLARE @name SYSNAME;
DECLARE @path NVARCHAR(256) = N''H:\BACKUP\'';
DECLARE @fileName NVARCHAR(400);
DECLARE @fileDate NVARCHAR(20);
DECLARE @retentionDays INT = 5;
DECLARE @hasError BIT = 0;
DECLARE @isAgDb BIT;
DECLARE @isPreferred BIT;
SET @fileDate = REPLACE(REPLACE(REPLACE(CONVERT(VARCHAR(19), GETDATE(), 120), ''-'', ''''), '' '', ''_''), '':'', '''');
-- ONEMLI DUZELTME: orijinal script''teki "is_read_only = 0" filtresi kaldirildi.
-- Bir AG''nin secondary replikasindaki veritabanlari sys.databases''ta HER ZAMAN
-- is_read_only = 1 gorunur (readable secondary kapali olsa bile). O filtreyle bu
-- job secondary node''da calistirildiginda AG uyesi hicbir DB''yi yedeklemezdi.
DECLARE db_cursor CURSOR FAST_FORWARD FOR
SELECT name
FROM sys.databases
WHERE name IN (''BAKICUBUK_DB1'', ''BAKICUBUK_DB2'', ''BAKICUBUK_DB3'', ''BAKICUBUK_DB4'', ''BAKICUBUK_DB5'')
AND state = 0
AND (is_read_only = 0 OR replica_id IS NOT NULL);
OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO @name;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @isAgDb = 0;
SET @isPreferred = 1;
-- DB bir Availability Group uyesiyse (replica_id NULL degilse), bu node''un
-- yedegi alip almayacagina AG''nin backup preference ayari karar versin.
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @name AND replica_id IS NOT NULL)
BEGIN
SET @isAgDb = 1;
SET @isPreferred = sys.fn_hadr_backup_is_preferred_replica(@name);
END
IF @isAgDb = 0 OR @isPreferred = 1
BEGIN
SET @fileName = @path + @name + ''_'' + @fileDate + ''.bak'';
BEGIN TRY
IF @isAgDb = 1
BEGIN
-- AG uyesi DB''lerde COPY_ONLY ZORUNLU: backup preference''a gore
-- farkli calismalarda yedek farkli replikalardan alinabildigi icin,
-- COPY_ONLY olmayan bir FULL backup, diger replikalardaki
-- differential base''i ve (varsa) log zincirini bozar.
BACKUP DATABASE @name
TO DISK = @fileName
WITH COPY_ONLY, INIT, COMPRESSION, CHECKSUM, STATS = 10;
END
ELSE
BEGIN
BACKUP DATABASE @name
TO DISK = @fileName
WITH INIT, COMPRESSION, CHECKSUM, STATS = 10;
END
RESTORE VERIFYONLY
FROM DISK = @fileName
WITH CHECKSUM;
PRINT ''BASARILI: '' + @name + '' -> '' + @fileName
+ CASE WHEN @isAgDb = 1 THEN '' (AG / COPY_ONLY)'' ELSE '''' END;
END TRY
BEGIN CATCH
PRINT ''HATA: '' + @name + '' -> '' + ERROR_MESSAGE();
SET @hasError = 1;
END CATCH;
END
ELSE
BEGIN
-- Bu node, bu veritabani icin AG''nin tercih ettigi yedekleme replikasi
-- degil. Job basarisiz sayilmaz; asil yedegi tercih edilen replika alir.
PRINT ''ATLANDI: '' + @name + '' - bu replika, bu veritabani icin tercih edilen yedekleme replikasi degil.'';
END
FETCH NEXT FROM db_cursor INTO @name;
END
CLOSE db_cursor;
DEALLOCATE db_cursor;
BEGIN TRY
DECLARE @deleteDate DATETIME = DATEADD(DAY, -@retentionDays, GETDATE());
EXEC master.dbo.xp_delete_file 0, @path, N''bak'', @deleteDate;
END TRY
BEGIN CATCH
PRINT ''Silme hatasi: '' + ERROR_MESSAGE();
END CATCH;
IF @hasError = 1
THROW 50001, N''Backup hatasi olustu!'', 1;
';
GO
-- 3. SCHEDULE
EXEC msdb.dbo.sp_add_schedule
@schedule_name = N'HerGece2100',
@enabled = 1,
@freq_type = 4,
@freq_interval = 1,
@active_start_time = 210000;
GO
EXEC msdb.dbo.sp_attach_schedule
@job_name = N'FullDBBackup_AG',
@schedule_name = N'HerGece2100';
GO
-- 4. SUNUCUYA BAGLA
EXEC msdb.dbo.sp_add_jobserver
@job_name = N'FullDBBackup_AG',
@server_name = @@SERVERNAME;
GO
Backup adımındaki seçenekler
| Seçenek | Ne yapar | AG (Availability Group)’deki önemi |
|---|---|---|
COPY_ONLY |
Differential base’e dokunmadan tam yedek alır | AG (Availability Group) üyesi veritabanlarında zorunlu |
COMPRESSION |
Yedeği sıkıştırır | Ağ üzerinden UNC’ye yazarken trafiği belirgin azaltır, CPU maliyeti getirir |
CHECKSUM |
Okunan her sayfanın checksum değerini doğrular, yedek dosyasına checksum yazar | Secondary’den yedek alırken sayfa bozulmasının erken yakalanmasını sağlar |
INIT |
Medya setini üzerine yazar | Dosya adları zaman damgalı olduğu için pratikte etkisizdir, güvenlik amaçlı bırakılmıştır |
STATS = 10 |
Her yüzde 10’da ilerleme yazar | Uzun süren yedeklerde job log’undan ilerleme takibi sağlar |
RESTORE VERIFYONLY ... WITH CHECKSUM adımı, yedek dosyasının okunabilir olduğunu ve checksum değerlerinin tuttuğunu doğrular. Bunun bir restore denemesi olmadığını belirtmek gerekir: dosya bütünlüğünü kontrol eder, veritabanının mantıksal tutarlılığını kontrol etmez. Gerçek güvence yalnızca periyodik test restore’larıyla elde edilir.
Dosya adı nasıl oluşuyor
@fileDate satırı ilk bakışta karmaşık görünür ama tamamen mekaniktir, düzenlemeniz gereken bir alan değildir. İçten dışa doğru çalışır:
CONVERT(VARCHAR(19), GETDATE(), 120) şu anki zamanı 2026-09-17 16:26:53 biçiminde verir. Ardından üç REPLACE sırayla temizlik yapar:
| Adım | İşlem | Ara sonuç |
|---|---|---|
| 1 | - karakterlerini siler |
20260917 16:26:53 |
| 2 | Boşluğu _ yapar |
20260917_16:26:53 |
| 3 | : karakterlerini siler |
20260917_162653 |
Sonuçta dosya adı H:\BACKUP\BAKICUBUK_DB1_20260917_162653.bak olur. Bu üç değişimin sebebi Windows dosya adı kurallarıdır: iki nokta üst üste karakteri dosya adında yasaktır, boşluk ise yasak değil ama komut satırında sorun çıkarır. Kalan biçim hem geçerlidir hem de ada göre sıralandığında kronolojik sırayı verir.
Önemli bir ayrıntı: @fileDate cursor döngüsünden önce bir kez hesaplanır. Yani tek bir çalışmada yedeklenen tüm veritabanlarının dosyası aynı zaman damgasını taşır, dosyaların diske yazılma saatleri farklı olsa bile. Bu kasıtlıdır; hangi dosyaların aynı çalışmada üretildiğini damgadan anlarsınız.
Yedekler node’lar arasında dolaştığı için dosya adına node adını eklemek isterseniz, @fileName satırını şöyle değiştirin:
SET @fileName = @path + @name + ''_'' + REPLACE(@@SERVERNAME, ''\'', ''_'') + ''_'' + @fileDate + ''.bak'';
REPLACE orada durur çünkü named instance kullanılan ortamlarda @@SERVERNAME değeri SUNUCU\INSTANCE biçiminde gelir ve içindeki ters bölü dosya yolunu bozar.
Script’i Düzenlerken: İki Katmanlı String Tuzağı
Bu script’in en sinsi tarafı, yedekleme mantığı değil, yapısıdır. Job adımının komutu @command = N'...' şeklinde bir string olarak saklanır. Yani script’i düzenlerken iki katmanlı bir metinle uğraşırsınız: dış katman sp_add_jobstep’e verilen string, iç katman SQL Agent’ın çalışma anında çalıştıracağı asıl T-SQL.
| İç katmanda görünmesini istediğiniz | Dış katmanda yazmanız gereken |
|---|---|
'H:\BACKUP\' |
''H:\BACKUP\'' |
'BAKICUBUK_DB1' |
''BAKICUBUK_DB1'' |
'Metin' (string sabiti) |
''Metin'' |
it's (metnin içinde apostrof) |
it''''s (dört tırnak) |
Son satır özellikle tehlikelidir ve Türkçe yazarken kolayca tuzağa düşürür. Örneğin bir PRINT mesajına “node’u” yazmak isterseniz:
PRINT ''ATLANDI: '' + @name + '' - ... yedekleme node''u degil.'';
Bu satır kurulumda hata vermez. Dış katman çözüldüğünde iç katmana şu geçer:
PRINT 'ATLANDI: ' + @name + ' - ... yedekleme node'u degil.';
String node' ile kapanır, geri kalan u degil.' ise parse edilemez. Job ilk çalıştığında log dosyasında şunu görürsünüz:
Msg 102, Sev 15, State 1, Line 68 : Incorrect syntax near 'u'. [SQLSTATE 42000]
Msg 105, Sev 15, State 1, Line 86 : Unclosed quotation mark after the character string ', 1; '. [SQLSTATE 42000]
İkinci hata ayrı bir sorun değildir, ilkinin devamıdır: string erken kapandığı için geri kalan kod yanlış yorumlanır.
Doğrusu node''''u yazmaktır, ama pratikte daha iyi bir yol var: iç komuttaki mesajlarda apostrof hiç kullanmayın. node’u değil yerine replikasi degil yazarsanız tuzak ortadan kalkar ve script’i sizden sonra düzenleyecek kişi de aynı hatayı yapamaz.
Kurulumun başarılı olması job’ın çalışacağı anlamına gelmez
Yukarıdaki hatanın kurulum sırasında değil, job ilk kez çalıştığında ortaya çıkması tesadüf değildir. sp_add_jobstep çalıştığında @command içeriği yalnızca metin olarak msdb tablosuna yazılır, parse edilmez. Bu yüzden bozuk bir komutla bile kurulum script’i Commands completed successfully der ve job SSMS (SQL Server Management Studio)’te görünür.
Sonuç olarak AG (Availability Group) ortamında kurulum iki aşamalıdır ve ikincisi atlanmamalıdır:
- Script’i çalıştırın, job oluşsun.
- Job’ı
Start Job at Stepile elle bir kez tetikleyin vejob_fullbackup.logdosyasını açıp içeriğini okuyun.
İkinci adımı atlarsanız, ilk gerçek çalışmanın gece 21:00’de olduğunu ve hatayı ertesi sabah öğreneceğinizi kabul etmiş olursunuz.
Gerçek Ortamda Doğrulama
Bu bölümdeki çıktılar iki node’lu gerçek bir test ortamından alınmıştır.
| Bileşen | Değer |
|---|---|
| SQL Server sürümü | 17.0.1135.8 (SQL Server 2025) |
| Availability Group | SQL25HAG |
| Replikalar | W25SQL25NOD1, W25SQL25NOD2 |
| Listener | SQL25AO |
| Veritabanları | BAKICUBUK_DB1 , BAKICUBUK_DB2 , BAKICUBUK_DB3 , BAKICUBUK_DB4 ,BAKICUBUK_DB5 |
| Yedek klasörü | H:\BACKUP\ |
Önce bir uyarı: listener üzerinden secondary’yi göremezsiniz
Bu ortamda SQL25AO bir listener’dır, bir node değil. Listener’a yapılan bağlantı her zaman primary’ye yönlendirilir. Dolayısıyla listener’a bağlıyken ne secondary’nin is_read_only değerini görebilir ne de job’ı secondary’ye kurabilirsiniz.
Bu yazıdaki doğrulama sorgularını çalıştırırken ve job’ı kurarken SSMS’te doğrudan node adına (W25SQL25NOD1, W25SQL25NOD2) bağlanın. Bu ayrıntı gözden kaçtığında, job yalnızca o anki primary’ye kurulur ve Sorun 3’te anlatılan failover senaryosu aynen gerçekleşir.
Availability Group tercihi ve replika öncelikleri
SELECT name, automated_backup_preference, automated_backup_preference_desc
FROM sys.availability_groups;
| name | automated_backup_preference | automated_backup_preference_desc |
|---|---|---|
| SQL25HAG | 3 | none |

Availability Group yedekleme tercihi sorgusunun çıktısı. automated_backup_preference değeri 3, metin karşılığı küçük harfle none.
Bu ortamda tercih 3, yani Any Replica. Dikkat edilecek bir nokta: _desc sütunu bu sürümde küçük harfle dönüyor. İzleme sorgularında bu değeri metin olarak karşılaştırıyorsanız büyük/küçük harf duyarlılığına dikkat edin; sayısal sütunu kullanmak daha güvenlidir.
SELECT ar.replica_server_name, ar.backup_priority,
ars.role_desc, ars.synchronization_health_desc
FROM sys.availability_replicas AS ar
JOIN sys.dm_hadr_availability_replica_states AS ars ON ar.replica_id = ars.replica_id;
| replica_server_name | backup_priority | role_desc | synchronization_health_desc |
|---|---|---|---|
| W25SQL25NOD1 | 50 | PRIMARY | HEALTHY |
| W25SQL25NOD2 | 50 | SECONDARY | HEALTHY |

Replikaların yedekleme öncelikleri ve rolleri. İki replikanın da önceliği 50, yani beraberlik durumu.
Tercih none ve iki replikanın önceliği de eşit. Bu kombinasyon, yedeğin iki node arasında dolaşabileceği anlamına gelir; yani COPY_ONLY zorunluluğunun ve Sorun 4’teki retention dağılımının en keskin biçimde geçerli olduğu yapılandırmadır.
sys.databases çıktısı, primary replikada
SELECT name, state_desc, is_read_only, replica_id
FROM sys.databases
WHERE database_id > 4;
| name | state_desc | is_read_only | replica_id |
|---|---|---|---|
| BAKICUBUK_DB1 | ONLINE | 0 | D1C85D47-C73B-4D53-B226-A1E6D3AF9001 |
| BAKICUBUK_DB2 | ONLINE | 0 | D1C85D47-C73B-4D53-B226-A1E6D3AF9001 |
| BAKICUBUK_DB3 | ONLINE | 0 | D1C85D47-C73B-4D53-B226-A1E6D3AF9001 |
| BAKICUBUK_DB4 | ONLINE | 0 | D1C85D47-C73B-4D53-B226-A1E6D3AF9001 |
| BAKICUBUK_DB5 | ONLINE | 0 | D1C85D47-C73B-4D53-B226-A1E6D3AF9001 |

Primary replikada sys.databases çıktısı. is_read_only değeri 0, replica_id dolu. Aynı sorgu secondary’de is_read_only = 1 döndürür.
Beş veritabanının da replica_id değeri dolu ve aynı GUID’i taşıyor, yani beşi de aynı AG’ye üye. is_read_only değeri ise 0, çünkü bu çıktı primary replikadan alındı.
Buradaki asıl ders şudur: primary’de eski filtre ile yeni filtre aynı sonucu verir.
Eski filtre, primary’de: 3 satir doner:
WHERE name IN (...) AND state = 0 AND is_read_only = 0;
Yeni filtre, primary’de: yine ayni 3 satir doner:
WHERE name IN (...) AND state = 0 AND (is_read_only = 0 OR replica_id IS NOT NULL);

Eski filtre (is_read_only = 0) primary replikada. Üç veritabanı da listeye giriyor.

Yeni filtre (is_read_only = 0 OR replica_id IS NOT NULL) primary replikada. Sonuç birebir aynı; fark yalnızca secondary’de ortaya çıkar.
İki sorgu primary’de birbirinden ayırt edilemez. Hatanın sinsi olmasının sebebi tam olarak budur: geliştirme ve test genellikle primary’de yapılır, orada her şey doğru görünür, fark yalnızca secondary’de ortaya çıkar. Bu yüzden is_read_only doğrulamasını mutlaka secondary node’a doğrudan bağlanarak yapın; aynı sorgu orada AG üyeleri için is_read_only = 1 döndürecek ve eski filtre boş sonuç verecektir.
Tercih kontrolünün bu node’daki sonucu
Job’ın her veritabanı için sorduğu bu yedeği ben mi almalıyım? sorusunun cevabı, fn_hadr_backup_is_preferred_replica çağrısıyla tek başına da sınanabilir:
DECLARE @name SYSNAME = N'BAKICUBUK_DB1';
DECLARE @isAgDb BIT = 0;
DECLARE @isPreferred BIT = 1;
IF EXISTS (SELECT 1 FROM sys.databases WHERE name = @name AND replica_id IS NOT NULL)
BEGIN
SET @isAgDb = 1;
SET @isPreferred = sys.fn_hadr_backup_is_preferred_replica(@name);
END
SELECT @@SERVERNAME AS bu_node,
@name AS veritabani,
@isAgDb AS ag_uyesi_mi,
@isPreferred AS tercih_edilen_mi;
| bu_node | veritabani | ag_uyesi_mi | tercih_edilen_mi |
|---|---|---|---|
| W25SQL25NOD1 | BAKICUBUK_DB1 | 1 | 1 |

Tercih kontrolünün primary node’daki sonucu. ag_uyesi_mi = 1 veritabanının AG üyesi olduğunu, tercih_edilen_mi = 1 yedeği bu node’un alacağını gösteriyor.
ag_uyesi_mi = 1 değeri veritabanının AG üyesi olduğunu, tercih_edilen_mi = 1 ise bu node’un o veritabanı için tercih edilen yedekleme replikası olduğunu gösterir. Tercih none ve öncelikler eşitken beraberliğin W25SQL25NOD1 lehine çözüldüğü buradan okunur.
Bu sorguyu diğer node’da da çalıştırıp tercih_edilen_mi değerinin 0 geldiğini görmek, mekanizmanın doğru çalıştığının kesin kanıtıdır. İki node’da da 1 görürseniz aynı yedek iki kere alınıyor, iki node’da da 0 görürseniz hiç alınmıyor demektir.
Tek bir yedeğin COPY_ONLY ile testi
DECLARE @name SYSNAME = N'BAKICUBUK_DB1';
DECLARE @fileName NVARCHAR(400) = N'H:\BACKUP\BAKICUBUK_DB1_Backup.bak';
BACKUP DATABASE @name
TO DISK = @fileName
WITH COPY_ONLY, INIT, COMPRESSION, CHECKSUM, STATS = 10;
Çıktı:
Processed 249760 pages for database 'BAKICUBUK_DB1', file 'BAKICUBUK_DB1' on file 1.
Processed 43 pages for database 'BAKICUBUK_DB1', file 'BAKICUBUK_DB1_Log' on file 1.
BACKUP DATABASE successfully processed 249803 pages in 2.353 seconds (829.401 MB/sec).

Tek bir veritabanının COPY_ONLY ile yedeklenmesi. 249.803 sayfa, 2,35 saniye, 829 MB/sn.
Job’ın kurulumu
Script msdb üzerinde çalıştırıldığında yalnızca Commands completed successfully mesajı döner. Bu mesaj job’ın oluştuğunu söyler, komutunun çalışacağını söylemez.

Kurulum script’inin çıktısı. sp_add_jobserver adımına kadar her şey sorunsuz tamamlandı.
Job, SQL Server Agent > Jobs altında görünür hale gelir. Ağacı yenilemeniz gerekebilir.

FullDBBackup_AG job’u SQL Server Agent altında oluştu.
Bu noktada zamanlamayı beklemeden elle tetikleyin. Job üzerinde sağ tık, Start Job at Step seçeneği.

Tek adımlı bir job olduğu için adım seçimi sorulmadan çalışır ve sonucu bir özet penceresiyle bildirir.

Job elle tetiklendi ve başarıyla tamamlandı.
Job’ın ilk çalışmasının sonucu
H:\BACKUP\ klasörünün son hali:
| Dosya | Boyut | Diske yazılma saati |
|---|---|---|
| BAKICUBUK_DB1_20260917_162653.bak | 312 MB | 16:26 |
| BAKICUBUK_DB2_20260917_162653.bak | 251 MB | 16:26 |
| BAKICUBUK_DB3_20260917_162653.bak | 3,52 GB | 16:27 |
| BAKICUBUK_DB4_20260917_162653.bak | 481 MB | 16:27 |
| BAKICUBUK_DB5_20260917_162653.bak | 9,07 GB | 16:28 |
| job_fullbackup.log | 9 KB | 16:29 |

Beş veritabanının yedeği ve job log dosyası. Dosya adlarındaki zaman damgası beşinde de aynı: 20260917_162653.
Bu çıktıda doğrulanan üç şey var.
Beş dosya da aynı zaman damgasını taşıyor (20260917_162653), oysa diske yazılma saatleri 16:26 ile 16:28 arasında değişiyor. Bu, @fileDate değişkeninin döngüden önce bir kez hesaplanmasının doğrudan sonucudur ve aynı çalışmanın çıktılarını tek bakışta gruplamanızı sağlar.
Beş veritabanı da bu node’da yedeklendi, hiçbiri ATLANDI almadı. Tercih none ve öncelikler eşitken beraberliğin W25SQL25NOD1 lehine çözüldüğü anlamına gelir. Aynı anda iki node’un da yedek almadığını doğrulamak için fn_hadr_backup_is_preferred_replica sorgusunu diğer node’da da çalıştırıp sonucun 0 geldiğini görmek gerekir.
Job log’u .log uzantılı olduğu için retention’dan etkilenmez. xp_delete_file yalnızca bak uzantılı dosyaları hedefler, dolayısıyla log dosyası aynı klasörde durabilir.
Kurulum Adımları
- Kendi ortamınıza göre değiştirilecek alanlar: yedek klasörü (
@path), veritabanı listesi (WHERE name IN), saklama süresi (@retentionDays), çalışma saati (@active_start_time) ve job log yolu (@output_file_name). İlk üçü@commandstring’inin içindedir, yani çift tırnak kuralı geçerlidir; sonuncusu dışındadır ve tek tırnak alır. - Yedek klasörü önceden var olmalı ve SQL Server servis hesabının o klasöre yazma ve silme yetkisi olmalıdır. Silme yetkisi retention için gereklidir; yalnızca yazma verirseniz yedekler alınır ama eski dosyalar hiç temizlenmez.
- Script’i AG (Availability Group)’deki her node’a ayrı ayrı uygulayın. SSMS (SQL Server Management Studio)’te doğrudan node adına bağlanın, listener’a değil. Tek bir node’a kurup diğerini atlarsanız, tercih o node’u seçmediği gecelerde hiç yedek alınmaz ve failover sonrası yedekleme tamamen durur.
- Her node’da job’ı
Start Job at Stepile bir kez elle çalıştırın ve log dosyasını açıp okuyun. Kurulumun başarılı görünmesi job’ın çalışacağını garanti etmez. - Beklenen davranış: AG (Availability Group) dışı veritabanları her node’da yedeklenir, AG (Availability Group) üyesi veritabanları yalnızca tercih edilen replikada yedeklenip diğerinde
ATLANDImesajı üretir. - Job’ın sahibi
saolarak tanımlanmıştır. Kurum politikanızsahesabının devre dışı bırakılmasını gerektiriyorsa@owner_login_namedeğerini uygun bir servis hesabıyla değiştirin. - Failover sonrası ek işlem gerekmez. Rol değişiminde tercih mekanizması yeni duruma göre çalışır ve yedekleme kendiliğinden doğru node’a kayar.
Bu Script’in Kapsamadıkları
Dürüst olmak gerekirse, bu script tam bir yedekleme stratejisi değil, tam yedek katmanıdır. Üretim ortamında şunlar ayrıca ele alınmalıdır:
Log yedekleri. Full recovery model kullanan veritabanlarında log yedeği alınmazsa transaction log sınırsız büyür ve nokta bazlı geri dönüş imkanı oluşmaz. AG (Availability Group) üyesi veritabanları neredeyse her zaman full recovery model’dedir; bu bir tercih değil, AG (Availability Group)’nin gereğidir. Log yedekleri için aynı tercih kontrolünü kullanan ayrı bir job gerekir, tek farkla: log yedeklerinde COPY_ONLY kullanılmaz.
Sistem veritabanları. master, msdb ve model Availability Group’a alınamaz. Bunlar her node’da yerel olarak ve ayrıca yedeklenmelidir. Bu script’in veritabanı listesine eklenmeleri yeterlidir; AG (Availability Group) üyesi olmadıkları için replica_id IS NULL döner ve normal full backup yolundan geçerler. msdb yedeği özellikle önemlidir, çünkü job tanımlarınızın kendisi orada durur.
Differential yedekler. Secondary replikada desteklenmediği için, differential kullanmak istiyorsanız AG (Availability Group)’nin yedekleme tercihini PRIMARY yapmanız ve full yedeklerden COPY_ONLY seçeneğini kaldırmanız gerekir. Bu, secondary’de yedek alarak primary’yi rahatlatma avantajından vazgeçmek anlamına gelir.
Restore testleri. RESTORE VERIFYONLY dosya bütünlüğünü doğrular, geri dönüşün çalışacağını garanti etmez. Periyodik olarak ayrı bir sunucuya gerçek restore yapılmalı ve ardından DBCC CHECKDB çalıştırılmalıdır.
İzleme. Job’ın fail olması bir uyarı üretmelidir. Ayrıca son başarılı yedek ne zaman alındı sorusunu tüm replikaların msdb kayıtlarını birleştirerek soran bir kontrol kurulmalıdır; tek node üzerinden bakan bir izleme AG (Availability Group)’de yanlış alarm üretir.
Sonuç
Standalone bir yedekleme script’ini Always On ortamına taşırken kırılan noktalar, kodun karmaşıklığından değil, altındaki varsayımların değişmesinden kaynaklanır. En sık atlanan dört nokta şunlardır:
is_read_onlyfiltresi secondary’de AG (Availability Group) üyesi tüm veritabanlarını eler ve sonuç sessizdir: job başarılı görünür, yedek yoktur. Primary’de test ederseniz bu hatayı asla göremezsiniz.COPY_ONLYeksikliği ya differential zincirini tutarsız hale getirir ya da secondary’de yedeklemeyi tamamen engeller.- Tercih kontrolü olmadan kurulan job ya failover sonrası durur ya da her node’da gereksiz kopyalar üretir. Job’ı listener üzerinden kurmak, farkında olmadan bu hataya düşmenin en kolay yoludur.
- Retention penceresi, yedekler node’lar arasında dolaştığı için tek bir node’dan bakıldığında göründüğünden kısadır. Ortak bir UNC paylaşımı bu sorunu kökünden çözer.
Bunlara script’in yapısından gelen beşinci bir tuzak eklenir: job komutu bir string içinde saklandığı için tırnak kuralları iki katmanlıdır ve buradaki bir hata kurulumda değil, ilk çalışmada ortaya çıkar.
Bütün bu sorunların ortak özelliği, yedekleme çalışırken değil, geri dönüş anında görünür olmalarıdır. Bu yüzden AG ortamında yedekleme kurulumunun son adımı her zaman bir test restore olmalıdır.
Başka bir yazımızda görüşmek dileğiyle…