Consulta SQL: ¿Eliminar todos los registros de la tabla excepto el último N?

90

¿Es posible construir una sola consulta mysql (sin variables) para eliminar todos los registros de la tabla, excepto la última N (ordenada por id desc)?

Algo como esto, solo que no funciona :)

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

Gracias.

serg
fuente

Respuestas:

140

No puede eliminar los registros de esa manera, el problema principal es que no puede usar una subconsulta para especificar el valor de una cláusula LIMIT.

Esto funciona (probado en 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 subconsulta intermedio se requiere. Sin él, nos encontraríamos con dos errores:

  1. Error de SQL (1093): no puede especificar la tabla de destino 'tabla' para la actualización en la cláusula FROM ; MySQL no le permite consultar la tabla que está eliminando desde una subconsulta directa.
  2. Error de SQL (1235): esta versión de MySQL aún no admite la subconsulta 'LIMIT & IN / ALL / ANY / SOME' . No puede usar la cláusula LIMIT dentro de una subconsulta directa de un operador NOT IN.

Afortunadamente, el uso de una subconsulta intermedia nos permite evitar estas dos limitaciones.


Nicole ha señalado que esta consulta se puede optimizar significativamente para ciertos casos de uso (como este). Recomiendo leer esa respuesta también para ver si se ajusta a la suya.

Alex Barrett
fuente
4
Está bien, eso funciona, pero para mí, es poco elegante e insatisfactorio tener que recurrir a trucos arcanos como ese. +1 sin embargo por la respuesta.
Bill Karwin
1
Lo marco como una respuesta aceptada, porque hace lo que le pedí. Pero personalmente lo haré probablemente en dos consultas solo para que sea simple :) Pensé que tal vez había una manera rápida y fácil.
serg
1
Gracias Alex, tu respuesta me ayudó. Veo que se requiere la subconsulta intermedia pero no entiendo por qué. ¿Tiene una explicación para eso?
Sv1
8
una pregunta: ¿para qué sirve el "foo"?
Sebastian Breit
9
Perroloco, lo intenté sin foo y obtuve este error: ERROR 1248 (42000): Cada tabla derivada debe tener su propio alias. Así que la suya es nuestra respuesta, ¡cada tabla derivada debe tener su propio alias!
codygman
106

Sé que estoy resucitando una pregunta bastante antigua, pero recientemente me encontré con este problema, pero necesitaba algo que se adapte bien a grandes números . No había datos de rendimiento existentes, y dado que esta pregunta ha recibido bastante atención, pensé en publicar lo que encontré.

Las soluciones que realmente funcionaron fueron el método / NOT INsubconsulta doble de Alex Barrett (similar al de Bill Karwin ) y elLEFT JOIN método de Quassnoi .

Desafortunadamente, los dos métodos anteriores crean tablas temporales intermedias muy grandes y el rendimiento se degrada rápidamente a medida que aumenta la cantidad de registros que no se eliminan.

Lo que me decidí utiliza la doble subconsulta de Alex Barrett (¡gracias!) Pero usa en <=lugar 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
  )

Se utiliza OFFSETpara obtener la identificación del registro N y elimina ese registro y todos los registros anteriores.

Dado que ordenar ya es una suposición de este problema ( ORDER BY id DESC), <=es un ajuste perfecto.

Es mucho más rápido, ya que la tabla temporal generada por la subconsulta contiene solo un registro en lugar de N registros.

Caso de prueba

Probé los tres métodos de trabajo y el nuevo método anterior en dos casos de prueba.

Ambos casos de prueba usan 10000 filas existentes, mientras que la primera prueba conserva 9000 (elimina las 1000 más antiguas) y la segunda prueba 50 (elimina las 9950 más antiguas).

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

Lo interesante es que el <=método ve un mejor rendimiento en todos los ámbitos, pero en realidad mejora cuanto más se conserva, en lugar de peor.

Nicole
fuente
11
Estoy leyendo este hilo de nuevo 4.5 años después. ¡Buena adición!
Alex Barrett
Vaya, esto se ve muy bien pero no funciona en Microsoft SQL 2008. Aparece este mensaje: "Sintaxis incorrecta cerca de 'Límite'. Es bueno que funcione en MySQL, pero tendré que encontrar una solución alternativa.
Ken Palmer
1
@KenPalmer Debería poder encontrar un desplazamiento de fila específico usando ROW_NUMBER(): stackoverflow.com/questions/603724/…
Nicole
3
@KenPalmer usa SELECT TOP en lugar de LIMIT al cambiar entre SQL y mySQL
Alpha G33k
1
Saludos por eso. ¡Redujo la consulta en mi conjunto de datos (muy grande) de 12 minutos a 3,64 segundos!
Lieuwe
10

Desafortunadamente, para todas las respuestas dadas por otras personas, no puede DELETEy SELECTde una tabla determinada en la misma consulta.

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 tampoco puede admitir LIMITen una subconsulta. Estas son las limitaciones 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 mejor respuesta que se me ocurre es hacer esto en dos etapas:

SELECT id FROM mytable ORDER BY id DESC LIMIT n; 

Recoge los ID y conviértelos en una cadena separada por comas:

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

(Normalmente, la interpolación de una lista separada por comas en una declaración SQL introduce cierto riesgo de inyección SQL, pero en este caso los valores no provienen de una fuente no confiable, se sabe que son valores enteros de la propia base de datos).

nota: aunque esto no hace el trabajo en una sola consulta, a veces una solución más simple y práctica es la más efectiva.

Bill Karwin
fuente
Pero puede hacer uniones internas entre eliminar y seleccionar. Lo que hice a continuación debería funcionar.
achinda99
Debe usar una subconsulta intermedia para que LIMIT funcione en la subconsulta.
Alex Barrett
@ achinda99: ¿No veo una respuesta tuya en este hilo ...?
Bill Karwin
Me llamaron para una reunión. Culpa mía. No tengo un entorno de prueba en este momento para probar el sql que escribí, pero hice tanto lo que hizo Alex Barret como lo hice funcionar con una combinación interna.
achinda99
Es una limitación estúpida de MySQL. Con PostgreSQL, DELETE FROM mytable WHERE id NOT IN (SELECT id FROM mytable ORDER BY id DESC LIMIT 3);funciona bien.
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
fuente
5

Si su identificación es incremental, use algo como

delete from table where id < (select max(id) from table)-N
Justin Wignall
fuente
2
Un gran problema en este buen truco: las publicaciones seriadas no siempre son contiguas (por ejemplo, cuando hubo retrocesos).
bortzmeyer
5

Para eliminar todos los registros excepto el último N , puede utilizar la consulta que se indica a continuación.

Es una consulta única pero con muchas declaraciones, por lo que en realidad no es una consulta única como se pretendía en la pregunta original.

También necesita una variable y una declaración preparada incorporada (en la consulta) debido a un error en MySQL.

Espero que pueda ser útil de todos modos ...

nnn son las filas que debe conservar y la Tabla es la tabla en la que está trabajando.

Supongo que tiene un registro de autoincremento llamado 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;

Lo bueno de este enfoque es el rendimiento : he probado la consulta en una base de datos local con aproximadamente 13.000 registros, manteniendo los últimos 1.000. Funciona en 0,08 segundos.

El guión de la respuesta aceptada ...

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

Tarda 0,55 segundos. Aproximadamente 7 veces más.

Entorno de prueba: mySQL 5.5.25 en una MacBookPro i7 de finales de 2011 con SSD

Paolo
fuente
2
DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)
Dave Swersky
fuente
1
Esto solo dejará la última fila
Justin Wignall
esta es la mejor solución que creo!
attaboyabhipro
1

prueba a continuación la consulta:

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

la subconsulta interna devolverá el valor de los 10 primeros y la consulta externa eliminará todos los registros excepto los 10 principales.

Nishant Nair
fuente
1
Alguna explicación sobre cómo funciona esto sería beneficioso para quienes se encuentren con esta respuesta. Por lo general, no se recomienda el volcado de código.
rayryeng
Esto no es correcto con una identificación no consistente
Slava Rozhnev
0

Qué pasa :

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

Devuelve filas con más de N filas antes. Podría ser útil ?

Hadrien
fuente
0

Usar id para esta tarea no es una opción en muchos casos. Por ejemplo, tabla con estados de Twitter. Aquí hay una variante con un campo de marca de tiempo especificado.

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

Solo quería incluir esto en la mezcla para cualquiera que use Microsoft SQL Server en lugar de MySQL. La palabra clave 'Límite' no es compatible con MSSQL, por lo que deberá utilizar una alternativa. Este código funcionó en SQL 2008 y se basa en esta publicación 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)

Es cierto que esto no es elegante. Si puede optimizar esto para Microsoft SQL, comparta su solución. ¡Gracias!

Ken Palmer
fuente
0

Si también necesita eliminar los registros basados ​​en alguna otra columna, aquí hay una solución:

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

Esto debería funcionar también:

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

Por qué no

DELETE FROM table ORDER BY id DESC LIMIT 1, 123456789

Simplemente elimine todo menos la primera fila (¡el orden es DESC!), Usando un número muy grande como segundo argumento LIMIT. Mira aquí

craesh
fuente
2
DELETEno admite [offset],o OFFSET: dev.mysql.com/doc/refman/5.0/en/delete.html
Nicole
-1

Respondiendo esto después de mucho tiempo ... Me encontré con la misma situación y en lugar de usar las respuestas mencionadas, vine a continuación:

DELETE FROM table_name order by ID limit 10

Esto eliminará los primeros 10 registros y mantendrá los últimos registros.

Nitesh
fuente
La pregunta era "todos excepto los últimos N registros" y "en una sola consulta". Pero parece que todavía necesita una primera consulta para contar todos los registros en la tabla y luego limitar al total - N
Paolo
@Paolo No requerimos una consulta para contar todos los registros ya que la consulta anterior borra todos excepto los últimos 10 registros.
Nitesh
1
No, esa consulta elimina los 10 registros más antiguos. El OP quiere eliminar todo excepto los n registros más recientes. La suya es la solución básica que se combinaría con una consulta de recuento, mientras que OP pregunta si hay una manera de combinar todo en una sola consulta.
ChrisMoll
@ChrisMoll Estoy de acuerdo. ¿Debo editar / eliminar esta respuesta ahora para que los usuarios no me voten en contra o la dejen como está?
Nitesh