Cubes passing one by one through a gate next to the same cubes grouped into one block, representing row-by-row cursors versus set-based SQL

SQL Server Cursor Kullanımı: Sözdizimi, Seçenekler, Performans ve Set Tabanlı Alternatifler

SQL Server cursor’ları nasıl çalışır, hangi seçenekler kullanılmalı, neden yavaştır, set tabanlı sorgulara dönüştürülmüş dört cursor kalıbı, oturumları güvenle sonlandırma ve döngünün doğru araç olduğu durumlar.

Temmuz 18, 2023

Cursor, T-SQL’in bir sonuç kümesini programlama dillerinin bir koleksiyon üzerinde döndüğü gibi satır satır işlemesini sağlar. Geliştiricilerin ona yönelmesinin nedeni de, pek çok yavaş batch job’ın arkasında çıkmasının nedeni de budur: SQL Server kümeler üzerinde çalışmak için tasarlanmıştır; cursor ise bir milyon satır üzerindeki tek bir ifadeyi bir milyon ayrı ifadeye dönüştürür.

Cursor yasak değildir. Veritabanları üzerinde dönen yönetim script’leri ya da gerçekten satır satır çalışması gereken mantık için en basit doğru araçtır. Bu rehberde cursor sözdizimini ve önemli seçenekleri, cursor’ların neden yavaş olduğunu, set tabanlı sorgulara dönüştürülmüş dört yaygın cursor kalıbını, bir veritabanındaki tüm oturumları sonlandırmanın doğru yolunu ve gerçekten gerektiğinde cursor’ın nasıl doğru yazılacağını anlatıyoruz.

SQL Server Performans Danışmanlığı

Saatler süren batch job'larınızı dakikalara indirelim

Aryasoft, cursor ve satır satır çalışan T-SQL kodunu set tabanlı sorgulara dönüştürerek gece job'larının ve raporların süresini kısaltır.

Destek Alın

Temel Cursor Sözdizimi

Her cursor aynı beş adımı izler: bir sorguyla tanımlanır, açılır, @@FETCH_STATUS 0 olmaktan çıkana kadar döngüde satırlar okunur, kapatılır ve bellekten kaldırılır.

DECLARE @AccountID int;

DECLARE account_cursor CURSOR LOCAL FAST_FORWARD FOR
    SELECT AccountID
    FROM dbo.Accounts
    WHERE IsActive = 1;

OPEN account_cursor;
FETCH NEXT FROM account_cursor INTO @AccountID;

WHILE @@FETCH_STATUS = 0
BEGIN
    EXEC dbo.RecalculateAccount @AccountID = @AccountID;  -- satır başına yapılan iş
    FETCH NEXT FROM account_cursor INTO @AccountID;
END;

CLOSE account_cursor;
DEALLOCATE account_cursor;

Önemli Cursor Seçenekleri

Seçenek belirtilmeden tanımlanan bir cursor neredeyse kimseye uymayan varsayılanları kullanır: global’dir (veritabanının CURSOR_DEFAULT seçeneği LOCAL değilse) ve dinamiktir; yani her fetch’te alttaki veriyi yeniden kontrol eder. Aşağıdaki seçenekler bunu değiştirir.

Seçenek Ne yapar Ne zaman kullanılır
LOCAL Kapsamı batch, procedure veya trigger ile sınırlar Neredeyse her zaman; isim çakışmalarını ve sızıntıları önler
GLOBAL Deallocate edilene kadar tüm bağlantıya görünür Nadiren; yalnızca başka bir batch’in kullanması gerekiyorsa
FAST_FORWARD Yalnızca ileri, salt okunur ve performans için optimize Basit döngüler için varsayılan tercih
STATIC Açılışta sonucu tempdb’ye kopyalar; sonradan yapılan değişiklikleri görmez Döngü okuduğu tabloyu değiştiriyorsa
KEYSET Key’leri tempdb’de tutar; key dışı kolonlardaki güncellemeleri görür, yeni satırları görmez Nadiren
DYNAMIC Gezinirken tüm değişiklikleri görür; en pahalısıdır Nadiren
READ_ONLY / SCROLL_LOCKS / OPTIMISTIC Satırların cursor üzerinden güncellenip güncellenemeyeceği ve nasıl WHERE CURRENT OF kullanmıyorsanız READ_ONLY

İpucu

Varsayılan olarak CURSOR LOCAL FAST_FORWARD yazın. Yalnızca döngü okuduğu veriyi değiştiriyorsa ve sabit bir anlık görüntüye ihtiyacınız varsa LOCAL STATIC READ_ONLY’ye geçin.

Cursor’lar Neden Yavaştır?

  • Satır başına bir çalıştırma. Her FETCH ve döngü içindeki her ifade, tüm satırları işleyen tek bir plan yerine kendi yüküyle ayrı ayrı çalışır.
  • Set tabanlı optimizasyon yok. Join’ler, hash aggregate’ler, batch mode ve paralellik, her seferinde tek satıra dokunan bir döngüye yardımcı olamaz.
  • İfade başına loglama. Autocommit modunda bir milyon tek satırlık insert, her biri diski bekleyen bir milyon log flush’ı demektir.
  • Daha uzun kilit ve transaction’lar. Döngü tek bir transaction içinde çalışıyorsa kilitler boyunca birikir ve diğer oturumları bloklar.
  • İlk fetch’ten önce veriyi kopyalayan STATIC ve KEYSET cursor’lar için tempdb işi.

Fark yüzde birkaç değildir. Bir cursor’ı tek bir ifade olarak yeniden yazmak, dakikaları ya da saatleri sıklıkla saniyelere indirir.

Kalıp 1: Satır Satır Insert

Bir tablodan satır okuyup başka bir tabloya ekleyen cursor en yaygın kalıptır ve değiştirmesi en kolay olandır:

-- Cursor versiyonu: her satır için bir INSERT
DECLARE @ID int, @First varchar(50), @Last varchar(50);
DECLARE c CURSOR LOCAL FAST_FORWARD FOR
    SELECT EmployeeID, FirstName, LastName FROM dbo.Employee;
OPEN c;
FETCH NEXT FROM c INTO @ID, @First, @Last;
WHILE @@FETCH_STATUS = 0
BEGIN
    INSERT INTO dbo.EmployeeArchive (EmployeeID, FullName)
    VALUES (@ID, @First + ' ' + @Last);
    FETCH NEXT FROM c INTO @ID, @First, @Last;
END;
CLOSE c; DEALLOCATE c;

-- Set tabanlı versiyon: tek ifade
INSERT INTO dbo.EmployeeArchive (EmployeeID, FullName)
SELECT EmployeeID, CONCAT(FirstName, ' ', LastName)
FROM dbo.Employee;

Kalıp 2: Her Satırı Başka Bir Tablodan Güncellemek

-- Siparişler üzerinde dönüp müşterinin indirim oranını aramak yerine:
UPDATE o
SET    o.DiscountRate = c.DiscountRate
FROM   dbo.Orders    AS o
JOIN   dbo.Customers AS c ON c.CustomerID = o.CustomerID
WHERE  o.OrderDate >= '20260101';

Kalıp 3: Kümülatif Toplam (Running Total)

Kümülatif toplam, window fonksiyonlarından önce cursor kullanmanın klasik nedenlerinden biriydi. SQL Server 2012’den bu yana tek bir sorgudur:

SELECT
    AccountID,
    TransactionDate,
    Amount,
    SUM(Amount) OVER (PARTITION BY AccountID
                      ORDER BY TransactionDate, TransactionID
                      ROWS UNBOUNDED PRECEDING) AS RunningBalance
FROM dbo.Transactions;

ROWS UNBOUNDED PRECEDING‘i açıkça belirtin. Belirtilmezse varsayılan çerçeve RANGE‘dir; daha yavaştır ve aynı ORDER BY değerine sahip satırları tek grup gibi ele alır.

Kalıp 4: Metni Bölmek ve Kelime Saymak

Metni karakter karakter ya da kelime kelime bir döngüde ayrıştırmak cursor’ların bir başka favorisidir. STRING_SPLIT ile (SQL Server 2016 ve sonrası, compatibility level 130 veya üzeri) gruplanmış bir sorgudur:

-- Tüm satırlarda en sık geçen üç kelime
SELECT TOP (3)
    LOWER(s.value) AS word,
    COUNT(*)       AS occurrences
FROM dbo.Notes AS n
CROSS APPLY STRING_SPLIT(n.NoteText, ' ') AS s
WHERE LEN(s.value) > 2
GROUP BY LOWER(s.value)
ORDER BY occurrences DESC;

Yönetim Döngüleri: Komutları Üretin

DBA script’leri çoğu zaman tablolar ya da veritabanları üzerinde dönerek her biri için bir komut çalıştırır. Komutları tek bir sorguyla üretmek, gözden geçirmek ve sonra çalıştırmak genellikle daha nettir:

-- "tmp_" ile başlayan ve bir aydan eski tablolar
SELECT STRING_AGG(
           CONVERT(nvarchar(max), N'DROP TABLE ' + QUOTENAME(s.name) + N'.' + QUOTENAME(t.name) + N';'),
           CHAR(13) + CHAR(10)) AS drop_script
FROM sys.tables  AS t
JOIN sys.schemas AS s ON s.schema_id = t.schema_id
WHERE t.name LIKE N'tmp[_]%'
  AND t.create_date < DATEADD(MONTH, -1, SYSDATETIME());

Uyarı

LIKE içinde alt çizgi, herhangi tek bir karakter anlamına gelen bir joker karakterdir. ‘b_%’ gibi bir kalıp yalnızca “b_” ile başlayanları değil, b ile başlayan tüm tabloları eşleştirir. Bunu ‘b[_]%’ şeklinde kaçırın ve üretilen bir DROP script’ini çalıştırmadan önce mutlaka gözden geçirin.

Önce script üretmek, neyin değiştirildiğine dair bir kayıt da sağlar. Kimsenin silmeye cesaret edemediği eski tablolar teknik olduğu kadar bir yönetişim sorunudur; veri yönetişimi nedir yazımıza göz atabilirsiniz.

Gerçekten bir döngü gerektiğinde, örneğin her veritabanında sırayla DBCC CHECKDB çalıştırmak için, sys.databases üzerinde bir cursor tamamen uygundur. Tekrar sayısı küçüktür ve her tekrar büyük, set tabanlı bir işlemdir.

Örnek: Tüm Veritabanlarında Son Başarılı CHECKDB Tarihi

Her veritabanının son başarılı DBCC CHECKDB tarihi, o veritabanının boot page’inde dbi_dbccLastKnownGood alanında tutulur ve DBCC DBINFO ile okunabilir. Komut veritabanı bağlamında çalıştığı için veritabanları üzerinde tek tek dönmek gerekir; bu, döngünün gerçekten anlamlı olduğu tipik bir DBA işidir.

Aşağıda aynı işi önce cursor, sonra WHILE döngüsüyle yapıyoruz. İki script de belirlediğiniz gün sayısından eski ya da hiç çalışmamış CHECKDB’leri işaretler.

1. Cursor ile

SET NOCOUNT ON;

DECLARE @ThresholdDay int = 7;   -- bu kadar günden eski CHECKDB "gecikmiş" sayılır
DECLARE @DatabaseName sysname,
        @sql nvarchar(max);

IF OBJECT_ID(N'tempdb..#DBInfo') IS NOT NULL DROP TABLE #DBInfo;
CREATE TABLE #DBInfo (ParentObject nvarchar(255), [Object] nvarchar(255),
                      Field nvarchar(255), [Value] nvarchar(4000));

IF OBJECT_ID(N'tempdb..#Result') IS NOT NULL DROP TABLE #Result;
CREATE TABLE #Result (DatabaseName sysname, LastGoodCheckDb datetime);

DECLARE db_cursor CURSOR LOCAL FAST_FORWARD FOR
    SELECT name
    FROM sys.databases
    WHERE state_desc = N'ONLINE'
      AND is_read_only = 0
      AND database_id > 4          -- sistem veritabanları hariç
    ORDER BY name;

OPEN db_cursor;
FETCH NEXT FROM db_cursor INTO @DatabaseName;

WHILE @@FETCH_STATUS = 0
BEGIN
    SET @sql = N'USE ' + QUOTENAME(@DatabaseName) + N'; DBCC DBINFO() WITH TABLERESULTS;';
    INSERT INTO #DBInfo EXEC sys.sp_executesql @sql;

    INSERT INTO #Result (DatabaseName, LastGoodCheckDb)
    SELECT TOP (1) @DatabaseName, CONVERT(datetime, [Value])
    FROM #DBInfo
    WHERE Field = N'dbi_dbccLastKnownGood';

    TRUNCATE TABLE #DBInfo;
    FETCH NEXT FROM db_cursor INTO @DatabaseName;
END

CLOSE db_cursor;
DEALLOCATE db_cursor;

SELECT DatabaseName,
       LastGoodCheckDb,
       CASE WHEN LastGoodCheckDb < '19000102' THEN N'Hiç çalışmamış'
            WHEN LastGoodCheckDb < DATEADD(DAY, -@ThresholdDay, GETDATE()) THEN N'Gecikmiş'
            ELSE N'OK' END AS CheckDbStatus
FROM #Result
ORDER BY LastGoodCheckDb;

DROP TABLE #DBInfo;
DROP TABLE #Result;

Veritabanı adı QUOTENAME ile köşeli paranteze alınır; böylece adında boşluk, tire ya da ] karakteri olan veritabanları da script’i bozmaz. Yalnızca DBCC DBINFO veritabanı bağlamına ihtiyaç duyduğu için dinamik SQL’e sadece o satır girer; sonuçların işlenmesi normal T-SQL olarak kalır. CHECKDB hiç çalışmamış bir veritabanında alan 1900-01-01 döner; script bunu ayrıca işaretler.

2. WHILE Döngüsü ve Kuyruk Tablosu ile

SET NOCOUNT ON;

DECLARE @ThresholdDay int = 7;
DECLARE @DatabaseName sysname,
        @sql nvarchar(max);

IF OBJECT_ID(N'tempdb..#DBInfo') IS NOT NULL DROP TABLE #DBInfo;
CREATE TABLE #DBInfo (ParentObject nvarchar(255), [Object] nvarchar(255),
                      Field nvarchar(255), [Value] nvarchar(4000));

IF OBJECT_ID(N'tempdb..#Result') IS NOT NULL DROP TABLE #Result;
CREATE TABLE #Result (DatabaseName sysname, LastGoodCheckDb datetime);

-- Kuyruk: 0 = bekliyor, 1 = tamamlandı, 2 = hata
IF OBJECT_ID(N'tempdb..#Queue') IS NOT NULL DROP TABLE #Queue;
SELECT name AS DatabaseName,
       CAST(0 AS tinyint) AS QueueStatus,
       CAST(NULL AS nvarchar(4000)) AS ErrorMessage
INTO #Queue
FROM sys.databases
WHERE state_desc = N'ONLINE'
  AND is_read_only = 0
  AND database_id > 4;

WHILE EXISTS (SELECT 1 FROM #Queue WHERE QueueStatus = 0)
BEGIN
    SELECT TOP (1) @DatabaseName = DatabaseName
    FROM #Queue
    WHERE QueueStatus = 0
    ORDER BY DatabaseName;

    BEGIN TRY
        SET @sql = N'USE ' + QUOTENAME(@DatabaseName) + N'; DBCC DBINFO() WITH TABLERESULTS;';
        INSERT INTO #DBInfo EXEC sys.sp_executesql @sql;

        INSERT INTO #Result (DatabaseName, LastGoodCheckDb)
        SELECT TOP (1) @DatabaseName, CONVERT(datetime, [Value])
        FROM #DBInfo
        WHERE Field = N'dbi_dbccLastKnownGood';

        UPDATE #Queue SET QueueStatus = 1 WHERE DatabaseName = @DatabaseName;
    END TRY
    BEGIN CATCH
        -- Hata veren veritabanını işaretle ve sıradakine geç
        UPDATE #Queue
        SET QueueStatus = 2, ErrorMessage = ERROR_MESSAGE()
        WHERE DatabaseName = @DatabaseName;
    END CATCH;

    TRUNCATE TABLE #DBInfo;
END

SELECT DatabaseName,
       LastGoodCheckDb,
       CASE WHEN LastGoodCheckDb < '19000102' THEN N'Hiç çalışmamış'
            WHEN LastGoodCheckDb < DATEADD(DAY, -@ThresholdDay, GETDATE()) THEN N'Gecikmiş'
            ELSE N'OK' END AS CheckDbStatus
FROM #Result
ORDER BY LastGoodCheckDb;

-- Okunamayan veritabanları ve nedenleri
SELECT DatabaseName, ErrorMessage FROM #Queue WHERE QueueStatus = 2;

DROP TABLE #Queue;
DROP TABLE #DBInfo;
DROP TABLE #Result;

Hangisi Daha Esnek?

İki script de aynı sonucu üretir; fark, döngünün durumunun nerede tutulduğundadır.

Cursor’da durum cursor’ın kendisindedir: liste OPEN anında sorgudan gelir, FETCH sırayla ilerler ve döngü bittiğinde geride bir iz kalmaz. Kısa, okunaklı, tek seferlik bir tarama için en az kodla işi görür; LOCAL FAST_FORWARD ile ek yükü de düşüktür.

WHILE sürümünde durum bir tablodadır ve bu tablo üzerinde istediğiniz her şeyi yapabilirsiniz:

  • Hata yönetimi: Bir veritabanı hata verdiğinde satırı “hata” olarak işaretleyip mesajını saklar, döngüye sıradaki veritabanından devam edersiniz. Sonunda hangi veritabanlarının neden atlandığını tek sorguyla görürsünüz.
  • Kaldığı yerden devam: Kuyruğu geçici tablo yerine kalıcı bir tabloda tutarsanız, yarıda kesilen bir iş yeniden başlatıldığında yalnızca bekleyen veritabanlarını işler. Gece boyunca süren CHECKDB ya da index bakımı job’larında bu büyük fark yaratır.
  • Sıra ve öncelik: ORDER BY’ı değiştirerek önce kritik ya da küçük veritabanlarını işleyebilir, tabloya bir öncelik kolonu ekleyebilirsiniz.
  • Ölçüm ve kayıt: Başlangıç ve bitiş zamanı, deneme sayısı gibi kolonlar ekleyerek her adımın ne kadar sürdüğünü kayıt altına alabilirsiniz.
  • İşi bölmek: Kalıcı bir kuyruk tablosunda satırları UPDATE ... OUTPUT ile “sahiplenerek” işi birden fazla oturuma ya da job’a dağıtabilirsiniz.

Bu esnekliğin bir bedeli vardır: döngünün ilerlemesi tamamen sizin elinizdedir. Bir satır hiçbir koşulda işaretlenmezse, örneğin hata durumunda UPDATE atlanırsa, WHILE EXISTS koşulu hiç sağlanmaz ve döngü sonsuza kadar aynı veritabanında döner. Bu yüzden yukarıdaki script satırı hem TRY hem CATCH bloğunda işaretler.

Pratik kural: tek seferlik, basit taramalar için LOCAL FAST_FORWARD bir cursor yeterlidir. Hata toleransı, yeniden başlatılabilirlik ya da kayıt gereken bakım işlerinde kuyruk tablosu ve WHILE döngüsü daha doğru seçimdir.

Bu Örnekte Döngüye Gerek Olmayabilir

SQL Server 2016 SP2, SQL Server 2017 CU9 ve sonrasında son başarılı CHECKDB zamanı DATABASEPROPERTYEX ile doğrudan okunabilir. Bu durumda iş, bu rehberin ana fikrine uygun şekilde tek bir set tabanlı sorguya iner:

SELECT name AS DatabaseName,
       DATABASEPROPERTYEX(name, 'LastGoodCheckDbTime') AS LastGoodCheckDb
FROM sys.databases
WHERE state_desc = N'ONLINE'
  AND database_id > 4
ORDER BY LastGoodCheckDb;

Döngülü yöntemler; eski sürümlerde, DBCC DBINFO‘nun döndürdüğü başka alanlara da ihtiyaç duyduğunuzda ya da her veritabanında gerçekten bir komut çalıştırmanız gerektiğinde işe yarar. DBCC DBINFO‘nun belgelenmemiş bir komut olduğunu ve çıktı biçiminin sürümler arasında değişebileceğini de unutmayın.

Bir Veritabanındaki Tüm Oturumları Sonlandırmak

Popüler bir cursor script’i sp_who çıktısını geçici bir tabloya okur ve bir veritabanındaki her oturum için KILL çalıştırır. Bunun için cursor’a gerek yoktur. Bir veritabanını özel kullanıma almak için işi SQL Server’a bırakın:

-- Herkesin bağlantısını kes ve açık transaction'larını geri al
ALTER DATABASE SalesDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
-- ... bakım ...
ALTER DATABASE SalesDB SET MULTI_USER;

Yalnızca oturumları kesip veritabanını kullanılabilir tutmanız gerekiyorsa, KILL komutlarını oturum DMV’sinden üretin:

DECLARE @sql nvarchar(max);

SELECT @sql = STRING_AGG(CONVERT(nvarchar(max), N'KILL ' + CONVERT(nvarchar(10), session_id) + N';'), N' ')
FROM sys.dm_exec_sessions
WHERE database_id = DB_ID(N'SalesDB')
  AND session_id <> @@SPID
  AND is_user_process = 1;

PRINT @sql;           -- önce gözden geçirin
-- EXEC sys.sp_executesql @sql;

Kritik

KILL bir transaction’ı yok etmez, geri alır; geri alma da yaptığı işi geri almak kadar, bazen daha uzun sürebilir. Bir olay sırasında uzun transaction’ları kill etmek, oturumların KILLED/ROLLBACK durumunda takılı kalmasına ve kurtarmanın uzamasına sıkça yol açar. Kill etmeden önce oturumun ne yaptığını kontrol edin; bunu kesinti runbook’umuzda ele alıyoruz.

Uygun Döngüler: Büyük Değişiklikleri Parçalara Bölmek

Her döngü bir cursor sorunu değildir. On milyonlarca satırı tek ifadeyle silmek ya da güncellemek transaction log’u doldurabilir, tabloyu uzun süre bloklayabilir ve iptal edilirse geri alınması saatler sürebilir. Değişikliği parçalar halinde işleyen bir döngü burada doğru araçtır:

DECLARE @rows int = 1;

WHILE @rows > 0
BEGIN
    DELETE TOP (5000)
    FROM dbo.AuditLog
    WHERE LoggedAt < DATEADD(YEAR, -2, SYSDATETIME());

    SET @rows = @@ROWCOUNT;
END;

Her parça kısa, set tabanlı bir transaction’dır. Kilitler parçalar arasında bırakılır, log backup’lardan sonra log yeniden kullanılabilir ve job’ı durdurmak yalnızca o anki parçayı geri alır. Filtre kolonundaki bir index, her parçanın tabloyu taramasını önler.

Cursor Kullanmanız Gerekiyorsa

  • LOCAL FAST_FORWARD olarak tanımlayın (döngü kaynağı değiştiriyorsa LOCAL STATIC READ_ONLY).
  • Cursor’ın sonuç kümesini küçük tutun; filtrelemeyi döngü içinde değil, cursor sorgusunda yapın.
  • Atomik olması gerekmiyorsa döngünün tamamını tek bir transaction’a sarmayın.
  • CATCH bloğu dahil her zaman CLOSE ve DEALLOCATE yapın.
  • İç içe cursor’lardan kaçının; neredeyse her zaman kılık değiştirmiş bir join’dir.
  • Ölçün: döngünün gövdesi tek bir ifadeyse, büyük ihtimalle tüm döngü de tek bir ifade olabilir.
Cursor, T-SQL’in bir sorgu sonucunun satırlarını DECLARE, OPEN, FETCH, CLOSE ve DEALLOCATE ile tek tek işlemesini sağlayan bir veritabanı nesnesidir.
Çünkü her satır, tüm küme üzerinde çalışan tek bir plan yerine ayrı ifadelerle işlenir. Bu, verimli join’ler, batch mode ve paralellik gibi set tabanlı optimizasyonları devre dışı bırakır ve ifade başına loglama ve kilitleme yükü ekler.
Genellikle tek bir INSERT … SELECT, JOIN’li UPDATE, MERGE, kümülatif toplamlar için SUM() OVER gibi window fonksiyonları ya da metin işleme için STRING_SPLIT ve STRING_AGG. Çok büyük değişikliklerde, tek dev bir ifade yerine parçalar halinde çalışan bir WHILE döngüsü daha iyidir.
Çoğu döngü için LOCAL FAST_FORWARD. Döngü cursor’ın okuduğu tabloyu değiştiriyorsa, cursor’ın sabit bir kopya üzerinde çalışması için LOCAL STATIC READ_ONLY kullanın.
Özel erişim almak için ALTER DATABASE … SET SINGLE_USER WITH ROLLBACK IMMEDIATE kullanın ya da KILL komutlarını database_id’ye göre filtrelenmiş sys.dm_exec_sessions’tan üretin. Kill edilen transaction’ların geri alındığını ve bunun uzun sürebileceğini unutmayın.

SQL Server Performans Danışmanlığı

En yavaş job’ınızın arkasında bir cursor mu var?

Aryasoft’un kıdemli DBA ekibi uzun süren job ve raporların arkasındaki satır satır çalışan kodu bulur, set tabanlı T-SQL olarak yeniden yazar ve sonuçların aynı olduğunu doğrular; böylece job, yaptığı iş değişmeden hızlanır.

4.7/5