Cześć!
Wszystkie zapytania pisałem z palca, gdyż zależy mi na ideowym opisaniu problemu, a nie na pokazywaniu całości kodu.
Mamy następującą sytuację: tabelka Customers i tabelka Positions. Chcemy skasować dane następującym zapytaniem
DELETE FROM Customers
WHERE Id IN (
SELECT C.Id
FROM Customers AS C
JOIN Positions AS P ON P.CustomerId = P.Id
WHERE
P.CreationDate >= 'jakas-data'
AND P.CreationDate <= 'jakas-data'
AND P.Type = 'jakis-typ'
AND P.Rodzaj NOT LIKE '%COS%'
)
Czyli chcemy skasować klientów, którzy mają pozycję odpowiedniego typu w jakimś tam okresie. Customers.Id to klucz główny (jest na nim indeks klastrowy), na tabelce Positions nie ma żadnych użytecznych dla tego zapytania indeksów.
W Customers jest 800 milionów rekordów, do skasowania jest około 75 milionów, czyli 10% tabeli. Odpalenie powyższego zapytania na raz nie wchodzi w grę, gdyż nie chcemy wyłączać aplikacji pracujących na tej bazie, więc aby uniknąć eskalacji blokad na poziom tabeli kasujemy dane w partiach po 4500 wierszy. Coś na kształt:
SET DEADLOCK_PRIORITY = -10
DECLARE @i INT;
SET @i = 1;
WHILE @i > 0
BEGIN
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITED
BEGIN TRAN
DELETE TOP(4500) FROM Customers
WHERE Id IN (
SELECT TOP(4500) C.Id
FROM Customers AS C
JOIN Positions AS P ON P.CustomerId = P.Id
WHERE
P.CreationDate >= 'jakas-data'
AND P.CreationDate <= 'jakas-data'
AND P.Type = 'jakis-typ'
AND P.Rodzaj NOT LIKE '%COS%'
)
SET @i = @@ROWCOUNT
COMMIT
PRINT GetDate()
END
Czyli obniżamy priorytet na potrzeby deadlocków (abyśmy to my byli zabici w razie potrzeby), obniżamy poziom transakcji i kasujemy 4500 wierszy (eskalacja następuje po 5000 wierszy). W kodzie są jeszcze bloki try i catch obsługujące deadlock i błędy, ale nie są one przesadnie istotne.
Zapytanie wykonuje się poprawnie, kasuje dobre dane, niestety każda iteracja wykonuje się coraz wolniej: pierwsza iteracja trwała około 5 sekund, po paru godzinach iteracja trwała już 50 sekund. Intuicyjnie rozumuję to tak, że serwer za każdym razem wykonuje zapytanie SELECT od początku tabeli, więc wielokrotnie przeszukuje masę wierszy, których nie usuwa, a że za każdym razem wiersze do usunięcia są "dalej", to i iteracja trwa dłużej. Brzmi sensownie?
Bazując na tej intuicji zmieniamy zapytanie na używające kursora. Na początku konfigurujemy serwer tak, aby nie zamykał kursora po commicie, potem kasujemy 4500 wierszy w pętli. Coś takiego:
Priorytety, zmienne itp
DECLARE kursor CURSOR DYNAMIC FOR
SELECT C.Id
FROM Customers AS C
JOIN Positions AS P ON P.CustomerId = P.Id
WHERE
P.CreationDate >= 'jakas-data'
AND P.CreationDate <= 'jakas-data'
AND P.Type = 'jakis-typ'
AND P.Rodzaj NOT LIKE '%COS%'
)
WHILE sa jakies dane do usuniecia
BEGIN
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITED
BEGIN TRAN
WHILE jest cos w kursorze i nie przekroczylismy 4500 wierszy
BEGIN
FETCH NEXT FROM kursor INTO @idToRemove
DELETE FROM Customers WHERE CURRENT OF kursor
END
COMMIT
...
END
I teraz tak: ponieważ serwer nie zamyka kursora po commicie, teoretycznie przy każdym obrocie powinniśmy przeszukiwać wiersze "dalej" (a nie "od początku"), więc czas wykonywania iteracji powinien być mniej więcej stały (przy założeniu, że dane do usunięcia są rozlokowane mniej więcej w równych odstępach). Jednocześnie commit zwalnia blokady założone przez kursor, więc eskalacja nam nie grozi. Jednakże zapytanie z kursorem zwalnia o wiele bardziej: o ile pierwsza iteracja wykonała się w jakieś 5 sekund, to już po kilku minutach iteracja wykonuje się około 60 sekund i zwalnia dalej.
Teraz pytanie: czy to zapytanie z kursorem ma szansę zadziałać zgodnie z opisaną przeze mnie intuicją, czy jest kompletnie do bani?
Próbowałem weryfikować te przypuszczenia, więc na boku odpaliłem to zapytanie z poziomem SERIALIZABLE i w iteracji co 5 wierszy, i przy każdym wierszu wypisywałem exec sp_lock. Blokady są zwalniane po commicie, a locki w każdym batchu są zakładane na inne wiersze (widzę inne zestawy guidów), więc teoretycznie działa zgodnie z koncepcją, a jednak bardzo zwalnia. Czy ktoś jest w stanie wyjaśnić, czy to intuicja jest zła i gdzieś popełniłem błąd, czy może koncepcyjnie wszystko jest okej i zwolnienie jest spowodowane czymś innym?
Baza o MS SQL 2005 Standard, wszystko działa na Windows Server 2003.
Dzięki za zainteresowanie
Afish