Jak usunąć duże dane tabeli w SQL bez dziennika?

128

Mam dużą tabelę danych. W tej tabeli jest 10 milionów rekordów.

Jaki jest najlepszy sposób na to zapytanie

   Delete LargeTable where readTime < dateadd(MONTH,-7,GETDATE())
user3107343
źródło
4
:) Obawiam się, że jeśli nie zechcesz napisać jakiegoś ETL, aby uzyskać wszystkie wiersze readTime> = dateadd (MONTH, -7, GETDATE ()) do innej tabeli, a następnie wydać tabelę Obetnij i wstaw dane z powrotem za pomocą ETL , nie
byłbyś w
Rejestrowanie jest funkcją „wszystko albo nic” w przypadku stabilnych transakcji. Dosłownie nie ma sensu nie mieć dziennika dla niektórych operacji, ale nie dla innych, w przeciwnym razie dziennik jest bezużyteczny.
Erik Philips,
1
Wyeksportuj dane, które chcesz zachować, skróć tabelę, a następnie zaimportuj z powrotem
język czeski
Inną opcją byłoby użycie zmiennych tabeli, które nie są rejestrowane. Dlatego przechowuj dane readTime> = dateadd (MONTH, -7, GETDATE ()) w zmiennej tabeli, a następnie obcinaj oryginalną tabelę i skopiuj dane z powrotem ze zmiennej tabeli. Chciałbym jednak zachować kopię zapasową danych na wypadek, gdyby coś poszło nie tak i tabela została nieumyślnie obcięta. :) I zawsze wykonuj testowe uruchomienie skryptu na mniejszym środowisku.
TMNT2014

Odpowiedzi:

203
  1. Jeśli usuwasz wszystkie wiersze w tej tabeli, najprostszą opcją jest obcięcie tabeli, coś w rodzaju

    TRUNCATE TABLE LargeTable
    GO

    Truncate table po prostu opróżni tabelę, nie możesz użyć klauzuli WHERE do ograniczenia liczby usuwanych wierszy i żadne wyzwalacze nie zostaną uruchomione.

  2. Z drugiej strony, jeśli usuwasz więcej niż 80-90 procent danych, powiedzmy, jeśli masz łącznie 11 milionów wierszy i chcesz usunąć 10 milionów innym sposobem byłoby wstawienie tych 1 miliona wierszy (rekordów, które chcesz zachować ) do innej tabeli pomostowej. Obetnij tę dużą tabelę i wstaw z powrotem te 1 milion wierszy.

  3. Lub jeśli upuszczenie tej tabeli nie ma wpływu na uprawnienia / widoki lub inne obiekty, które mają tę dużą tabelę jako tabelę bazową, możesz przenieść tę stosunkowo niewielką liczbę wierszy do innej tabeli i utworzyć inną tabelę z tym samym schematem i zaimportować je wiersze z powrotem do tej byłej dużej tabeli.

  4. Ostatnią opcją, o której przychodzi mi do głowy, jest zmiana bazy danych, Recovery Mode to SIMPLEa następnie usuwanie wierszy w mniejszych partiach za pomocą pętli while, podobnej do tego.

    DECLARE @Deleted_Rows INT;
    SET @Deleted_Rows = 1;
    
    
    WHILE (@Deleted_Rows > 0)
      BEGIN
       -- Delete some small number of rows at a time
         DELETE TOP (10000)  LargeTable 
         WHERE readTime < dateadd(MONTH,-7,GETDATE())
    
      SET @Deleted_Rows = @@ROWCOUNT;
    END

i nie zapomnij zmienić trybu odzyskiwania z powrotem na pełny i myślę, że musisz wykonać kopię zapasową, aby była w pełni skuteczna (tryby zmiany lub odzyskiwania).

M.Ali
źródło
14
Pamiętaj również, że jeśli skracasz tabelę, nie możesz mieć do niej przypisanych żadnych SK.
HLGEM
1
Ale jak się upewnić, że usuwasz 80-90% danych? Załóżmy, że mam tylko zakres wartości, które powinny zostać usunięte. Mam kilka stolików. Muszę więc sprawdzić każdy z nich i obliczyć procent, a jeśli wynosi około 30%, to myślę, że ta metoda nie jest zbyt skuteczna ... Próbuję znaleźć optymalne rozwiązanie dla nieznanego przypadku.
Archont
7
@Archont optimal solution for unknown case, to jest sen, prawda? Niestety nie można wyleczyć każdej choroby jedną tabletką; Zasugerowałem kilka możliwych rozwiązań dla różnych scenariuszy. Niestety, nie ma tu żadnej srebrnej kuli.
M.Ali
5
Jedna rzecz, na którą należy zwrócić uwagę, wybierając opcję 4: w zależności od tego, jak używana jest tabela, lepszym rozwiązaniem może być usunięcie mniej niż 5000 wierszy jednocześnie, aby uniknąć eskalacji blokad .
Daniel
Jeśli liczba rekordów do usunięcia jest znacznie większa niż rekordy, które pozostaną w tabeli, stwierdziłem, że prosty wybór do tabeli tymczasowej rekordów, które pozostaną w tabeli i upuszczenie oryginalnej tabeli, a zmiana nazwy tabeli tymczasowej jest znacznie szybsza. Zakładając, że nigdzie nie używasz klucza obcego identyfikatora tożsamości.
Vladimir Bozic
96

@ m-ali odpowiedź jest prawidłowa, ale pamiętaj również, że dzienniki mogą się znacznie rozrosnąć, jeśli nie zatwierdzisz transakcji po każdej porcji i nie wykonasz punktu kontrolnego. Oto jak bym to zrobił i wziął ten artykuł http://sqlperformance.com/2013/03/io-subsystem/chunk-deletes jako odniesienie, z testami wydajności i wykresami:

DECLARE @Deleted_Rows INT;
SET @Deleted_Rows = 1;


WHILE (@Deleted_Rows > 0)
  BEGIN

   BEGIN TRANSACTION

   -- Delete some small number of rows at a time
     DELETE TOP (10000)  LargeTable 
     WHERE readTime < dateadd(MONTH,-7,GETDATE())

     SET @Deleted_Rows = @@ROWCOUNT;

   COMMIT TRANSACTION
   CHECKPOINT -- for simple recovery model
END
Francisco Goldenstein
źródło
1
Powinna to być akceptowana odpowiedź w przypadku, gdy dostępne miejsce na dysku jest ograniczone. Bez COMMIT TRANSACTIONi CHECKPOINTdzienniki wciąż rosną. Dziękuję za wyjaśnienie.
gkoul
+1. Zwróć uwagę, że możesz chcieć porównać @Deleted_Rowsdo 10000 lub możesz skończyć z nieskończoną pętlą, ponieważ na czas nieokreślony usuwa małe zestawy danych. Tak więc WHILE (@Deleted_Rows = 10000)- gdy tylko nie będzie pełnej "strony" danych do usunięcia, zatrzyma się. W Twojej implementacji WHILE (@Deleted_Rows > 0)pętla while zostanie wykonana ponownie, nawet jeśli usunie tylko jeden wiersz, a następne wykonanie może również znaleźć wiersz lub dwa do usunięcia, co spowoduje nieskończoną pętlę.
NS du Toit
@NSduToit klauzula WHERE rozważa rekordy, które mają co najmniej 7 miesięcy, więc nie będzie nowych rekordów spełniających ten warunek podczas usuwania.
Francisco Goldenstein
@FranciscoGoldenstein Dobrze, termin użyty w zapytaniu będzie inny przy każdej iteracji, jak wielokrotnie obliczyć datę obrębie WHILEsamej pętli: dateadd(MONTH,-7,GETDATE()).
NS du Toit
@FranciscoGoldenstein Może również w innych przypadkach użycia niż ten - być może nowe dane zostaną dodane do podstawowej tabeli, co spowoduje powstanie nowych rekordów, które można usunąć między różnymi iteracjami WHILEpętli.
NS du Toit
52

Możesz także użyć GO +, ile razy chcesz wykonać to samo zapytanie.

DELETE TOP (10000)  [TARGETDATABASE].[SCHEMA].[TARGETTABLE] 
WHERE readTime < dateadd(MONTH,-1,GETDATE());
-- how many times you want the query to repeat
GO 100
Bunkerbuster
źródło
Podoba mi się to, działa u mnie przypadkowo wstawiłem ten sam wiersz do tabeli 26 milionów razy i musiałem usunąć wszystkie jego wystąpienia, które w jednej instrukcji usuwania zabrakło pamięci na serwerze, więc to jest świetne jedno pytanie , czy zatrzyma środkową pętlę, jeśli zabraknie wierszy do usunięcia?
ScottC
2
@ScottC, to nie jest pętla, po prostu powtarza zapytanie (jak partia) i jeśli zabraknie wierszy, nie może niczego usunąć. Ale to się nie skończy. dostaniesz coś takiego jak (0 dotkniętych wierszy), jeśli skończy się liczba usuniętych wierszy.
Bunkerbuster
Ach, tak, odkryłem, że około 5 minut po opublikowaniu mojego pytania, ponieważ zakończyło się usuwanie, dzięki temu było to bardzo pomocne!
ScottC,
1
Na jakim serwerze MS SQL GO xxma działać ta składnia ? Pojawia się błąd „Nie można znaleźć procedury składowanej” ” . GOJednak bez polecenia działa dobrze.
Abel
3
Hmm, wygląda na to, że mogę go wykonać i działa on rzeczywiście wiele razy, ale w MS SQL Mgt Studio pokazuje czerwoną kręconą linię ze wspomnianym błędem (ale F5-run działa wtedy)
Abel
11

@Francisco Goldenstein, tylko drobna poprawka. COMMIT musi być użyty po ustawieniu zmiennej, w przeciwnym razie WHILE zostanie wykonane tylko raz:

DECLARE @Deleted_Rows INT;
SET @Deleted_Rows = 1;

WHILE (@Deleted_Rows > 0)
BEGIN
    BEGIN TRANSACTION

    -- Delete some small number of rows at a time
    DELETE TOP (10000)  LargeTable 
    WHERE readTime < dateadd(MONTH,-7,GETDATE())

    SET @Deleted_Rows = @@ROWCOUNT;

    COMMIT TRANSACTION
    CHECKPOINT -- for simple recovery model

END
Cassio Veras
źródło
10

Ta odmiana M.Ali działa dobrze dla mnie. Usuwa niektóre, czyści dziennik i powtarza. Patrzę, jak kłoda rośnie, spada i zaczynam od nowa.

DECLARE @Deleted_Rows INT;
SET @Deleted_Rows = 1;
WHILE (@Deleted_Rows > 0)
  BEGIN
   -- Delete some small number of rows at a time
    delete top (100000) from InstallLog where DateTime between '2014-12-01' and '2015-02-01'
    SET @Deleted_Rows = @@ROWCOUNT;
    dbcc shrinkfile (MobiControlDB_log,0,truncateonly);
END
Ken Koehler
źródło
To było bardzo przydatne! Zmodyfikowałem go, aby sparametryzować, # of rowsaby usunąć jednocześnie, a także WHEREklauzulę. Działa jak marzenie!
Shiva
7

Jeśli chcesz (i możesz) zaimplementować partycjonowanie, jest to skuteczna technika usuwania dużych ilości danych przy niewielkim nakładzie czasu wykonywania. Jednak nieopłacalne w przypadku jednorazowego ćwiczenia.

Michael Green
źródło
4

Udało mi się usunąć 19 milionów wierszy z mojej tabeli z 21 milionami wierszy w ciągu kilku minut . Oto moje podejście.

Jeśli masz automatycznie zwiększający się klucz podstawowy w tej tabeli, możesz użyć tego klucza podstawowego.

  1. Uzyskaj minimalną wartość klucza podstawowego dużej tabeli, gdzie readTime <dateadd (MONTH, -7, GETDATE ()). (Dodaj indeks w readTime, jeśli jeszcze nie jest obecny, ten indeks i tak zostanie usunięty wraz z tabelą w kroku 3). Przechowujmy to w zmiennej „min_primary”

  2. Wstaw wszystkie wiersze z kluczem podstawowym> min_primary do tabeli pomostowej (tabela pamięci, jeśli liczba wierszy nie jest duża).

  3. Opuść duży stół.

  4. Odtwórz tabelę. Skopiuj wszystkie wiersze z tabeli pomostowej do tabeli głównej.

  5. Porzuć stół pomostowy.

Arpan Jain
źródło
3

Możesz usuwać małe partie za pomocą pętli while, na przykład:

DELETE TOP (10000)  LargeTable 
WHERE readTime < dateadd(MONTH,-7,GETDATE())
WHILE @@ROWCOUNT > 0
BEGIN
    DELETE TOP (10000)  LargeTable 
    WHERE readTime < dateadd(MONTH,-7,GETDATE())
END
Fábio Nascimento
źródło
2

Inne zastosowanie:

SET ROWCOUNT 1000 -- Buffer

DECLARE @DATE AS DATETIME = dateadd(MONTH,-7,GETDATE())

DELETE LargeTable  WHERE readTime < @DATE
WHILE @@ROWCOUNT > 0
BEGIN
   DELETE LargeTable  WHERE readTime < @DATE
END
SET ROWCOUNT 0

Opcjonalny;

Jeśli dziennik transakcji jest włączony, wyłącz dzienniki transakcji.

ALTER DATABASE dbname SET RECOVERY SIMPLE;
Ali Osman Yavuz
źródło
2

Krótsza składnia

select 1
WHILE (@@ROWCOUNT > 0)
BEGIN
  DELETE TOP (10000) LargeTable 
  WHERE readTime < dateadd(MONTH,-7,GETDATE())
END
paparazzo
źródło
1

Jeśli używasz programu SQL Server 2016 lub nowszego i jeśli w Twojej tabeli są utworzone partycje na podstawie kolumny, którą próbujesz usunąć (na przykład kolumna Timestamp), możesz użyć tego nowego polecenia do usunięcia danych według partycji.

TRUNCATE TABLE WITH (PARTYCJE ({|} [, ... n]))

Spowoduje to usunięcie danych tylko z wybranych partycji i powinno być najbardziej wydajnym sposobem usunięcia danych z części tabeli, ponieważ nie utworzy dzienników transakcji i zostanie wykonane tak samo szybko, jak zwykłe obcinanie, ale bez usuwania wszystkich danych ze stołu.

Wadą jest to, że jeśli twoja tabela nie jest skonfigurowana z partycją, musisz przejść do starej szkoły i usunąć dane zwykłym podejściem, a następnie ponownie utworzyć tabelę z partycjami, abyś mógł to zrobić w przyszłości, co zrobiłem. Dodałem tworzenie i usuwanie partycji do samej procedury wstawiania. Miałem tabelę z 500 milionami wierszy, więc była to jedyna opcja, aby skrócić czas usuwania.

Aby uzyskać więcej informacji, skorzystaj z poniższych linków: https://docs.microsoft.com/en-us/sql/t-sql/statements/truncate-table-transact-sql?view=sql-server-2017

SQL Server 2016 Obetnij tabelę z partycjami

Poniżej opisano, co najpierw zrobiłem, aby usunąć dane, zanim mogłem odtworzyć tabelę z partycjami z wymaganymi danymi. To zapytanie będzie działało przez kilka dni w określonym przedziale czasu, aż do usunięcia danych.

:connect <<ServerName>>
use <<DatabaseName>>

SET NOCOUNT ON;
DECLARE @Deleted_Rows INT;
DECLARE @loopnum INT;
DECLARE @msg varchar(100);
DECLARE @FlagDate datetime;
SET @FlagDate =  getdate() - 31;
SET @Deleted_Rows = 1;
SET @loopnum = 1;

/*while (getdate() < convert(datetime,'2018-11-08 14:00:00.000',120))
BEGIN
    RAISERROR( 'WAIT for START' ,0,1) WITH NOWAIT   
    WAITFOR DELAY '00:10:00'
END*/
RAISERROR( 'STARTING PURGE' ,0,1) WITH NOWAIT   

WHILE (1=1)
BEGIN
    WHILE (@Deleted_Rows > 0 AND (datepart(hh, getdate() ) >= 12 AND datepart(hh, getdate() ) <= 20)) -- (getdate() < convert(datetime,'2018-11-08 19:00:00.000',120) )
      BEGIN
       -- Delete some small number of rows at a time
         DELETE TOP (500000)  dbo.<<table_name>>
         WHERE timestamp_column < convert(datetime, @FlagDate,102)
         SET @Deleted_Rows = @@ROWCOUNT;
         WAITFOR DELAY '00:00:01'
         select @msg = 'ROWCOUNT' + convert(varchar,@Deleted_Rows);
         set @loopnum = @loopnum + 1
         if @loopnum > 1000
             begin 
                 begin try
                        DBCC SHRINKFILE (N'<<databasename>>_log' , 0, TRUNCATEONLY)
                        RAISERROR( @msg ,0,1) WITH NOWAIT
                 end try
                 begin catch
                     RAISERROR( 'DBCC SHRINK' ,0,1) WITH NOWAIT  
                 end catch
                 set @loopnum = 1
             end
        END
WAITFOR DELAY '00:10:00'
END 
select getdate()
digital_inspired
źródło
0

Jeśli powiem bez pętli, mogę użyć GOTOinstrukcji do usunięcia dużej ilości rekordów za pomocą serwera sql. exa.

 IsRepeat:
    DELETE TOP (10000)
    FROM <TableName>
    IF @@ROWCOUNT > 0
         GOTO IsRepeat

w ten sposób możesz usunąć dużą ilość danych przy mniejszym rozmiarze.

daj mi znać, jeśli potrzebujesz więcej informacji.

Lalji Dhameliya
źródło