Query SQL: eliminare tutti i record dalla tabella tranne l'ultimo N?


91

È possibile costruire una singola query mysql (senza variabili) per rimuovere tutti i record dalla tabella, eccetto gli ultimi N (ordinati per id desc)?

Qualcosa di simile, solo che non funziona :)

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

Grazie.

Risposte:


141

Non è possibile eliminare i record in questo modo, il problema principale è che non è possibile utilizzare una sottoquery per specificare il valore di una clausola LIMIT.

Funziona (testato 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
);

La sottoquery intermedia è obbligatoria. Senza di esso incorreremmo in due errori:

  1. Errore SQL (1093): non è possibile specificare la tabella di destinazione "tabella" per l'aggiornamento nella clausola FROM - MySQL non consente di fare riferimento alla tabella che si sta eliminando da una sottoquery diretta.
  2. Errore SQL (1235): questa versione di MySQL non supporta ancora "LIMIT & IN / ALL / ANY / SOME sottoquery" : non è possibile utilizzare la clausola LIMIT all'interno di una sottoquery diretta di un operatore NOT IN.

Fortunatamente, l'utilizzo di una sottoquery intermedia ci consente di aggirare entrambe queste limitazioni.


Nicole ha sottolineato che questa query può essere ottimizzata in modo significativo per alcuni casi d'uso (come questo). Consiglio di leggere anche quella risposta per vedere se si adatta alla tua.


4
Ok, funziona, ma per me è inelegante e insoddisfacente dover ricorrere a trucchi arcani come quello. +1 comunque per la risposta.
Bill Karwin,

1
La contrassegno come risposta accettata, perché fa quello che ho chiesto. Ma io personalmente lo farò probabilmente in due domande solo per mantenerlo semplice :) Ho pensato che forse ci fosse un modo semplice e veloce.
serg

1
Grazie Alex, la tua risposta mi ha aiutato. Vedo che è richiesta la sottoquery intermedia ma non capisco perché. Hai una spiegazione per questo?
Sv1

9
una domanda: a cosa serve il "foo"?
Sebastian Breit

9
Perroloco, ho provato senza pippo e ho ricevuto questo errore: ERRORE 1248 (42000): Ogni tabella derivata deve avere il proprio alias Quindi la nostra risposta, ogni tabella derivata deve avere il proprio alias!
codygman

109

So che sto resuscitando una vecchia domanda, ma recentemente mi sono imbattuto in questo problema, ma avevo bisogno di qualcosa che si adattasse bene a grandi numeri . Non c'erano dati sulle prestazioni esistenti e, poiché questa domanda ha ricevuto un po 'di attenzione, ho pensato di pubblicare ciò che ho trovato.

Le soluzioni che hanno effettivamente funzionato erano la doppia NOT INsottoquery / metodo di Alex Barrett (simile a quello di Bill Karwin ) e ilLEFT JOIN metodo di Quassnoi .

Sfortunatamente, entrambi i metodi precedenti creano tabelle temporanee intermedie molto grandi e le prestazioni diminuiscono rapidamente man mano che il numero di record non eliminati aumenta.

Quello che ho scelto utilizza la doppia sottoquery di Alex Barrett (grazie!) Ma usa <=invece di 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
  )

Viene utilizzato OFFSETper ottenere l'ID dell'N- esimo record ed elimina quel record e tutti i record precedenti.

Poiché l'ordinazione è già un presupposto di questo problema ( ORDER BY id DESC), <=è perfetto.

È molto più veloce, poiché la tabella temporanea generata dalla sottoquery contiene solo un record invece di N record.

Scenario di prova

Ho testato i tre metodi di lavoro e il nuovo metodo sopra in due casi di test.

Entrambi i casi di test utilizzano 10000 righe esistenti, mentre il primo test ne mantiene 9000 (elimina le 1000 meno recenti) e il secondo ne mantiene 50 (elimina le 9950 meno recenti).

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

La cosa interessante è che il <=metodo vede prestazioni migliori su tutta la linea, ma in realtà migliora più mantieni, anziché peggio.


11
Sto leggendo di nuovo questo thread 4,5 anni dopo. Bella aggiunta!
Alex Barrett

Wow, sembra fantastico ma non funziona in Microsoft SQL 2008. Ricevo questo messaggio: "Sintassi non corretta vicino a" Limite ". È bello che funzioni in MySQL, ma dovrò trovare una soluzione alternativa.
Ken Palmer

1
@KenPalmer Dovresti essere ancora in grado di trovare uno specifico offset di riga usando ROW_NUMBER(): stackoverflow.com/questions/603724/…
Nicole

3
@KenPalmer usa SELECT TOP invece di LIMIT quando si passa da SQL a mySQL
Alpha G33k

1
Grazie per questo. Ha ridotto la query sul mio set di dati (molto grande) da 12 minuti a 3,64 secondi!
Lieuwe

10

Sfortunatamente per tutte le risposte fornite da altre persone, non puoi DELETEe SELECTda una data tabella nella stessa query.

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

Né MySQL può supportare LIMITin una sottoquery. Queste sono limitazioni di 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 migliore risposta che posso trovare è farlo in due fasi:

SELECT id FROM mytable ORDER BY id DESC LIMIT n; 

Raccogli gli ID e trasformali in una stringa separata da virgole:

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

(Normalmente l'interpolazione di un elenco separato da virgole in un'istruzione SQL introduce alcuni rischi di iniezione SQL, ma in questo caso i valori non provengono da una fonte non attendibile, sono noti come valori interi dal database stesso.)

nota: sebbene questo non porti a termine il lavoro in una singola query, a volte una soluzione più semplice e completa è la più efficace.


Ma puoi eseguire join interni tra un'eliminazione e una selezione. Quello che ho fatto di seguito dovrebbe funzionare.
achinda99

È necessario utilizzare una sottoquery intermedia per far funzionare LIMIT nella sottoquery.
Alex Barrett

@ achinda99: non vedo una tua risposta in questo thread ...?
Bill Karwin,

Sono stato attratto per una riunione. Colpa mia. Non ho un ambiente di test in questo momento per testare lo sql che ho scritto, ma ho fatto sia quello che ha fatto Alex Barret sia l'ho fatto funzionare con un join interno.
achinda99

È una stupida limitazione di MySQL. Con PostgreSQL, DELETE FROM mytable WHERE id NOT IN (SELECT id FROM mytable ORDER BY id DESC LIMIT 3);funziona bene.
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

5

Se il tuo ID è incrementale, usa qualcosa come

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

2
Un grosso problema in questo bel trucco: i periodici non sono sempre contigui (ad esempio quando c'erano i rollback).
bortzmeyer

5

Per cancellare tutti i record tranne l'ultimo N puoi usare la query riportata di seguito.

È una singola query ma con molte affermazioni, quindi in realtà non è una singola query nel modo in cui era intesa nella domanda originale.

Inoltre è necessaria una variabile e un'istruzione preparata incorporata (nella query) a causa di un bug in MySQL.

Spero che possa essere utile comunque ...

nnn sono le righe da mantenere e theTable è la tabella si sta lavorando.

Presumo che tu abbia un record con incremento automatico denominato 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 cosa buona di questo approccio sono le prestazioni : ho testato la query su un DB locale con circa 13.000 record, mantenendo gli ultimi 1.000. Funziona in 0,08 secondi.

Il copione dalla risposta accettata ...

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

Richiede 0,55 secondi. Circa 7 volte di più.

Ambiente di test: mySQL 5.5.25 su un MacBookPro i7 di fine 2011 con SSD



1

prova sotto la query:

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

la sottoquery interna restituirà il valore dei primi 10 e la query esterna eliminerà tutti i record tranne i primi 10.


1
Qualche spiegazione su come funziona sarebbe utile per coloro che si imbattono in questa risposta. Il code dump di solito non è raccomandato.
rayryeng

0

Che dire :

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

Restituisce righe con più di N righe prima. Potrebbe essere utile?


0

L'utilizzo di id per questa attività non è un'opzione in molti casi. Ad esempio: tabella con stati di Twitter. Ecco una variante con il campo timestamp specificato.

delete from table 
where access_time >= 
(
    select access_time from  
    (
        select access_time from table 
            order by access_time limit 150000,1
    ) foo    
)

0

Volevo solo metterlo nel mix per chiunque utilizzi Microsoft SQL Server invece di MySQL. La parola chiave "Limite" non è supportata da MSSQL, quindi dovrai utilizzare un'alternativa. Questo codice ha funzionato in SQL 2008 e si basa su questo post 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)

Certo, questo non è elegante. Se sei in grado di ottimizzarlo per Microsoft SQL, condividi la tua soluzione. Grazie!


0

Se è necessario eliminare i record anche in base a qualche altra colonna, ecco una soluzione:

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

Questo dovrebbe funzionare anche:

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


-1

Rispondendo dopo molto tempo ... Mi sono imbattuto nella stessa situazione e invece di usare le risposte menzionate, sono venuto con di seguito -

DELETE FROM table_name order by ID limit 10

Ciò eliminerà i primi 10 record e manterrà gli ultimi record.


La domanda ha chiesto "tutti tranne gli ultimi N record" e "in una singola query". Ma sembra che sia ancora necessaria una prima query per contare tutti i record nella tabella e poi limitare al totale - N
Paolo

@Paolo Non è necessaria una query per contare tutti i record poiché la query precedente elimina tutti tranne gli ultimi 10 record.
Nitesh

1
No, quella query elimina i 10 record più vecchi. L'OP vuole cancellare tutto tranne gli n record più recenti. La tua è la soluzione di base che sarebbe accoppiata a una query di conteggio, mentre OP chiede se c'è un modo per combinare tutto in una singola query.
ChrisMoll

@ChrisMoll Sono d'accordo. Devo modificare / eliminare questa risposta ora per consentire agli utenti di non votarmi o lasciarla così com'è?
Nitesh
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.