web-dev-qa-db-fra.com

Requête SQL: supprimer tous les enregistrements de la table sauf le dernier N?

Est-il possible de construire 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.

76
serg

Vous ne pouvez pas supprimer les enregistrements de cette façon, le problème principal é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 obligatoire. Sans cela, nous aurions rencontré 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 la sous-requête "LIMIT & IN/ALL/ANY/SOME" - 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.

122
Alex Barrett

Je sais que je ressuscite une assez vieille question, mais j'ai récemment rencontré ce problème, mais j'avais besoin de quelque chose qui s'adapte bien aux grands nombres . Il n'y avait pas de données de performances 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 les double sous-requête d'Alex Barrett/NOT IN méthode (similaire à Bill Karwin's ), et Quassnoi's LEFT JOIN méthode.

Malheureusement, les deux méthodes ci-dessus créent de très grandes tables temporaires intermédiaires et les performances se dégradent rapidement lorsque le nombre d'enregistrements non en cours de suppression devient important.

Ce que j'ai décidé utilise la double sous-requête d'Alex Barrett (merci!) Mais utilise <= au lieu 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 OFFSET pour obtenir l'id du Ne enregistrement et supprime cet enregistrement et tous les enregistrements précédents.

Étant donné que 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 contient un seul enregistrement au lieu des enregistrements [~ # ~] n [~ # ~] .

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 conserve 9000 (supprime les 1000 plus anciens) et le second test en conserve 50 (supprime les 9950 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 le <= la méthode permet d'obtenir de meilleures performances dans tous les domaines, mais elle s'améliore en fait plus vous en gardez, au lieu de pire.

96
Nicole

Malheureusement pour toutes les réponses données par d'autres personnes, vous ne pouvez pas DELETE et SELECT à partir 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 en charge LIMIT 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... );

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

note: Bien que cela ne fasse pas le travail dans une seule requête , parfois plus simple, get-it- solution faite est la plus efficace.

9
Bill Karwin
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
8
Quassnoi

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

delete from table where id < (select max(id) from table)-N
5
Justin Wignall

Pour supprimer tous les enregistrements sauf le 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 telle qu'elle était prévue 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 pourra être utile de toute façon ...

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

Je suppose que vous avez un enregistrement à incrémentation automatique 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 performance: J'ai testé la requête sur une base de données locale avec environ 13 000 enregistrements, en conservant les 1 000 derniers. Il s'exécute 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

4
Paolo
DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)
2
Dave Swersky

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 du top 10 et la requête externe supprimera tous les enregistrements sauf le top 10.

1
Nishant Nair

Je voulais juste mettre cela dans le mélange pour toute personne utilisant Microsoft SQL Server au lieu de MySQL. Le mot clé "Limit" 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 ce SO post. 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 êtes en mesure d'optimiser cela pour Microsoft SQL, veuillez partager votre solution. Merci!

0
Ken Palmer

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

0
Mike Reedell

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]
0
achinda99

Utiliser id pour cette tâche n'est pas une option dans de nombreux cas. Par exemple - tableau 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    
)
0
Alexander Dem'yanenko

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 contenant plus de N lignes auparavant. Pourrait être utile?

0
Hadrien

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
0
Nivesh Saharan