Muszę usunąć ponad 16 milionów rekordów z tabeli ponad 221 milionów wierszy i idzie to bardzo wolno.
Doceniam, jeśli podzielisz się sugestiami, aby kod poniżej był szybszy:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE @BATCHSIZE INT,
@ITERATION INT,
@TOTALROWS INT,
@MSG VARCHAR(500);
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;
BEGIN TRY
BEGIN TRANSACTION;
WHILE @BATCHSIZE > 0
BEGIN
DELETE TOP (@BATCHSIZE) FROM MySourceTable
OUTPUT DELETED.*
INTO MyBackupTable
WHERE NOT EXISTS (
SELECT NULL AS Empty
FROM dbo.vendor AS v
WHERE VendorId = v.Id
);
SET @BATCHSIZE = @@ROWCOUNT;
SET @ITERATION = @ITERATION + 1;
SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);
PRINT @MSG;
COMMIT TRANSACTION;
CHECKPOINT;
END;
END TRY
BEGIN CATCH
IF @@ERROR <> 0
AND @@TRANCOUNT > 0
BEGIN
PRINT 'There is an error occured. The database update failed.';
ROLLBACK TRANSACTION;
END;
END CATCH;
GO
Plan wykonania (ograniczony do 2 iteracji)
VendorId
jest PK i nieklastrowany , gdzie indeks klastrowany nie jest używany przez ten skrypt. Istnieje 5 innych nieunikalnych indeksów nieklastrowych.
Zadanie polega na „usunięciu dostawców, którzy nie istnieją w innej tabeli” i skopiowaniu ich do innej tabeli. Mam 3 tabel vendors, SpecialVendors, SpecialVendorBackups
. Próbuję usunąć te, SpecialVendors
których nie ma w Vendors
tabeli, i wykonać kopię zapasową usuniętych rekordów na wypadek, gdyby to, co robię, było złe i musiałem je przywrócić za tydzień lub dwa.
sql-server
query-performance
delete
cilerler
źródło
źródło
Odpowiedzi:
Plan wykonania pokazuje, że odczytuje wiersze z indeksu nieklastrowanego w określonej kolejności, a następnie wykonuje wyszukiwanie dla każdego odczytanego wiersza zewnętrznego w celu oceny
NOT EXISTS
Usuwasz 7,2% tabeli. 16 000 000 wierszy w 3556 partiach po 4500
Zakładając, że kwalifikujące się wiersze są oczywiście rozmieszczone w całym indeksie, oznacza to, że usunie około 1 wiersza co 13,8 wierszy.
Tak więc iteracja 1 odczyta 62 156 wierszy i wykona tyle wyszukiwań indeksu, zanim znajdzie 4500 do usunięcia.
iteracja 2 odczyta 57 656 (62 156 - 4500) wierszy, które zdecydowanie nie kwalifikują się do zignorowania jakichkolwiek równoczesnych aktualizacji (ponieważ zostały już przetworzone), a następnie kolejne 62 156 wierszy, aby 4500 usunięto.
iteracja 3 odczyta (2 * 57 656) + 62 156 wierszy i tak dalej, aż w końcu iteracja 3556 odczyta (3555 * 57 656) + 62 156 wierszy i wykona tyle prób.
Tak więc liczba wyszukiwań indeksu wykonanych we wszystkich partiach wynosi
SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)
Który jest
((3555 * 3556 / 2) * 57656) + (3556 * 62156)
- lub364,652,494,976
Sugerowałbym, aby najpierw zmaterializować wiersze do usunięcia w tabeli tymczasowej
I zmienić
DELETE
, aby usunąćWHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)
Nadal może trzeba obejmująNOT EXISTS
wDELETE
zapytaniu samego celu zaspokojenia aktualizacjach ponieważ tabela temp została wypełniona, ale to powinno być znacznie bardziej skuteczne, jak to tylko będzie trzeba wykonać 4500 ma na partię.źródło
PK
kolumna? (DELETE TOP (@BATCHSIZE) FROM MySourceTable
powinien po prostuDELETE FROM MySourceTable
indeksować tabelę tempCREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );
i czyVendorId
zdecydowanie PK jest sam w sobie? Masz> 221 milionów różnych dostawców?Plan wykonania sugeruje, że każda kolejna pętla wykona więcej pracy niż poprzednia. Zakładając, że wiersze do usunięcia są równomiernie rozmieszczone w całej tabeli, pierwsza pętla będzie musiała zeskanować około 4500 * 221000000/16000000 = 62156 wierszy, aby znaleźć 4500 wierszy do usunięcia. Wykona również tę samą liczbę wyszukiwań indeksu klastrowego względem
vendor
tabeli. Jednak druga pętla będzie musiała odczytać te same wiersze 62156 - 4500 = 57656, które nie zostały usunięte za pierwszym razem. Możemy się spodziewać, że druga pętla przeskanuje 120000 wierszy ziMySourceTable
wykona 120000 wyszukiwań względemvendor
tabeli. Ilość pracy potrzebnej na pętlę rośnie w tempie liniowym. W przybliżeniu możemy powiedzieć, że średnia pętla będzie musiała odczytać 102516868 wierszy ziMySourceTable
do 102516868 szuka względemvendor
stół. Aby usunąć 16 milionów wierszy o wielkości partii 4500, kod musi wykonać 16000000/4500 = 3556 pętli, więc łączna ilość pracy do wykonania przez kod wynosi około 364,5 miliarda wierszy odczytanychMySourceTable
i 364,5 miliarda indeksów.Mniejszy problem polega na tym, że używasz zmiennej lokalnej
@BATCHSIZE
w wyrażeniu TOP bez żadnejRECOMPILE
innej wskazówki. Optymalizator zapytań nie pozna wartości tej zmiennej lokalnej podczas tworzenia planu. Zakłada się, że jest równa 100. W rzeczywistości usuwasz 4500 wierszy zamiast 100, i możesz ewentualnie skończyć z mniej wydajnym planem z powodu tej rozbieżności. Niska ocena liczności przy wstawianiu do tabeli może również spowodować obniżenie wydajności. SQL Server może wybrać inny wewnętrzny interfejs API do wstawiania, jeśli uzna, że musi wstawić 100 wierszy zamiast 4500 wierszy.Jedną z możliwości jest po prostu wstawienie kluczy podstawowych / kluczy klastrowych wierszy, które chcesz usunąć, do tabeli tymczasowej. W zależności od wielkości twoich kluczowych kolumn może to łatwo zmieścić się w tempdb. W takim przypadku możesz uzyskać minimalne logowanie , co oznacza, że dziennik transakcji nie zostanie wysadzony. Możesz także uzyskać minimalne rejestrowanie w dowolnej bazie danych z modelem odzyskiwania
SIMPLE
. Zobacz link, aby uzyskać więcej informacji na temat wymagań.Jeśli nie jest to opcja, należy zmienić kod, aby móc skorzystać z indeksu klastrowego
MySourceTable
. Kluczową sprawą jest napisanie kodu, aby wykonać mniej więcej tyle samo pracy na pętlę. Możesz to zrobić, wykorzystując indeks zamiast za każdym razem skanować tabelę od początku. Napisałem post na blogu, który omawia różne metody zapętlania. Przykłady w tym poście zamiast wstawiania wstawiają do tabeli, ale powinieneś być w stanie dostosować kod.W przykładowym kodzie poniżej zakładam, że klucz podstawowy i klucz klastrowany twojego
MySourceTable
. Napisałem ten kod dość szybko i nie jestem w stanie go przetestować:Kluczowa część jest tutaj:
Każda pętla odczyta tylko 60000 wierszy
MySourceTable
. Powinno to spowodować średni rozmiar usuwania wynoszący 4500 wierszy na transakcję i maksymalny rozmiar usuwania wynoszący 60000 wierszy na transakcję. Jeśli chcesz być bardziej konserwatywny z mniejszym rozmiarem partii, to też jest w porządku. Te@STARTID
zmienne przesunie się po każdej pętli, dzięki czemu można uniknąć czytając ten sam wiersz więcej niż raz z tabeli źródłowej.źródło
Przychodzą mi na myśl dwie myśli:
Opóźnienie jest prawdopodobnie spowodowane indeksowaniem z taką ilością danych. Spróbuj upuścić indeksy, usunąć i ponownie zbudować indeksy.
Lub..
Szybsze może być skopiowanie wierszy, które chcesz zachować, do tabeli tymczasowej, upuszczenie tabeli z 16 milionami wierszy i zmiana nazwy tabeli tymczasowej (lub skopiowanie do nowej instancji tabeli źródłowej).
źródło