Zapytanie SQL: usunąć wszystkie rekordy z tabeli z wyjątkiem ostatniego N?

90

Czy jest możliwe zbudowanie pojedynczego zapytania mysql (bez zmiennych) w celu usunięcia wszystkich rekordów z tabeli, z wyjątkiem ostatniego N (posortowanego według identyfikatora desc)?

Coś takiego, tylko to nie działa :)

delete from table order by id ASC limit ((select count(*) from table ) - N)

Dzięki.

serg
źródło

Odpowiedzi:

140

Nie możesz usunąć rekordów w ten sposób, a głównym problemem jest to, że nie możesz użyć podzapytania do określenia wartości klauzuli LIMIT.

To działa (testowane w MySQL 5.0.67):

DELETE FROM `table`
WHERE id NOT IN (
  SELECT id
  FROM (
    SELECT id
    FROM `table`
    ORDER BY id DESC
    LIMIT 42 -- keep this many records
  ) foo
);

Wymagane jest podzapytanie pośrednie . Bez tego napotkalibyśmy dwa błędy:

  1. Błąd SQL (1093): Nie można określić tabeli docelowej „tabela” do aktualizacji w klauzuli FROM - MySQL nie zezwala na odwoływanie się do usuwanej tabeli w ramach bezpośredniego podzapytania.
  2. Błąd SQL (1235): ta wersja MySQL nie obsługuje jeszcze podzapytania „LIMIT & IN / ALL / ANY / SOME” - nie można używać klauzuli LIMIT w bezpośrednim podzapytaniu operatora NOT IN.

Na szczęście użycie pośredniego podzapytania pozwala ominąć oba te ograniczenia.


Nicole zwróciła uwagę, że to zapytanie można znacznie zoptymalizować dla pewnych przypadków użycia (takich jak ten). Polecam również przeczytanie tej odpowiedzi, aby sprawdzić, czy pasuje do Twojej.

Alex Barrett
źródło
4
Dobra, to działa - ale dla mnie to nieeleganckie i niezadowalające, że muszę uciekać się do takich tajemnych sztuczek. Mimo to +1 za odpowiedź.
Bill Karwin
1
Oznaczam to jako zaakceptowaną odpowiedź, ponieważ robi to, o co prosiłem. Ale osobiście zrobię to prawdopodobnie w dwóch zapytaniach, żeby wszystko było proste :) Pomyślałem, że może jest jakiś szybki i łatwy sposób.
serg
1
Dzięki Alex, twoja odpowiedź mi pomogła. Widzę, że wymagane jest podzapytanie pośrednie, ale nie rozumiem dlaczego. Czy masz na to wytłumaczenie?
Sv1
8
pytanie: po co to „foo”?
Sebastian Breit,
9
Perroloco, próbowałem bez foo i otrzymałem ten błąd: BŁĄD 1248 (42000): Każda tabela pochodna musi mieć swój własny alias Więc ich odpowiedź brzmi: każda tabela pochodna musi mieć swój własny alias!
codygman
106

Wiem, że wskrzeszam dość stare pytanie, ale ostatnio napotkałem ten problem, ale potrzebowałem czegoś, co dobrze skaluje się do dużej liczby . Nie było żadnych istniejących danych dotyczących wydajności, a ponieważ to pytanie przyciągnęło sporo uwagi, pomyślałem, że opublikuję to, co znalazłem.

Rozwiązaniami, które faktycznie zadziałały, były podwójne pod-zapytanie /NOT IN metoda Alexa Barretta (podobne do Billa Karwina ) i metoda QuassnoiLEFT JOIN .

Niestety obie powyższe metody tworzą bardzo duże pośrednie tabele tymczasowe, a wydajność szybko spada, ponieważ liczba rekordów, które nie są usuwane, staje się duża.

To, na czym się zdecydowałem, wykorzystuje podwójne zapytanie podrzędne Alexa Barretta (dzięki!), Ale <=zamiast NOT IN:

DELETE FROM `test_sandbox`
  WHERE id <= (
    SELECT id
    FROM (
      SELECT id
      FROM `test_sandbox`
      ORDER BY id DESC
      LIMIT 1 OFFSET 42 -- keep this many records
    ) foo
  )

Używa OFFSETdo uzyskania identyfikatora N- tego rekordu i usuwa ten rekord oraz wszystkie poprzednie rekordy.

Ponieważ zamawianie jest już założeniem tego problemu ( ORDER BY id DESC), <=jest to idealne dopasowanie.

Jest to znacznie szybsze, ponieważ tymczasowa tabela wygenerowana przez podzapytanie zawiera tylko jeden rekord zamiast N rekordów.

Przypadek testowy

Przetestowałem trzy metody pracy i nową metodę powyżej w dwóch przypadkach testowych.

Oba przypadki testowe używają 10000 istniejących wierszy, podczas gdy pierwszy test zachowuje 9000 (usuwa najstarszy 1000), a drugi test zachowuje 50 (usuwa najstarszy 9950).

+-----------+------------------------+----------------------+
|           | 10000 TOTAL, KEEP 9000 | 10000 TOTAL, KEEP 50 |
+-----------+------------------------+----------------------+
| NOT IN    |         3.2542 seconds |       0.1629 seconds |
| NOT IN v2 |         4.5863 seconds |       0.1650 seconds |
| <=,OFFSET |         0.0204 seconds |       0.1076 seconds |
+-----------+------------------------+----------------------+

Co ciekawe, <=metoda zapewnia lepszą wydajność we wszystkich obszarach, ale w rzeczywistości staje się lepsza, im więcej trzymasz, zamiast gorzej.

Nicole
źródło
11
Czytam ten wątek ponownie 4,5 roku później. Niezły dodatek!
Alex Barrett
Wow, to wygląda świetnie, ale nie działa w Microsoft SQL 2008. Otrzymuję komunikat: „Niepoprawna składnia w pobliżu„ Limit ”. Fajnie, że działa w MySQL, ale muszę znaleźć alternatywne rozwiązanie.
Ken Palmer
1
@KenPalmer Powinieneś nadal być w stanie znaleźć określone przesunięcie wiersza za pomocą ROW_NUMBER(): stackoverflow.com/questions/603724/ ...
Nicole
3
@KenPalmer używa SELECT TOP zamiast LIMIT podczas przełączania się między SQL a mySQL
Alpha G33k
1
Dzięki za to. Zmniejszyło to zapytanie dotyczące mojego (bardzo dużego) zestawu danych z 12 minut do 3,64 sekundy!
Lieuwe
10

Niestety w przypadku wszystkich odpowiedzi udzielonych przez innych ludzi nie możesz DELETEiz SELECTdanej tabeli w tym samym zapytaniu.

DELETE FROM mytable WHERE id NOT IN (SELECT MAX(id) FROM mytable);

ERROR 1093 (HY000): You can't specify target table 'mytable' for update 
in FROM clause

Nie może też obsługiwać MySQL LIMITw podzapytaniu. To są ograniczenia MySQL.

DELETE FROM mytable WHERE id NOT IN 
  (SELECT id FROM mytable ORDER BY id DESC LIMIT 1);

ERROR 1235 (42000): This version of MySQL doesn't yet support 
'LIMIT & IN/ALL/ANY/SOME subquery'

Najlepszą odpowiedzią, jaką mogę wymyślić, jest zrobienie tego w dwóch etapach:

SELECT id FROM mytable ORDER BY id DESC LIMIT n; 

Zbierz identyfikatory i utwórz z nich ciąg oddzielony przecinkami:

DELETE FROM mytable WHERE id NOT IN ( ...comma-separated string... );

(Zwykle interpolacja listy oddzielonej przecinkami do instrukcji SQL wprowadza pewne ryzyko iniekcji SQL, ale w tym przypadku wartości nie pochodzą z niezaufanego źródła, są znane jako liczby całkowite z samej bazy danych).

uwaga: chociaż nie pozwala to na wykonanie zadania w jednym zapytaniu, czasami najskuteczniejsze jest prostsze rozwiązanie do wykonania.

Bill Karwin
źródło
Ale możesz wykonać połączenia wewnętrzne między usunięciem a zaznaczeniem. To, co zrobiłem poniżej, powinno działać.
achinda99
Musisz użyć podzapytania pośredniego, aby LIMIT działał w podzapytaniu.
Alex Barrett
@ achinda99: Nie widzę odpowiedzi od Ciebie w tym wątku ...?
Bill Karwin
Zostałem wciągnięty na spotkanie. Mój błąd. Nie mam w tej chwili środowiska testowego do testowania napisanego przeze mnie sql, ale zrobiłem zarówno to, co zrobił Alex Barret, jak i sprawiłem, że działa ze złączeniem wewnętrznym.
achinda99
To głupie ograniczenie MySQL. Z PostgreSQL DELETE FROM mytable WHERE id NOT IN (SELECT id FROM mytable ORDER BY id DESC LIMIT 3);działa dobrze.
bortzmeyer,
8
DELETE  i1.*
FROM    items i1
LEFT JOIN
        (
        SELECT  id
        FROM    items ii
        ORDER BY
                id DESC
        LIMIT 20
        ) i2
ON      i1.id = i2.id
WHERE   i2.id IS NULL
Quassnoi
źródło
5

Jeśli twój identyfikator jest przyrostowy, użyj czegoś takiego jak

delete from table where id < (select max(id) from table)-N
Justin Wignall
źródło
2
Jeden duży problem w tej fajnej sztuczce: seriale nie zawsze są ciągłe (na przykład, gdy wystąpiły wycofania).
bortzmeyer,
5

Aby usunąć wszystkie rekordy z wyjątkiem ostatniego N , możesz użyć zapytania przedstawionego poniżej.

Jest to jedno zapytanie, ale zawiera wiele instrukcji, więc w rzeczywistości nie jest to jedno zapytanie, tak jak było to zamierzone w pierwotnym pytaniu.

Potrzebujesz także zmiennej i wbudowanej (w zapytaniu) przygotowanej instrukcji z powodu błędu w MySQL.

Mam nadzieję, że i tak może się przydać ...

nnn to wiersze do zachowania, a tabela to tabela, nad którą pracujesz.

Zakładam, że masz rekord automatycznie zwiększający się o nazwie id

SELECT @ROWS_TO_DELETE := COUNT(*) - nnn FROM `theTable`;
SELECT @ROWS_TO_DELETE := IF(@ROWS_TO_DELETE<0,0,@ROWS_TO_DELETE);
PREPARE STMT FROM "DELETE FROM `theTable` ORDER BY `id` ASC LIMIT ?";
EXECUTE STMT USING @ROWS_TO_DELETE;

Zaletą tego podejścia jest wydajność : przetestowałem zapytanie w lokalnej bazie danych z około 13 000 rekordami, zachowując ostatnie 1000. Działa w 0,08 sekundy.

Skrypt z zaakceptowanej odpowiedzi ...

DELETE FROM `table`
WHERE id NOT IN (
  SELECT id
  FROM (
    SELECT id
    FROM `table`
    ORDER BY id DESC
    LIMIT 42 -- keep this many records
  ) foo
);

Trwa 0,55 sekundy. Około 7 razy więcej.

Środowisko testowe: mySQL 5.5.25 na MacBookPro i7 z końca 2011 roku z dyskiem SSD

Paolo
źródło
2
DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)
Dave Swersky
źródło
1
To pozostawi tylko jeden ostatni wiersz
Justin Wignall
uważam, że to najlepsze rozwiązanie!
attaboyabhipro
1

wypróbuj poniższe zapytanie:

DELETE FROM tablename WHERE id < (SELECT * FROM (SELECT (MAX(id)-10) FROM tablename ) AS a)

wewnętrzne zapytanie podrzędne zwróci 10 pierwszych wartości, a zapytanie zewnętrzne usunie wszystkie rekordy z wyjątkiem 10 pierwszych.

Nishant Nair
źródło
1
Pewne wyjaśnienie, jak to działa, byłoby korzystne dla osób, które napotkają tę odpowiedź. Zwykle nie zaleca się zrzutu kodu.
rayryeng
To nie jest poprawne z niespójnym identyfikatorem
Slava Rozhnev
0

Co powiesz na :

SELECT * FROM table del 
         LEFT JOIN table keep
         ON del.id < keep.id
         GROUP BY del.* HAVING count(*) > N;

Zwraca wiersze z więcej niż N wierszami wcześniej. Może się przydać?

Hadrien
źródło
0

W wielu przypadkach użycie identyfikatora do tego zadania nie jest możliwe. Na przykład - stół ze statusami Twittera. Oto wariant z określonym polem datownika.

delete from table 
where access_time >= 
(
    select access_time from  
    (
        select access_time from table 
            order by access_time limit 150000,1
    ) foo    
)
Aleksandra Demyanenko
źródło
0

Chciałem tylko wrzucić to do miksu dla każdego, kto używa Microsoft SQL Server zamiast MySQL. Słowo kluczowe „Limit” nie jest obsługiwane przez MSSQL, więc musisz użyć alternatywy. Ten kod działał w SQL 2008 i jest oparty na tym poście SO. https://stackoverflow.com/a/1104447/993856

-- Keep the last 10 most recent passwords for this user.
DECLARE @UserID int; SET @UserID = 1004
DECLARE @ThresholdID int -- Position of 10th password.
SELECT  @ThresholdID = UserPasswordHistoryID FROM
        (
            SELECT ROW_NUMBER()
            OVER (ORDER BY UserPasswordHistoryID DESC) AS RowNum, UserPasswordHistoryID
            FROM UserPasswordHistory
            WHERE UserID = @UserID
        ) sub
WHERE   (RowNum = 10) -- Keep this many records.

DELETE  UserPasswordHistory
WHERE   (UserID = @UserID)
        AND (UserPasswordHistoryID < @ThresholdID)

Trzeba przyznać, że to nie jest eleganckie. Jeśli jesteś w stanie zoptymalizować to pod kątem Microsoft SQL, udostępnij swoje rozwiązanie. Dzięki!

Ken Palmer
źródło
0

Jeśli chcesz usunąć rekordy również w oparciu o inną kolumnę, oto rozwiązanie:

DELETE
FROM articles
WHERE id IN
    (SELECT id
     FROM
       (SELECT id
        FROM articles
        WHERE user_id = :userId
        ORDER BY created_at DESC LIMIT 500, 10000000) abc)
  AND user_id = :userId
Nivesh Saharan
źródło
0

To również powinno działać:

DELETE FROM [table] 
INNER JOIN (
    SELECT [id] 
    FROM (
        SELECT [id] 
        FROM [table] 
        ORDER BY [id] DESC
        LIMIT N
    ) AS Temp
) AS Temp2 ON [table].[id] = [Temp2].[id]
achinda99
źródło
0
DELETE FROM table WHERE id NOT IN (
    SELECT id FROM table ORDER BY id, desc LIMIT 0, 10
)
Mike Reedell
źródło
-1

Dlaczego nie

DELETE FROM table ORDER BY id DESC LIMIT 1, 123456789

Po prostu usuń wszystkie wiersze oprócz pierwszego (kolejność to DESC!), Używając bardzo dużej liczby jako drugiego argumentu LIMIT. Spójrz tutaj

craesh
źródło
2
DELETEnie obsługuje [offset],lub OFFSET: dev.mysql.com/doc/refman/5.0/en/delete.html
Nicole
-1

Odpowiadając na to po długim czasie ... Przyszedłem do tej samej sytuacji i zamiast skorzystać z wymienionych odpowiedzi, przyszedłem poniżej -

DELETE FROM table_name order by ID limit 10

Spowoduje to usunięcie pierwszych 10 rekordów i zachowanie najnowszych rekordów.

Nitesh
źródło
Pytanie zadawano „wszystko oprócz ostatnich N rekordów” i „w jednym zapytaniu”. Ale wydaje się, że nadal potrzebujesz pierwszego zapytania, aby policzyć wszystkie rekordy w tabeli, a następnie ograniczyć do sumy - N
Paolo
@Paolo Nie wymagamy zapytania, aby policzyć wszystkie rekordy, ponieważ powyższe zapytanie usuwa wszystkie z wyjątkiem ostatnich 10 rekordów.
Nitesh
1
Nie, to zapytanie usuwa 10 najstarszych rekordów. OP chce usunąć wszystko oprócz n ostatnich rekordów. Twoje jest podstawowym rozwiązaniem, które byłoby sparowane z zapytaniem liczącym, podczas gdy OP pyta, czy istnieje sposób na połączenie wszystkiego w jedno zapytanie.
ChrisMoll,
@ChrisMoll Zgadzam się. Czy mam teraz edytować / usunąć tę odpowiedź, aby użytkownicy nie głosowali na mnie lub pozostawili ją tak, jak jest?
Nitesh