sql >> Databasteknik >  >> RDS >> Sqlserver

Hur uppdaterar man en stor tabell med miljontals rader i SQL Server?

  1. 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.

  2. Du borde inte använda SET ROWCOUNT för att begränsa antalet rader som kommer att ändras. Det finns två problem här:

    1. 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

    2. 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.

  3. 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).

  4. 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 en TRY / 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:

  1. det finns inget index som stöder frågan, eller
  2. 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 en COLLATE 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:

  1. Skapa uttryckligen #targetIds tabell istället för att använda SELECT INTO...
  2. För #targetIds tabell, deklarera en klustrad primärnyckel på kolumnen/kolumnerna.
  3. För #batchIds tabell, deklarera en klustrad primärnyckel på kolumnen/kolumnerna.
  4. För infogning i #targetIds , använd INSERT INTO #targetIds (column_name(s)) SELECT och ta bort ORDER 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).



  1. Generisk Ruby-lösning för SQLite3 LIKE eller PostgreSQL ILIKE?

  2. Hur trunkerar man alla tabeller i en databas med TSQL?

  3. Lagra filer i databas kontra filsystem

  4. Hur man hittar namnet på en begränsning i MySQL