Consulta SQL: Exclua todos os registros da tabela, exceto os N?

91

É possível construir uma única consulta mysql (sem variáveis) para remover todos os registros da tabela, exceto o último N (classificado por id desc)?

Algo assim, só que não funciona :)

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

Obrigado.

serg
fonte

Respostas:

141

Você não pode excluir os registros dessa maneira, o principal problema é que você não pode usar uma subconsulta para especificar o valor de uma cláusula LIMIT.

Isso funciona (testado em 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
);

A subconsulta intermediária é necessária. Sem ele, teríamos dois erros:

  1. Erro de SQL (1093): Você não pode especificar a tabela de destino 'tabela' para atualização na cláusula FROM - o MySQL não permite que você consulte a tabela que está excluindo de uma subconsulta direta.
  2. Erro SQL (1235): Esta versão do MySQL ainda não suporta 'subconsulta LIMIT & IN / ALL / ANY / SOME' - Você não pode usar a cláusula LIMIT em uma subconsulta direta de um operador NOT IN.

Felizmente, usar uma subconsulta intermediária nos permite ignorar essas duas limitações.


Nicole apontou que essa consulta pode ser otimizada significativamente para certos casos de uso (como este). Recomendo também a leitura dessa resposta para ver se ela se encaixa na sua.

Alex Barrett
fonte
4
Ok, isso funciona - mas para mim, é deselegante e insatisfatório ter que recorrer a truques misteriosos como esse. 1, no entanto, para a resposta.
Bill Karwin
1
Eu marquei como uma resposta aceita, porque faz o que eu pedi. Mas eu pessoalmente farei isso provavelmente em duas consultas apenas para mantê-lo simples :) Eu pensei que talvez houvesse uma maneira rápida e fácil.
serg
1
Obrigado Alex, sua resposta me ajudou. Vejo que a subconsulta intermediária é necessária, mas não entendo por quê. Você tem uma explicação para isso?
Sv1,
9
uma pergunta: para que serve o "foo"?
Sebastian Breit
9
Perroloco, tentei sem foo e recebi este erro: ERROR 1248 (42000): Cada tabela derivada deve ter seu próprio alias Portanto, nossa resposta, cada tabela derivada deve ter seu próprio alias!
codygman
109

Sei que estou ressuscitando uma questão bastante antiga, mas recentemente me deparei com esse problema, mas precisava de algo que se dimensionasse bem para grandes números . Não havia nenhum dado de desempenho existente, e como esta questão chamou bastante atenção, pensei em postar o que encontrei.

As soluções que realmente funcionaram foram a subconsulta /NOT IN método duplo de Alex Barrett (semelhante ao de Bill Karwin ) e oLEFT JOIN método de Quassnoi .

Infelizmente, os dois métodos acima criam tabelas temporárias intermediárias muito grandes e o desempenho diminui rapidamente à medida que aumenta o número de registros que não estão sendo excluídos.

O que decidi utiliza a subconsulta dupla de Alex Barrett (obrigado!), Mas usa em <=vez 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
  )

Ele usa OFFSETpara obter o ID do N º registro e exclui esse registro e todos os registros anteriores.

Como ordenar já é uma suposição desse problema ( ORDER BY id DESC), <=é um ajuste perfeito.

É muito mais rápido, pois a tabela temporária gerada pela subconsulta contém apenas um registro em vez de N registros.

Caso de teste

Testei os três métodos de trabalho e o novo método acima em dois casos de teste.

Ambos os casos de teste usam 10.000 linhas existentes, enquanto o primeiro teste mantém 9.000 (exclui os 1.000 mais antigos) e o segundo teste mantém 50 (exclui os 9950 mais antigos).

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

O que é interessante é que o <=método tem melhor desempenho em todas as áreas, mas na verdade fica melhor quanto mais você guarda, em vez de piorar.

Nicole
fonte
11
Estou lendo este tópico novamente 4,5 anos depois. Boa adição!
Alex Barrett
Uau, isso parece ótimo, mas não funciona no Microsoft SQL 2008. Recebo esta mensagem: "Sintaxe incorreta perto de 'Limite'. É bom que funcione no MySQL, mas vou precisar encontrar uma solução alternativa.
Ken Palmer
1
@KenPalmer Você ainda deve conseguir encontrar um deslocamento de linha específico usando ROW_NUMBER(): stackoverflow.com/questions/603724/…
Nicole,
3
@KenPalmer use SELECT TOP em vez de LIMIT ao alternar entre SQL e mySQL
Alpha G33k
1
Viva por isso. Isso reduziu a consulta no meu conjunto de dados (muito grande) de 12 minutos para 3,64 segundos!
Lieuwe
10

Infelizmente, para todas as respostas dadas por outras pessoas, você não pode DELETEe SELECTde uma determinada tabela na mesma 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

Nem o MySQL oferece suporte LIMITem uma subconsulta. Essas são limitações do 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'

A melhor resposta que posso dar é fazer isso em dois estágios:

SELECT id FROM mytable ORDER BY id DESC LIMIT n; 

Colete os ids e transforme-os em uma string separada por vírgulas:

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

(Normalmente, interpolar uma lista separada por vírgulas em uma instrução SQL apresenta algum risco de injeção de SQL, mas, neste caso, os valores não vêm de uma fonte não confiável, eles são conhecidos como valores inteiros do próprio banco de dados.)

nota: embora isso não conclua o trabalho em uma única consulta, às vezes uma solução mais simples e pronta é a mais eficaz.

Bill Karwin
fonte
Mas você pode fazer junções internas entre excluir e selecionar. O que eu fiz abaixo deve funcionar.
achinda99
Você precisa usar uma subconsulta intermediária para fazer LIMIT funcionar na subconsulta.
Alex Barrett
@ achinda99: Não estou vendo uma resposta sua neste tópico ...?
Bill Karwin
Fui chamado para uma reunião. Foi mal. Não tenho um ambiente de teste agora para testar o sql que escrevi, mas fiz o que Alex Barret fez e fiz com que funcionasse com uma junção interna.
achinda99
É uma limitação estúpida do MySQL. Com PostgreSQL, DELETE FROM mytable WHERE id NOT IN (SELECT id FROM mytable ORDER BY id DESC LIMIT 3);funciona bem.
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
fonte
5

Se o seu id for incremental, use algo como

delete from table where id < (select max(id) from table)-N
Justin Wignall
fonte
2
Um grande problema neste belo truque: os seriais nem sempre são contíguos (por exemplo, quando havia rollbacks).
bortzmeyer
5

Para excluir todos os registros, exceto o último N, você pode usar a consulta relatada abaixo.

É uma consulta única, mas com muitas instruções, portanto, não é realmente uma única consulta da maneira que se pretendia na pergunta original.

Além disso, você precisa de uma variável e uma instrução preparada embutida (na consulta) devido a um bug no MySQL.

Espero que possa ser útil de qualquer maneira ...

nnn são as linhas a serem mantidas e theTable é a tabela na qual você está trabalhando.

Presumo que você tenha um registro de incremento automático chamado 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;

O bom dessa abordagem é o desempenho : testei a consulta em um banco de dados local com cerca de 13.000 registros, mantendo os últimos 1.000. Ele roda em 0,08 segundos.

O script da resposta aceita ...

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

Demora 0,55 segundos. Cerca de 7 vezes mais.

Ambiente de teste: mySQL 5.5.25 em um MacBookPro i7 do final de 2011 com SSD

Paolo
fonte
2
DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)
Dave Swersky
fonte
1
Isso deixará apenas uma última linha
Justin Wignall
esta é a melhor solução que eu acho!
attaboyabhipro
1

experimente a consulta abaixo:

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

a subconsulta interna retornará os 10 valores principais e a consulta externa excluirá todos os registros, exceto os 10 principais.

Nishant Nair
fonte
1
Alguma explicação de como isso funciona seria benéfica para aqueles que encontrarem esta resposta. O despejo de código geralmente não é recomendado.
rayryeng
0

A respeito :

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

Ele retorna linhas com mais de N linhas antes. Pode ser útil?

Adriano
fonte
0

O uso de id para esta tarefa não é uma opção em muitos casos. Por exemplo - tabela com status do Twitter. Aqui está uma variante com campo de carimbo de data / hora 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
fonte
0

Só queria adicionar isso à mistura para qualquer pessoa que use o Microsoft SQL Server em vez do MySQL. A palavra-chave 'Limite' não é compatível com MSSQL, portanto, você precisará usar uma alternativa. Este código funcionou no SQL 2008 e é baseado neste post do 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 que isso não é elegante. Se você puder otimizar isso para o Microsoft SQL, compartilhe sua solução. Obrigado!

Ken Palmer
fonte
0

Se você precisar excluir os registros com base em alguma outra coluna também, aqui está uma solução:

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

Isso também deve funcionar:

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

Por que não

DELETE FROM table ORDER BY id DESC LIMIT 1, 123456789

Apenas exclua tudo, exceto a primeira linha (a ordem é DESC!), Usando um número muito grande como segundo argumento LIMIT. Veja aqui

craesh
fonte
2
DELETEnão suporta [offset],ou OFFSET: dev.mysql.com/doc/refman/5.0/en/delete.html
Nicole
-1

Respondendo isso depois de muito tempo ... Encontrei a mesma situação e em vez de usar as respostas mencionadas, vim com abaixo -

DELETE FROM table_name order by ID limit 10

Isso excluirá os primeiros 10 registros e manterá os registros mais recentes.

Nitesh
fonte
A pergunta feita "todos exceto os últimos N registros" e "em uma única consulta". Mas parece que você ainda precisa de uma primeira consulta para contar todos os registros na tabela e então limitar o total - N
Paolo
@Paolo Não exigimos uma consulta para contar todos os registros, pois a consulta acima exclui todos, exceto os últimos 10 registros.
Nitesh,
1
Não, essa consulta exclui os 10 registros mais antigos. O OP deseja excluir tudo, exceto os n registros mais recentes. A sua é a solução básica que seria emparelhada com uma consulta de contagem, enquanto o OP pergunta se há uma maneira de combinar tudo em uma única consulta.
ChrisMoll,
@ChrisMoll eu concordo. Devo editar / excluir esta resposta agora para permitir que os usuários não votem em mim ou deixar como está?
Nitesh