-
Du bör inte uppdatera 10 000 rader i en uppsättning om du inte är säker på att operationen får sidlås (på grund av att flera rader per sida ingår i
UPDATE
drift). Problemet är att låseskalering (från antingen rad- eller sida-till-tabell-lås) sker vid 5000 lås . Så det är säkrast att hålla den strax under 5000, ifall operationen använder radlås. -
Du borde inte använda SET ROWCOUNT för att begränsa antalet rader som kommer att ändras. Det finns två problem här:
-
Det har blivit utfasat sedan SQL Server 2005 släpptes (11 år sedan):
Att använda SET ROWCOUNT påverkar inte DELETE-, INSERT- och UPDATE-satserna i en framtida version av SQL Server. Undvik att använda SET ROWCOUNT med DELETE-, INSERT- och UPDATE-satser i nytt utvecklingsarbete, och planera för att modifiera applikationer som för närvarande använder det. För ett liknande beteende, använd TOP-syntaxen
-
Det kan påverka mer än bara uttalandet du har att göra med:
Att ställa in alternativet SET ROWCOUNT gör att de flesta Transact-SQL-satser slutar bearbetas när de har påverkats av det angivna antalet rader. Detta inkluderar triggers. Alternativet ROWCOUNT påverkar inte dynamiska markörer, men det begränsar raduppsättningen av tangentuppsättningar och okänsliga markörer. Det här alternativet bör användas med försiktighet.
Använd istället
TOP ()
klausul. -
-
Det finns inget syfte med att ha en explicit transaktion här. Det komplicerar koden och du har ingen hantering för en
ROLLBACK
, vilket inte ens behövs eftersom varje uttalande är sin egen transaktion (d.v.s. auto-commit). -
Förutsatt att du hittar en anledning att behålla den explicita transaktionen, så har du ingen
TRY
/CATCH
strukturera. Se mitt svar på DBA.StackExchange för enTRY
/CATCH
mall som hanterar transaktioner:Är vi skyldiga att hantera transaktioner i C#-kod samt i butiksprocedur
Jag misstänker att den verkliga WHERE
klausulen visas inte i exempelkoden i frågan, så att helt enkelt lita på det som har visats, en bättre modell skulle vara:
DECLARE @Rows INT,
@BatchSize INT; -- keep below 5000 to be safe
SET @BatchSize = 2000;
SET @Rows = @BatchSize; -- initialize just to enter the loop
BEGIN TRY
WHILE (@Rows = @BatchSize)
BEGIN
UPDATE TOP (@BatchSize) tab
SET tab.Value = 'abc1'
FROM TableName tab
WHERE tab.Parameter1 = 'abc'
AND tab.Parameter2 = 123
AND tab.Value <> 'abc1' COLLATE Latin1_General_100_BIN2;
-- Use a binary Collation (ending in _BIN2, not _BIN) to make sure
-- that you don't skip differences that compare the same due to
-- insensitivity of case, accent, etc, or linguistic equivalence.
SET @Rows = @@ROWCOUNT;
END;
END TRY
BEGIN CATCH
RAISERROR(stuff);
RETURN;
END CATCH;
Genom att testa @Rows
mot @BatchSize
, kan du undvika den sista UPDATE
fråga (i de flesta fall) eftersom den slutliga uppsättningen vanligtvis är ett antal rader mindre än @BatchSize
, i så fall vet vi att det inte finns fler att bearbeta (vilket är vad du ser i resultatet som visas i ditt svar). Endast i de fall där den slutliga uppsättningen rader är lika med @BatchSize
kommer den här koden att köra en sista UPDATE
påverkar 0 rader.
Jag har också lagt till ett villkor i WHERE
klausul för att förhindra att rader som redan har uppdaterats uppdateras igen.
OBS OM PRESTANDA
Jag betonade "bättre" ovan (som i "det här är ett bättre). modell") eftersom detta har flera förbättringar jämfört med O.P.:s ursprungliga kod och fungerar bra i många fall, men är inte perfekt för alla fall. För tabeller av åtminstone en viss storlek (som varierar på grund av flera faktorer så jag kan' Om det inte är mer specifik), kommer prestanda att försämras eftersom det finns färre rader att fixa om antingen:
- det finns inget index som stöder frågan, eller
- det finns ett index, men minst en kolumn i
WHERE
klausul är en strängdatatyp som inte använder en binär sammanställning, därav enCOLLATE
sats läggs till i frågan här för att tvinga fram den binära sammanställningen, och om du gör det ogiltigförklaras indexet (för just den här frågan).
Det här är situationen som @mikesigs stötte på, vilket kräver ett annat tillvägagångssätt. Den uppdaterade metoden kopierar ID:n för alla rader som ska uppdateras till en temporär tabell och använder sedan den temporära tabellen för att INNER JOIN
till tabellen som uppdateras på kolumnen/kolumnerna för klustrade indexnyckel. (Det är viktigt att fånga och gå med i det klustrade indexet kolumner, oavsett om de är de primära nyckelkolumnerna eller inte!).
Se @mikesigs svar nedan för detaljer. Tillvägagångssättet som visas i det svaret är ett mycket effektivt mönster som jag själv har använt vid många tillfällen. De enda ändringarna jag skulle göra är:
- Skapa uttryckligen
#targetIds
tabell istället för att användaSELECT INTO...
- För
#targetIds
tabell, deklarera en klustrad primärnyckel på kolumnen/kolumnerna. - För
#batchIds
tabell, deklarera en klustrad primärnyckel på kolumnen/kolumnerna. - För infogning i
#targetIds
, användINSERT INTO #targetIds (column_name(s)) SELECT
och ta bortORDER BY
eftersom det är onödigt.
Så om du inte har ett index som kan användas för den här operationen och inte tillfälligt kan skapa ett som faktiskt fungerar (ett filtrerat index kan fungera, beroende på din WHERE
klausul för UPDATE
fråga), prova sedan metoden som visas i @mikesigs svar (och om du använder den lösningen, vänligen rösta upp den).