SQL-Abfrage: Alle Datensätze aus der Tabelle löschen, mit Ausnahme der letzten N?

89

Ist es möglich, eine einzelne MySQL-Abfrage (ohne Variablen) zu erstellen, um alle Datensätze mit Ausnahme des letzten N (sortiert nach id desc) aus der Tabelle zu entfernen?

So etwas, nur funktioniert es nicht :)

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

Vielen Dank.

serg
quelle

Antworten:

136

Sie können die Datensätze nicht auf diese Weise löschen. Das Hauptproblem besteht darin, dass Sie keine Unterabfrage verwenden können, um den Wert einer LIMIT-Klausel anzugeben.

Dies funktioniert (getestet in 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
);

Die Zwischenunterabfrage ist erforderlich. Ohne sie würden wir auf zwei Fehler stoßen:

  1. SQL-Fehler (1093): Sie können die Zieltabelle 'Tabelle' für die Aktualisierung in der FROM-Klausel nicht angeben. Mit MySQL können Sie nicht auf die Tabelle verweisen, aus der Sie in einer direkten Unterabfrage löschen.
  2. SQL-Fehler (1235): Diese Version von MySQL unterstützt die Unterabfrage 'LIMIT & IN / ALL / ANY / SOME' noch nicht. Sie können die LIMIT-Klausel nicht in einer direkten Unterabfrage eines NOT IN-Operators verwenden.

Glücklicherweise können wir durch die Verwendung einer Zwischenunterabfrage diese beiden Einschränkungen umgehen.


Nicole hat darauf hingewiesen, dass diese Abfrage für bestimmte Anwendungsfälle (wie diesen) erheblich optimiert werden kann. Ich empfehle auch, diese Antwort zu lesen , um zu sehen, ob sie zu Ihrer passt.

Alex Barrett
quelle
4
Okay, das funktioniert - aber für mich ist es unelegant und unbefriedigend, auf solche arkanen Tricks zurückgreifen zu müssen. +1 trotzdem für die antwort.
Bill Karwin
1
Ich markiere es als akzeptierte Antwort, weil es das tut, wonach ich gefragt habe. Aber ich persönlich werde es wahrscheinlich in zwei Abfragen tun, um es einfach zu halten :) Ich dachte, es gäbe vielleicht einen schnellen und einfachen Weg.
Serg
1
Danke Alex, deine Antwort hat mir geholfen. Ich sehe, dass die Zwischenunterabfrage erforderlich ist, aber ich verstehe nicht warum. Hast du eine Erklärung dafür?
Sv1
8
eine frage: wofür ist das "foo"?
Sebastian Breit
9
Perroloco, ich habe es ohne foo versucht und diesen Fehler erhalten: FEHLER 1248 (42000): Jede abgeleitete Tabelle muss ihren eigenen Alias ​​haben. Also unsere Antwort, jede abgeleitete Tabelle muss ihren eigenen Alias ​​haben!
Codygman
105

Ich weiß, dass ich eine ziemlich alte Frage wiederbelebe, aber ich bin kürzlich auf dieses Problem gestoßen, brauchte aber etwas, das sich gut auf große Zahlen skalieren lässt . Es gab keine vorhandenen Leistungsdaten, und da diese Frage einige Aufmerksamkeit auf sich gezogen hat, dachte ich, ich würde das posten, was ich gefunden habe.

Die Lösungen, die tatsächlich funktionierten, waren die doppelte NOT INUnterabfrage / Methode von Alex Barrett (ähnlich der von Bill Karwin ) und dieLEFT JOIN Methode von Quassnoi .

Leider erstellen beide oben genannten Methoden sehr große temporäre Zwischentabellen, und die Leistung nimmt schnell ab, da die Anzahl der nicht gelöschten Datensätze groß wird.

Was ich beschlossen habe, verwendet Alex Barretts doppelte Unterabfrage (danke!), Verwendet aber <=anstelle von 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
  )

Es wird verwendet OFFSET, um die ID des N- ten Datensatzes abzurufen und diesen Datensatz und alle vorherigen Datensätze zu löschen.

Da die Bestellung bereits eine Annahme dieses Problems ist ( ORDER BY id DESC), <=passt sie perfekt.

Dies ist viel schneller, da die von der Unterabfrage generierte temporäre Tabelle nur einen Datensatz anstelle von N Datensätzen enthält.

Testfall

Ich habe die drei Arbeitsmethoden und die neue Methode oben in zwei Testfällen getestet.

Beide Testfälle verwenden 10000 vorhandene Zeilen, während der erste Test 9000 (löscht die ältesten 1000) und der zweite Test 50 (löscht die ältesten 9950) behält.

+-----------+------------------------+----------------------+
|           | 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 |
+-----------+------------------------+----------------------+

Interessant ist, dass die <=Methode auf der ganzen Linie eine bessere Leistung erzielt, aber tatsächlich besser wird, je mehr Sie behalten, anstatt schlechter.

Nicole
quelle
11
Ich lese diesen Thread 4,5 Jahre später wieder. Schöne Ergänzung!
Alex Barrett
Wow, das sieht gut aus, funktioniert aber nicht in Microsoft SQL 2008. Ich erhalte die folgende Meldung: "Falsche Syntax in der Nähe von 'Limit'. Es ist schön, dass es in MySQL funktioniert, aber ich muss eine alternative Lösung finden.
Ken Palmer
1
@ KenPalmer Sie sollten in der Lage sein, immer noch einen bestimmten Zeilenversatz zu finden, indem Sie Folgendes verwenden ROW_NUMBER(): stackoverflow.com/questions/603724/…
Nicole
3
@ KenPalmer verwenden SELECT TOP anstelle von LIMIT, wenn Sie zwischen SQL und mySQL
wechseln
1
Prost dafür. Die Abfrage für meinen (sehr großen) Datensatz wurde von 12 Minuten auf 3,64 Sekunden reduziert!
Lieuwe
10

Leider können Sie für alle Antworten anderer Leute nicht DELETEund SELECTaus einer bestimmten Tabelle in derselben Abfrage.

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

MySQL kann auch LIMITkeine Unterabfrage unterstützen. Dies sind Einschränkungen von 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'

Die beste Antwort, die ich finden kann, ist, dies in zwei Schritten zu tun:

SELECT id FROM mytable ORDER BY id DESC LIMIT n; 

Sammeln Sie die IDs und machen Sie sie zu einer durch Kommas getrennten Zeichenfolge:

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

(Normalerweise birgt das Interpolieren einer durch Kommas getrennten Liste in eine SQL-Anweisung ein gewisses Risiko für eine SQL-Injection. In diesem Fall stammen die Werte jedoch nicht aus einer nicht vertrauenswürdigen Quelle, sondern sind ganzzahlige Werte aus der Datenbank selbst.)

Hinweis: Obwohl dies nicht in einer einzigen Abfrage erledigt wird , ist manchmal eine einfachere Lösung am effektivsten.

Bill Karwin
quelle
Sie können jedoch innere Verknüpfungen zwischen Löschen und Auswählen vornehmen. Was ich unten getan habe, sollte funktionieren.
Achinda99
Sie müssen eine Zwischenunterabfrage verwenden, damit LIMIT in der Unterabfrage funktioniert.
Alex Barrett
@ achinda99: Ich sehe keine Antwort von dir auf diesen Thread ...?
Bill Karwin
Ich wurde für ein Treffen gezogen. Mein Fehler. Ich habe momentan keine Testumgebung, um das von mir geschriebene SQL zu testen, aber ich habe sowohl das getan, was Alex Barret getan hat, als auch es dazu gebracht, mit einem inneren Join zu arbeiten.
Achinda99
Es ist eine dumme Einschränkung von MySQL. Funktioniert mit PostgreSQL DELETE FROM mytable WHERE id NOT IN (SELECT id FROM mytable ORDER BY id DESC LIMIT 3);einwandfrei.
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
quelle
5

Wenn Ihre ID inkrementell ist, verwenden Sie so etwas wie

delete from table where id < (select max(id) from table)-N
Justin Wignall
quelle
2
Ein großes Problem bei diesem schönen Trick: Serien sind nicht immer zusammenhängend (zum Beispiel bei Rollbacks).
Bortzmeyer
5

Um alle Datensätze mit Ausnahme des letzten N zu löschen , können Sie die unten angegebene Abfrage verwenden.

Es ist eine einzelne Abfrage, aber mit vielen Anweisungen, so dass es eigentlich keine einzelne Abfrage ist, wie es in der ursprünglichen Frage beabsichtigt war.

Außerdem benötigen Sie aufgrund eines Fehlers in MySQL eine Variable und eine integrierte (in der Abfrage) vorbereitete Anweisung.

Hoffe es kann trotzdem nützlich sein ...

nnn sind die zu behaltenden Zeilen und theTable ist die Tabelle, an der Sie arbeiten.

Ich gehe davon aus, dass Sie einen automatisch inkrementierenden Datensatz mit dem Namen id haben

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;

Das Gute an diesem Ansatz ist die Leistung : Ich habe die Abfrage in einer lokalen Datenbank mit etwa 13.000 Datensätzen getestet, wobei die letzten 1.000 beibehalten wurden. Es läuft in 0,08 Sekunden.

Das Skript aus der akzeptierten Antwort ...

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
);

Dauert 0,55 Sekunden. Etwa 7 mal mehr.

Testumgebung: mySQL 5.5.25 auf einem i7 MacBookPro mit SSD Ende 2011

Paolo
quelle
2
DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)
Dave Swersky
quelle
1
Dies wird nur eine letzte Zeile verlassen
Justin Wignall
Das ist die beste Lösung, die ich denke!
Attaboyabhipro
1

versuchen Sie unten Abfrage:

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

Die innere Unterabfrage gibt den Top-10-Wert zurück und die äußere Abfrage löscht alle Datensätze mit Ausnahme der Top-10.

Nishant Nair
quelle
1
Eine Erklärung, wie dies funktioniert, wäre für diejenigen von Vorteil, die auf diese Antwort stoßen. Code-Dumping wird normalerweise nicht empfohlen.
Rayryeng
0

DELETE FROM table WHERE id NICHT IN (SELECT id FROM table ORDER BY id, desc LIMIT 0, 10)

Mike Reedell
quelle
0

Dies sollte auch funktionieren:

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
quelle
0

Wie wäre es mit :

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

Es werden Zeilen mit mehr als N Zeilen zurückgegeben. Könnte nützlich sein?

Hadrien
quelle
0

Die Verwendung von id für diese Aufgabe ist in vielen Fällen keine Option. Zum Beispiel - Tabelle mit Twitter-Status. Hier ist eine Variante mit angegebenem Zeitstempelfeld.

delete from table 
where access_time >= 
(
    select access_time from  
    (
        select access_time from table 
            order by access_time limit 150000,1
    ) foo    
)
Alexander Dem'yanenko
quelle
0

Ich wollte dies nur für jeden in den Mix werfen, der Microsoft SQL Server anstelle von MySQL verwendet. Das Schlüsselwort 'Limit' wird von MSSQL nicht unterstützt, daher müssen Sie eine Alternative verwenden. Dieser Code funktionierte in SQL 2008 und basiert auf diesem SO-Beitrag. 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)

Zugegeben, das ist nicht elegant. Wenn Sie dies für Microsoft SQL optimieren können, teilen Sie Ihre Lösung mit. Vielen Dank!

Ken Palmer
quelle
0

Wenn Sie die Datensätze auch basierend auf einer anderen Spalte löschen müssen, finden Sie hier eine Lösung:

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

Warum nicht

DELETE FROM table ORDER BY id DESC LIMIT 1, 123456789

Löschen Sie einfach alle bis auf die erste Zeile (Reihenfolge ist DESC!) Und verwenden Sie eine sehr sehr große Zahl als zweites LIMIT-Argument. Siehe hier

Craesh
quelle
2
DELETEunterstützt nicht [offset],oder OFFSET: dev.mysql.com/doc/refman/5.0/en/delete.html
Nicole
-1

Nach langer Zeit darauf zu antworten ... Kam über die gleiche Situation und anstatt die genannten Antworten zu verwenden, kam ich mit -

DELETE FROM table_name order by ID limit 10

Dadurch werden die ersten 10 Datensätze gelöscht und die neuesten Datensätze aufbewahrt.

Nitesh
quelle
Die Frage lautete "alle außer den letzten N Datensätzen" und "in einer einzigen Abfrage". Aber es scheint, dass Sie noch eine erste Abfrage benötigen, um alle Datensätze in der Tabelle zu zählen und dann auf total - N
Paolo
@Paolo Wir benötigen keine Abfrage, um alle Datensätze zu zählen, da die obige Abfrage alle außer den letzten 10 Datensätzen löscht.
Nitesh
1
Nein, diese Abfrage löscht die 10 ältesten Datensätze. Das OP möchte alles außer den n letzten Datensätzen löschen. Ihre ist die grundlegende Lösung, die mit einer Zählabfrage gepaart werden würde, während OP fragt, ob es eine Möglichkeit gibt, alles in einer einzigen Abfrage zu kombinieren.
ChrisMoll
@ ChrisMoll Ich stimme zu. Soll ich diese Antwort jetzt bearbeiten / löschen, damit Benutzer mich nicht abstimmen oder so lassen, wie sie ist?
Nitesh