Requête SQL: supprimer tous les enregistrements de la table à l'exception du dernier N?

90

Est-il possible de créer une seule requête mysql (sans variables) pour supprimer tous les enregistrements de la table, à l'exception du dernier N (trié par id desc)?

Quelque chose comme ça, mais ça ne marche pas :)

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

Merci.

serg
la source

Réponses:

140

Vous ne pouvez pas supprimer les enregistrements de cette façon, le principal problème étant que vous ne pouvez pas utiliser une sous-requête pour spécifier la valeur d'une clause LIMIT.

Cela fonctionne (testé dans 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
);

La sous - requête intermédiaire est requise. Sans cela, nous nous heurterions à deux erreurs:

  1. Erreur SQL (1093): vous ne pouvez pas spécifier la table cible 'table' pour la mise à jour dans la clause FROM - MySQL ne vous permet pas de faire référence à la table que vous supprimez dans une sous-requête directe.
  2. Erreur SQL (1235): Cette version de MySQL ne prend pas encore en charge «LIMIT & IN / ALL / ANY / SOME sous-requête» - Vous ne pouvez pas utiliser la clause LIMIT dans une sous-requête directe d'un opérateur NOT IN.

Heureusement, l'utilisation d'une sous-requête intermédiaire nous permet de contourner ces deux limitations.


Nicole a souligné que cette requête peut être optimisée de manière significative pour certains cas d'utilisation (comme celui-ci). Je recommande également de lire cette réponse pour voir si elle correspond à la vôtre.

Alex Barrett
la source
4
D'accord, cela fonctionne - mais pour moi, c'est inélégant et insatisfaisant de devoir recourir à des trucs obscurs comme ça. +1 néanmoins pour la réponse.
Bill Karwin
1
Je la marque comme une réponse acceptée, car elle fait ce que j'ai demandé. Mais personnellement, je le ferai probablement en deux requêtes juste pour rester simple :) J'ai pensé qu'il y avait peut-être un moyen rapide et facile.
serg
1
Merci Alex, votre réponse m'a aidé. Je vois que la sous-requête intermédiaire est requise mais je ne comprends pas pourquoi. Avez-vous une explication à cela?
Sv1
8
une question: à quoi sert le "foo"?
Sebastian Breit
9
Perroloco, j'ai essayé sans foo et j'ai obtenu cette erreur: ERREUR 1248 (42000): Chaque table dérivée doit avoir son propre alias Alors leur réponse, chaque table dérivée doit avoir son propre alias!
codygman
106

Je sais que je ressuscite une question assez ancienne, mais j'ai récemment rencontré ce problème, mais j'avais besoin de quelque chose qui s'adapte bien à de grands nombres . Il n'y avait pas de données de performance existantes, et comme cette question a suscité beaucoup d'attention, j'ai pensé publier ce que j'ai trouvé.

Les solutions qui ont réellement fonctionné étaient la double sous-requête /NOT IN méthode d' Alex Barrett (similaire à celle de Bill Karwin ) et laLEFT JOIN méthode de Quassnoi .

Malheureusement, les deux méthodes ci-dessus créent de très grandes tables temporaires intermédiaires et les performances se dégradent rapidement à mesure que le nombre d'enregistrements non supprimés augmente.

Ce sur quoi j'ai choisi utilise la double sous-requête d'Alex Barrett (merci!) Mais utilise à la <=place de 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
  )

Il utilise OFFSETpour obtenir l'ID du N ème enregistrement et supprime cet enregistrement et tous les enregistrements précédents.

Puisque la commande est déjà une hypothèse de ce problème ( ORDER BY id DESC), <=est un ajustement parfait.

C'est beaucoup plus rapide, car la table temporaire générée par la sous-requête ne contient qu'un seul enregistrement au lieu de N enregistrements.

Cas de test

J'ai testé les trois méthodes de travail et la nouvelle méthode ci-dessus dans deux cas de test.

Les deux cas de test utilisent 10000 lignes existantes, tandis que le premier test en conserve 9000 (supprime les 1000 plus anciens) et le deuxième test en conserve 50 (supprime les 9950 les plus anciens).

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

Ce qui est intéressant, c'est que la <=méthode voit de meilleures performances dans tous les domaines, mais s'améliore à mesure que vous en conservez, au lieu de pire.

Nicole
la source
11
Je relis ce fil 4,5 ans plus tard. Bel ajout!
Alex Barrett
Wow, cela a l'air génial mais ne fonctionne pas dans Microsoft SQL 2008. Je reçois ce message: "Syntaxe incorrecte près de 'Limit'. C'est bien que cela fonctionne dans MySQL, mais je vais devoir trouver une solution alternative.
Ken Palmer
1
@KenPalmer Vous devriez toujours pouvoir trouver un décalage de ligne spécifique en utilisant ROW_NUMBER(): stackoverflow.com/questions/603724/...
Nicole
3
@KenPalmer utilise SELECT TOP au lieu de LIMIT lors du basculement entre SQL et mySQL
Alpha G33k
1
Bravo pour ça. Cela a réduit la requête sur mon (très grand) jeu de données de 12 minutes à 3,64 secondes!
Lieuwe
10

Malheureusement pour toutes les réponses données par d'autres personnes, vous ne pouvez pas DELETEet à SELECTpartir d'une table donnée dans la même requête.

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 ne peut pas non plus prendre LIMITen charge dans une sous-requête. Ce sont des limitations de 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'

La meilleure réponse que je puisse trouver est de le faire en deux étapes:

SELECT id FROM mytable ORDER BY id DESC LIMIT n; 

Collectez les identifiants et transformez-les en une chaîne séparée par des virgules:

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

(Normalement, l'interpolation d'une liste séparée par des virgules dans une instruction SQL présente un certain risque d'injection SQL, mais dans ce cas, les valeurs ne proviennent pas d'une source non approuvée, elles sont connues pour être des valeurs entières de la base de données elle-même.)

Remarque: bien que cela ne fasse pas le travail en une seule requête, une solution plus simple et plus efficace est parfois la plus efficace.

Bill Karwin
la source
Mais vous pouvez faire des jointures internes entre une suppression et une sélection. Ce que j'ai fait ci-dessous devrait fonctionner.
achinda99
Vous devez utiliser une sous-requête intermédiaire pour que LIMIT fonctionne dans la sous-requête.
Alex Barrett
@ achinda99: Je ne vois pas de réponse de votre part sur ce fil ...?
Bill Karwin
J'ai été tiré pour une réunion. Ma faute. Je n'ai pas d'environnement de test pour le moment pour tester le sql que j'ai écrit, mais j'ai fait ce qu'Alex Barret a fait et je l'ai fait fonctionner avec une jointure interne.
achinda99
C'est une limitation stupide de MySQL. Avec PostgreSQL, DELETE FROM mytable WHERE id NOT IN (SELECT id FROM mytable ORDER BY id DESC LIMIT 3);fonctionne très bien.
bortzmeyer le
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
la source
5

Si votre identifiant est incrémentiel, utilisez quelque chose comme

delete from table where id < (select max(id) from table)-N
Justin Wignall
la source
2
Un gros problème dans cette belle astuce: les séries ne sont pas toujours contiguës (par exemple quand il y avait des rollbacks).
bortzmeyer le
5

Pour supprimer tous les enregistrements à l'exception du dernier N, vous pouvez utiliser la requête indiquée ci-dessous.

Il s'agit d'une seule requête mais avec de nombreuses instructions, ce n'est donc pas une seule requête comme prévu dans la question d'origine.

Vous avez également besoin d'une variable et d'une instruction préparée intégrée (dans la requête) en raison d'un bogue dans MySQL.

J'espère que cela peut être utile de toute façon ...

nnn sont les lignes à garder et theTable est la table sur laquelle vous travaillez.

Je suppose que vous avez un enregistrement à auto-incrémentation nommé 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;

La bonne chose à propos de cette approche est la performance : j'ai testé la requête sur une base de données locale avec environ 13000 enregistrements, en conservant les 1000 derniers. Il fonctionne en 0,08 seconde.

Le script de la réponse acceptée ...

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

Prend 0,55 seconde. Environ 7 fois plus.

Environnement de test: mySQL 5.5.25 sur un MacBookPro i7 fin 2011 avec SSD

Paolo
la source
2
DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)
Dave Swersky
la source
1
Cela ne laissera qu'une seule dernière ligne
Justin Wignall
c'est la meilleure solution que je pense!
attaboyabhipro
1

essayez ci-dessous la requête:

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

la sous-requête interne renverra la valeur des 10 premiers et la requête externe supprimera tous les enregistrements sauf les 10 premiers.

Nishant Nair
la source
1
Une explication sur la façon dont cela fonctionne serait bénéfique pour ceux qui rencontrent cette réponse. Le vidage de code n'est généralement pas recommandé.
rayryeng
Ce n'est pas correct avec un identifiant non cohérent
Slava Rozhnev
0

Qu'en est-il de :

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

Il renvoie des lignes avec plus de N lignes auparavant. Cela pourrait être utile?

Hadrien
la source
0

L'utilisation de id pour cette tâche n'est pas une option dans de nombreux cas. Par exemple - table avec les statuts Twitter. Voici une variante avec un champ d'horodatage spécifié.

delete from table 
where access_time >= 
(
    select access_time from  
    (
        select access_time from table 
            order by access_time limit 150000,1
    ) foo    
)
Alexandre Demyanenko
la source
0

Je voulais juste ajouter cela à tous ceux qui utilisent Microsoft SQL Server au lieu de MySQL. Le mot-clé «Limite» n'est pas pris en charge par MSSQL, vous devrez donc utiliser une alternative. Ce code a fonctionné dans SQL 2008 et est basé sur cette publication 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)

Certes, ce n'est pas élégant. Si vous pouvez optimiser cela pour Microsoft SQL, veuillez partager votre solution. Merci!

Ken Palmer
la source
0

Si vous devez également supprimer les enregistrements basés sur une autre colonne, voici une solution:

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 Saharien
la source
0

Cela devrait également fonctionner:

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
la source
0
DELETE FROM table WHERE id NOT IN (
    SELECT id FROM table ORDER BY id, desc LIMIT 0, 10
)
Mike Reedell
la source
-1

Pourquoi pas

DELETE FROM table ORDER BY id DESC LIMIT 1, 123456789

Supprimez tout sauf la première ligne (l'ordre est DESC!), En utilisant un très très grand nombre comme deuxième argument LIMIT. Vois ici

fou
la source
2
DELETEne prend pas en charge [offset],ou OFFSET: dev.mysql.com/doc/refman/5.0/en/delete.html
Nicole
-1

Répondre à cela après un long moment ... Je suis tombé sur la même situation et au lieu d'utiliser les réponses mentionnées, je suis venu avec ci-dessous -

DELETE FROM table_name order by ID limit 10

Cela supprimera les 10 premiers enregistrements et conservera les derniers enregistrements.

Nitesh
la source
La question demandait «tous sauf les N derniers enregistrements» et «en une seule requête». Mais il semble que vous ayez encore besoin d'une première requête pour compter tous les enregistrements de la table, puis limiter au total - N
Paolo
@Paolo Nous n'avons pas besoin d'une requête pour compter tous les enregistrements car la requête ci-dessus supprime tous sauf les 10 derniers enregistrements.
Nitesh
1
Non, cette requête supprime les 10 enregistrements les plus anciens. L'OP veut tout supprimer sauf les n enregistrements les plus récents. La vôtre est la solution de base qui serait associée à une requête de comptage, tandis que OP demande s'il existe un moyen de tout combiner en une seule requête.
ChrisMoll
@ChrisMoll Je suis d'accord. Dois-je modifier / supprimer cette réponse maintenant pour permettre aux utilisateurs de ne pas voter contre moi ou de la laisser telle quelle?
Nitesh