Mert Özen Her Satırda Daha İleri
Veritabanı SQL Server Derinlemesine — #22

SQL Server Agent: Job, Step, Schedule, Alert, Operator ve Database Mail; Linux ve Container'da Agent

Mert Özen 15 Kas 2026 12 dk 10 görüntülenme
SQL Server Agent: Job, Step, Schedule, Alert, Operator ve Database Mail; Linux ve Container'da Agent

Docker'daki SQL Server'da Agent varsayılan olarak kapalıdır: job tanımı msdb'ye yazılır ama sp_start_job 'SQLServerAgent is not currently running' der ve hiçbir şey çalışmaz. Idempotent bir rapor procedure'ü, kaynak kontrolünde tutulan job script'i, retry ve bildirim ayarları, job sahibi ve msdb rolleri, Linux'ta desteklenmeyen alert'ler, Database Mail XPs ve Agent job'ı ile uygulamadaki background job arasındaki sınır.

Sürüm notu: Bu yazıdaki T-SQL örnekleri SQL Server 2025 CU10 (17.0.5035.5, Enterprise Developer Edition) üzerinde, Linux container'da serinin Bookstore laboratuvar veritabanıyla çalıştırılmıştır. Bu laboratuvar sunucusunda SQL Server Agent kapalıdır; job tanımları, alert, operator ve Database Mail ayarları gerçekten oluşturulup msdb'den okunmuştur, ama job'ların çalışması, retry'ları ve e-posta gönderimi bu sunucuda çalıştırılamamıştır. Onların davranışı Microsoft Learn'deki belgelere dayanır.

Önce Tahmin Et: Job Hazır, Gece 02:30'da Ne Olur?

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) raporlama ekranı her sabah bir önceki günün kategori bazında satışlarını gösteriyor. Rakamları bir stored procedure hazırlıyor ve onu her gece 02:30'da çalışan bir SQL Server Agent job'ı çağırıyor. Job'ı tanımladım, schedule'ı bağladım, başarısız olursa ekibe e-posta gidecek şekilde operator'ü ekledim ve emin olmak için bir kez elle başlattım:

EXEC msdb.dbo.sp_start_job @job_name = N'sqls_22_nightly_sales_report';

Job bu gece çalışır mı? Hata alırsa e-posta gider mi? Docker'daki SQL Server'ın cevabı:

HResult 0x5606, Level 16, State 1
SQLServerAgent is not currently running so it cannot be notified of this action.
SELECT servicename, status_desc, startup_type_desc FROM sys.dm_server_services;
SELECT name, value_in_use FROM sys.configurations WHERE name IN (N'Agent XPs', N'Database Mail XPs');
servicename                     status_desc  startup_type_desc
------------------------------  -----------  -----------------
MSSQLSERVER                     Running      Automatic
SQL Server Agent (MSSQLSERVER)  Stopped      Automatic

name               value_in_use
-----------------  ------------
Agent XPs          0
Database Mail XPs  0

Hayır. Job'ın tanımı msdb'de duruyor, ama onu çalıştıracak süreç ayakta değil. Linux'taki SQL Server'da Agent pakete dahil ama varsayılan olarak kapalı, Docker image'ında da öyle. Bu hatayı göremeyen bir ekip, "job'ı kurduk" diye haftalarca boş bir rapor tablosuna bakabilir. Bu yazıda Agent'ın job, step, schedule, alert ve operator yapısını, Database Mail'i, Linux ve container'daki sınırları ve hangi işin Agent'a, hangisinin uygulamaya ait olduğunu inceliyoruz.

Agent'ı Açmak: Windows, Linux ve Container

Windows'ta Agent ayrı bir Windows service'tir (SQLSERVERAGENT); kurulumda başlangıç tipi seçilir ve SQL Server Configuration Manager'dan açılır. Express edition'da Agent yoktur. Linux'ta mssql-conf ile açılır ve SQL Server yeniden başlatılır; container'da ise ortam değişkeniyle:

sudo /opt/mssql/bin/mssql-conf set sqlagent.enabled true
sudo systemctl restart mssql-server

docker run -d --name sql1 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$env:MSSQL_SA_PASSWORD" -e "MSSQL_PID=Developer" -e "MSSQL_AGENT_ENABLED=true" -p 1433:1433 -v bookstore-sql-data:/var/opt/mssql mcr.microsoft.com/mssql/server:2025-latest

Agent açılınca Agent XPs ayarı da 1 olur; SSMS (SQL Server'ın masaüstü yönetim aracı) Object Explorer'da SQL Server Agent düğümünün içeriğini bu ayar açık değilse göstermez. Linux'taki Agent'ın Windows'tan farkı ise yazının ilerisinde önemli olacak: Microsoft'un SQL Server 2025 on Linux özellik sayfasına göre CmdExec, PowerShell ve SSIS (SQL Server Integration Services; Microsoft'un veri aktarım aracı) subsystem'leri ile alert'ler Linux'ta desteklenmiyor. Linux'ta job step'leri T-SQL'dir ve alert yoktur.

Job, Step ve Schedule

Önce job'ın çağıracağı procedure. İki kuralı var: idempotent (aynı gün için iki kez çalışırsa aynı sonucu üretir) ve sessizce boş geçmez (veri yoksa hata fırlatır, job başarısız olur ve biri haberdar edilir):

CREATE TABLE dbo.DailySalesReport
(
    ReportDate date NOT NULL,
    Category varchar(30) NOT NULL,
    Orders int NOT NULL,
    Items int NOT NULL,
    Revenue decimal(14,2) NOT NULL,
    CreatedAt datetime2(0) NOT NULL CONSTRAINT DF_DailySalesReport_CreatedAt DEFAULT SYSUTCDATETIME(),
    CONSTRAINT PK_DailySalesReport PRIMARY KEY (ReportDate, Category)
);
GO
CREATE OR ALTER PROCEDURE dbo.FillDailySalesReport @ReportDate date = NULL
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;
    SET @ReportDate = ISNULL(@ReportDate, DATEADD(DAY, -1, CAST(SYSUTCDATETIME() AS date)));
    IF NOT EXISTS (SELECT 1 FROM dbo.Orders WHERE CreatedAt >= @ReportDate AND CreatedAt < DATEADD(DAY, 1, @ReportDate))
        THROW 50001, 'No orders found for the report date; the order import may not have run.', 1;
    BEGIN TRANSACTION;
    DELETE FROM dbo.DailySalesReport WHERE ReportDate = @ReportDate;
    INSERT dbo.DailySalesReport (ReportDate, Category, Orders, Items, Revenue)
    SELECT @ReportDate, b.Category, COUNT(DISTINCT o.OrderId), SUM(i.Quantity), SUM(i.Quantity * i.UnitPrice)
    FROM dbo.Orders AS o
    JOIN dbo.OrderItems AS i ON i.OrderId = o.OrderId
    JOIN dbo.Books AS b ON b.BookId = i.BookId
    WHERE o.CreatedAt >= @ReportDate AND o.CreatedAt < DATEADD(DAY, 1, @ReportDate)
      AND o.Status <> 'Cancelled'
    GROUP BY b.Category;
    PRINT CONCAT('report rows for ', @ReportDate, ': ', @@ROWCOUNT);
    COMMIT;
END;

Procedure'ü job'a bağlamadan önce elle denedim. Laboratuvar verisi 20 Ekim 2026'da bittiği için parametresiz çağrı, yani "dün", hata vermeli:

EXEC dbo.FillDailySalesReport @ReportDate = '2026-10-19';
EXEC dbo.FillDailySalesReport @ReportDate = '2026-10-19';
SELECT Category, Orders, Items, Revenue FROM dbo.DailySalesReport WHERE ReportDate = '2026-10-19' ORDER BY Category;
EXEC dbo.FillDailySalesReport;
report rows for 2026-10-19: 4
report rows for 2026-10-19: 4
Category  Orders  Items  Revenue
--------  ------  -----  --------
Classic   565     1112   31131.85
Fantasy   146     192    5339.03
Horror    70      85     2554.59
SciFi     382     578    16115.50

Msg 50001, Level 16, State 1, Server sql1, Procedure dbo.FillDailySalesReport, Line 8
No orders found for the report date; the order import may not have run.

İkinci çağrı satırları çoğaltmadı, üçüncüsü boş bir rapor üretmek yerine hata verdi. Job'ın işi yalnız bu procedure'ü çağırmak; iş kuralı procedure'de, procedure de kaynak kontrolünde. Job'ın kendisi de bir script olarak kaynak kontrolünde durmalı ve tekrar çalıştırılabilir olmalı:

USE msdb;
GO
IF EXISTS (SELECT 1 FROM dbo.sysjobs WHERE name = N'sqls_22_nightly_sales_report')
    EXEC dbo.sp_delete_job @job_name = N'sqls_22_nightly_sales_report', @delete_unused_schedule = 1;
EXEC dbo.sp_add_job
    @job_name = N'sqls_22_nightly_sales_report',
    @description = N'Fills dbo.DailySalesReport for yesterday. Source: db/agent/nightly-sales-report.sql',
    @owner_login_name = N'sa';
EXEC dbo.sp_add_jobstep
    @job_name = N'sqls_22_nightly_sales_report',
    @step_name = N'Fill daily sales report',
    @subsystem = N'TSQL',
    @database_name = N'sqls_22',
    @command = N'EXEC dbo.FillDailySalesReport;',
    @retry_attempts = 2,
    @retry_interval = 5,
    @on_success_action = 1,
    @on_fail_action = 2;
EXEC dbo.sp_add_schedule
    @schedule_name = N'sqls_22_daily_0230',
    @freq_type = 4,
    @freq_interval = 1,
    @active_start_time = 023000;
EXEC dbo.sp_attach_schedule @job_name = N'sqls_22_nightly_sales_report', @schedule_name = N'sqls_22_daily_0230';
EXEC dbo.sp_add_jobserver @job_name = N'sqls_22_nightly_sales_report', @server_name = N'(local)';
IF NOT EXISTS (SELECT 1 FROM dbo.sysoperators WHERE name = N'sqls_22_bookstore_dba')
    EXEC dbo.sp_add_operator @name = N'sqls_22_bookstore_dba', @email_address = N'dba-team@example.com';
EXEC dbo.sp_update_job
    @job_name = N'sqls_22_nightly_sales_report',
    @notify_level_email = 2,
    @notify_email_operator_name = N'sqls_22_bookstore_dba';
SQLServerAgent is not currently running so it cannot be notified of this action.
SQLServerAgent is not currently running so it cannot be notified of this action.
SQLServerAgent is not currently running so it cannot be notified of this action.

Laboratuvarda server seviyesindeki nesnelerin adları sqls_22 önekiyle başlıyor; gerçek bir sunucuda job'ı "Bookstore - Nightly sales report" gibi adlandırırsın. Parçalar şöyle: job bir kapsayıcıdır ve sahibi (owner) vardır. Step, çalışacak komuttur; subsystem'i (TSQL), hangi veritabanında çalışacağı, başarı ve hatada ne yapılacağı (1 başarıyla bitir, 2 hatayla bitir, 3 sonraki step'e geç) ve retry ayarı onundur. Burada step hata alırsa beşer dakika arayla iki kez daha denenir; deadlock (1205) ya da timeout gibi geçici bir hata için faydalı, "veri yok" gibi kalıcı bir hata için yalnız on dakikalık gecikme. Schedule (freq_type = 4 her gün, active_start_time = 023000 02:30:00) ayrı bir nesnedir ve birden çok job'a bağlanabilir. sp_add_jobserver job'ı bu sunucuya hedefler; bunu unutan bir job hiç çalışmaz. Son olarak @notify_level_email = 2 yalnız hata durumunda (1 başarıda, 3 her durumda) operator'e e-posta gönderir. Ayarların hepsi msdb'de:

SELECT j.name AS job, SUSER_SNAME(j.owner_sid) AS owner, s.step_name, s.subsystem, s.retry_attempts,
       s.retry_interval, sc.name AS schedule, sc.freq_type, sc.active_start_time,
       j.notify_level_email, o.name AS operator
FROM msdb.dbo.sysjobs AS j
JOIN msdb.dbo.sysjobsteps AS s ON s.job_id = j.job_id
JOIN msdb.dbo.sysjobschedules AS js ON js.job_id = j.job_id
JOIN msdb.dbo.sysschedules AS sc ON sc.schedule_id = js.schedule_id
LEFT JOIN msdb.dbo.sysoperators AS o ON o.id = j.notify_email_operator_id
WHERE j.name = N'sqls_22_nightly_sales_report';
job                           owner  step_name                subsystem  retry_attempts  retry_interval  schedule            freq_type  active_start_time  notify_level_email  operator
----------------------------  -----  -----------------------  ---------  --------------  --------------  ------------------  ---------  -----------------  ------------------  ---------------------
sqls_22_nightly_sales_report  sa     Fill daily sales report  TSQL       2               5               sqls_22_daily_0230  4          23000              2                   sqls_22_bookstore_dba

active_start_time'ın 23000 görünmesi bir hata değil: saat HHMMSS biçiminde bir int olarak tutuluyor ve baştaki sıfır düşüyor. Agent çalışan bir sunucuda job'ın geçmişi msdb.dbo.sysjobhistory'dedir; tarih ve saat de aynı int biçiminde tutulduğu için msdb.dbo.agent_datetime ile okunur. run_status değerleri 0 başarısız, 1 başarılı, 2 retry, 3 iptal ve 4 devam ediyor; step_id = 0 satırı job'ın genel sonucudur:

SELECT TOP (20) j.name, h.step_id, h.step_name, h.run_status,
       msdb.dbo.agent_datetime(h.run_date, h.run_time) AS run_at, h.run_duration, h.retries_attempted, h.message
FROM msdb.dbo.sysjobhistory AS h
JOIN msdb.dbo.sysjobs AS j ON j.job_id = h.job_id
WHERE j.name = N'sqls_22_nightly_sales_report'
ORDER BY h.instance_id DESC;

Geçmiş sınırsız tutulmaz: Windows'ta varsayılan olarak toplam 1.000, job başına 100 satır saklanır ve sık çalışan bir job dün gecenin başarısız kaydını bir saat içinde silebilir. Gerektiğinde bu sınırı artır ya da sp_purge_jobhistory ile tarih bazlı temizliğe geç.

Yetki: Job Sahibi, Proxy ve msdb Rolleri

Bir T-SQL step'i kimin yetkisiyle çalışır? Job sahibi sysadmin ise Agent'ın kendi hesabıyla, değilse sahibin login'i adına (EXECUTE AS LOGIN) çalışır. Bu yüzden job sahibi önemlidir. En sık görülen hata, job'ı oluşturan kişinin kendi login'inin sahip olarak kalmasıdır: kişi şirketten ayrılıp login'i silindiğinde ya da devre dışı bırakıldığında job'lar çalışmayı bırakır ve bunu genellikle ilk başarısız gecede öğrenirsin. Sahip olarak sa ya da kişiye bağlı olmayan, yalnız bu iş için açılmış bir login kullan. Proxy'ler yalnız T-SQL dışındaki subsystem'ler (CmdExec, PowerShell, SSIS) içindir ve step'i belirli bir credential'la çalıştırır; Linux'ta bu subsystem'ler olmadığı için proxy de yoktur. Job oluşturma ve yönetme yetkisini sysadmin vermeden dağıtmak için msdb'deki üç rol var: SQLAgentUserRole (kendi job'larını yönetir), SQLAgentReaderRole (bütün job'ları ve geçmişi görür) ve SQLAgentOperatorRole (başka job'ları başlatabilir, durdurabilir). Login, user ve role ayrımını serinin 17. yazısında gördük.

Alert, Operator ve Database Mail

Operator bir isim ve e-posta adresidir. Alert ise belirli bir hata numarası, severity ya da performance condition gerçekleşince operator'e haber veren kuraldır. Klasik örnek, 21. yazıda gördüğümüz 824 hatası ve severity 19–25 arasındaki ciddi hatalardır:

EXEC msdb.dbo.sp_add_alert
    @name = N'sqls_22_error_824',
    @message_id = 824,
    @delay_between_responses = 600,
    @include_event_description_in = 1;
EXEC msdb.dbo.sp_add_notification
    @alert_name = N'sqls_22_error_824',
    @operator_name = N'sqls_22_bookstore_dba',
    @notification_method = 1;
SELECT name, message_id, severity, enabled, occurrence_count FROM msdb.dbo.sysalerts WHERE name LIKE N'sqls[_]22%';
name               message_id  severity  enabled  occurrence_count
-----------------  ----------  --------  -------  ----------------
sqls_22_error_824  824         0         1        0

Alert oluştu, ama bu bir Linux sunucusu ve Linux'ta alert desteklenmiyor. SQL Server sana bunu söylemiyor; tanım msdb'de duruyor ve hiçbir zaman tetiklenmeyecek. Windows'ta alert'ler Windows uygulama event log'unu izleyerek çalışır ve yalnız log'a yazılan hatalar için tetiklenir. Linux'ta aynı ihtiyacı, hata log'unu ya da bir Extended Events (18. yazı) oturumunu okuyan bir T-SQL job'ı veya sunucunun dışındaki bir izleme aracı karşılar.

E-postayı gönderen Database Mail'dir. Hesap (SMTP sunucusu) ve profil msdb'de tanımlanır:

EXEC msdb.dbo.sysmail_add_account_sp
    @account_name = N'sqls_22_bookstore_alerts',
    @email_address = N'sql-alerts@example.com',
    @display_name = N'Bookstore SQL Server',
    @mailserver_name = N'smtp.example.com',
    @port = 587,
    @enable_ssl = 1;
EXEC msdb.dbo.sysmail_add_profile_sp @profile_name = N'sqls_22_bookstore';
EXEC msdb.dbo.sysmail_add_profileaccount_sp
    @profile_name = N'sqls_22_bookstore',
    @account_name = N'sqls_22_bookstore_alerts',
    @sequence_number = 1;
EXEC msdb.dbo.sp_send_dbmail
    @profile_name = N'sqls_22_bookstore',
    @recipients = N'dba-team@example.com',
    @subject = N'Database Mail test',
    @body = N'Test message from the Bookstore SQL Server.';
HResult 0x3BB1, Level 16, State 1
SQL Server blocked access to procedure 'dbo.sp_send_dbmail' of component 'Database Mail XPs' because this component is turned off as part of the security configuration for this server. A system administrator can enable the use of 'Database Mail XPs' by using sp_configure. For more information about enabling 'Database Mail XPs', search for 'Database Mail XPs' in SQL Server Books Online.

Hesap ve profil oluştu, gönderim engellendi. Database Mail varsayılan olarak kapalıdır ve server seviyesinde sp_configure 'Database Mail XPs', 1 ile açılır; laboratuvar sunucusu paylaşıldığı için bu ayarı değiştirmedim. Bir ayrıntı daha: Agent'ın job bildirimlerini hangi profille göndereceği ayrıca söylenmelidir. Windows'ta bu SQL Server Agent özelliklerindeki Alert System sayfasıdır; Linux'ta mssql-conf set sqlagent.databasemailprofile, container'da MSSQL_AGENT_EMAIL_PROFILE ortam değişkeni. Bunu atlayan bir sunucuda job başarısız olur, bildirim adımı da sessizce başarısız olur ve hatayı Agent'ın kendi log'unda bulursun. Kurulumdan sonra bilerek başarısız olan bir job'la (yukarıdaki procedure'ü veri olmayan bir günle çağırmak yeterli) e-postanın gerçekten geldiğini test et.

Agent Job'ı mı, Uygulamadaki Background Job mı?

Background Job'lar serisinin ilk yazısı "Request Dışında İş Yapmak" uygulama tarafındaki seçenekleri (BackgroundService, Hangfire, Quartz.NET) anlatıyor. Hangisinin nereye ait olduğunu şu ayrım belirliyor:

SoruSQL Server Agent jobUygulamadaki background job
Ne içinVeritabanının kendi bakımı: backup, index, istatistik, CHECKDB, history temizliğiİş kuralı: e-posta, entegrasyon, sipariş durumu, rapor üretimi
Kod neredemsdb'de; kaynak kontrolü için ayrıca script tutmak gerekirUygulamanın repo'sunda, testleri ve code review'u ile
Dış sistemlerLinux'ta yalnız T-SQL; HTTP, dosya, kuyruk için uygun değil.NET'in bütün kütüphaneleri
Ölçeklenme ve HA (yüksek erişilebilirlik)Sunucuya bağlı; klasik availability group'ta job'lar her replica'da ayrı tanımlanırUygulama instance'larıyla birlikte
Azure SQL DatabaseAgent yok; elastic jobs ya da Azure SQL Managed InstanceAynen çalışır

Bookstore'un rapor job'ı sınırda bir örnek: iş bütünüyle veritabanının içinde ve veri tek bir procedure'le hazırlanıyor, bu yüzden Agent'ta kalabilir. Rapor ileride PDF olarak üretilip müşterilere e-postayla gidecekse, o iş uygulamaya taşınmalı. Azure SQL Database'de Agent yoktur; veritabanları arasında T-SQL job'ı çalıştırmak için elastic jobs, Agent'ın tamamı için Azure SQL Managed Instance kullanılır.

SQL Server 2019/2022'de farkı: job, step, schedule ve operator yapısı yıllardır aynı. SQL Server 2022'nin contained availability group'ları kendi msdb'lerini taşıdığı için job'lar failover'da replica'yla birlikte gider; klasik availability group'ta gitmez. SQL Server 2025, Agent'ın sunucuya bağlantısında TDS (SQL Server'ın istemci-sunucu protokolü) 8.0 ve TLS (ağ trafiğini şifreleyen protokol) 1.3 ile strict encryption desteği getirdi.

Üretimde Dikkat

Agent'ın çalıştığını izle. Servisin durumu (sys.dm_server_services) ve her job'ın son başarılı çalışma zamanı bir izleme sorgusunun parçası olmalı; "job hiç başarısız olmadı" ile "job hiç çalışmadı" arasındaki fark bildirimden anlaşılmaz.

Job'ları script olarak kaynak kontrolünde tut ve deploy'la birlikte uygula. Yukarıdaki script iki kez çalıştırılınca aynı job'ı üretir; SSMS'te elle değiştirilen bir job ise bir sonraki sunucu taşımasında kaybolur.

Availability group kullanıyorsan job'lar her replica'da ayrı tanımlanır ve job'ın ilk adımı o replica'nın primary olup olmadığını kontrol etmelidir; yoksa failover'dan sonra rapor iki kez ya da hiç üretilmez.

Sık Düşülen Tuzaklar

Container'da Agent'ın açık olduğunu varsaymak. Job tanımı oluşur, schedule bağlanır, hiçbir şey çalışmaz.

İş mantığını Agent job'larının step'lerine dağıtmak. Kaynak kontrolünün, testlerin ve code review'un dışında kalan kod, kimsenin değiştirmeye cesaret edemediği koda dönüşür. Step yalnız bir procedure çağırmalı.

Job sahibini kişisel bir login yapmak. Kişi gidince job'lar da durur.

Bildirimi yalnız tanımlamak. Database Mail XPs kapalıysa ya da Agent'a profil verilmemişse hiçbir e-posta gitmez; bunu ilk gerçek hatada öğrenmemek için bilerek başarısız olan bir job'la test et.

Linux'ta alert'lere güvenmek. Tanım oluşur ama tetiklenmez.

Küçük Bir Deneme

★☆☆ Kendi container'ını MSSQL_AGENT_ENABLED=true ile başlat, bu yazıdaki procedure'ü ve job script'ini çalıştır, job'ı sp_start_job ile başlat ve sysjobhistory'de step_id 0 ve 1 satırlarını oku.

★★☆ Job'ı veri olmayan bir tarihle çalışacak şekilde değiştir. Retry'lar sysjobhistory'de nasıl görünüyor? retries_attempted ve run_status değerleri ne?

★★★ Kendi test ortamında bir SMTP sunucusu (örneğin bir container'da çalışan bir test SMTP sunucusu) kur, Database Mail'i açıp Agent'a profil ver ve başarısız job'ın e-postasının geldiğini doğrula. msdb.dbo.sysmail_allitems ve sysmail_event_log ne gösteriyor?

Gece job'larımızın bir kısmı veri taşıyor: yayınevlerinden gelen fiyat listeleri, geliştirme ortamına kopyalanan veritabanları. Bir sonraki yazıda BULK INSERT, bcp, OPENROWSET ve BACPAC (şemayı ve veriyi birlikte taşıyan dışa aktarım dosyası) ile veri taşımayı ölçerek inceleyeceğiz.