SQL-запрос: удалить все записи из таблицы, кроме последних N?

Возможно ли построить один mysql-запрос (без переменных), чтобы удалить все записи из таблицы, кроме последнего N (отсортировано по id desc)?

Что-то вроде этого, только он не работает:)

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

Спасибо.

Ответ 1

Вы не можете удалить записи таким образом, основная проблема в том, что вы не можете использовать подзапрос, чтобы указать значение предложения LIMIT.

Это работает (проверено в 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
);

Промежуточный подзапрос обязателен. Без этого мы столкнулись бы с двумя ошибками:

  1. Ошибка SQL (1093): вы не можете указать целевую таблицу 'table' для обновления в предложении FROM - MySQL не позволяет вам ссылаться на таблицу, которую вы удаляете из прямого подзапроса.
  2. Ошибка SQL (1235): эта версия MySQL еще не поддерживает "подзапрос LIMIT & IN/ALL/ANY/SOME" - вы не можете использовать предложение LIMIT в прямом подзапросе оператора NOT IN.

К счастью, использование промежуточного подзапроса позволяет нам обойти оба эти ограничения.


Николь указала, что этот запрос может быть значительно оптимизирован для определенных случаев использования (таких как этот). Я рекомендую прочитать этот ответ, чтобы увидеть, подходит ли он вам.

Ответ 2

Я знаю, что я воскрешаю довольно старый вопрос, но недавно я столкнулся с этой проблемой, но мне нужно что-то, что масштабируется до больших чисел хорошо. Не было никаких существующих данных о производительности, и, поскольку этот вопрос имел довольно много внимания, я думал, что опубликую то, что нашел.

Решения, которые фактически работали, были метод двойного подзапроса Alex Barrett/ NOT IN (аналогичный Bill Karwin's) и Quassnoi LEFT JOIN.

К сожалению, оба вышеупомянутых метода создают очень большие промежуточные временные таблицы, и производительность быстро снижается по мере того, как количество удаляемых записей не становится большим.

То, что я установил, использует двойной подзапрос Alex Barrett (спасибо!), но использует <= вместо 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
  )

Он использует OFFSET для получения идентификатора записи N th и удаляет эту запись и все предыдущие записи.

Так как упорядочение уже является предположением этой проблемы (ORDER BY id DESC), <= является идеальным подходом.

Это намного быстрее, поскольку временная таблица, сгенерированная подзапросом, содержит только одну запись вместо записей N.

Тестовый кейс

Я тестировал три рабочих метода и новый метод выше в двух тестовых случаях.

Оба тестовых примера используют 10000 существующих строк, в то время как первый тест хранит 9000 (удаляет старейший 1000), а второй тест хранит 50 (удаляет самый старый 9950).

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

Интересно, что метод <= видит лучшую производительность по всем направлениям, но на самом деле становится лучше, чем больше вы держите, а не хуже.

Ответ 3

К сожалению, для всех ответов, предоставленных другими людьми, вы не можете DELETE и SELECT из данной таблицы в том же запросе.

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 поддерживать LIMIT в подзапросе. Это ограничения 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'

Лучший ответ, который я могу придумать, - сделать это в два этапа:

SELECT id FROM mytable ORDER BY id DESC LIMIT n; 

Соберите идентификатор и сделайте их в строку, разделенную запятыми:

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

(Обычно интерполяция отдельного списка с запятой в оператор SQL вводит некоторый риск внедрения SQL, но в этом случае значения не поступают из ненадежного источника, они, как известно, являются значениями id из самой базы данных.)

примечание:. Хотя это не позволяет выполнить задание в одном запросе, иногда более простое, готовое решение является наиболее эффективным.

Ответ 4

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

Если ваш идентификатор является инкрементным, используйте что-то вроде

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

Ответ 6

Чтобы удалить все записи, кроме последнего N, вы можете использовать запрос, указанный ниже.

Это один запрос, но со многими утверждениями, так что на самом деле это не один запрос, как это было задумано в исходном вопросе.

Также вам нужна переменная и встроенный (в запросе) подготовленный оператор из-за ошибки в MySQL.

Надеюсь, это может быть полезно в любом случае...

nnn - это строки, которые нужно сохранить, а theTable - это таблица, над которой вы работаете.

Я предполагаю, что у вас есть автоинкрементная запись с именем 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;

Хорошая вещь в этом подходе - производительность: я проверил запрос к локальной БД с около 13 000 записей, сохранив последние 1000. Работает за 0,08 секунды.

Сценарий из принятого ответа...

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

Занимает 0,55 секунды. Примерно в 7 раз больше.

Тестовая среда: MySQL 5.5.25 на MacBook Pro, выпущенном в конце 2011 года, с SSD

Ответ 7

DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)

Ответ 8

попробуйте выполнить запрос:

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

внутренний запрос будет возвращать значение верхнего 10, а внешний запрос удалит все записи, кроме первых 10.

Ответ 9

УДАЛИТЬ ОТ таблицы WHERE id NOT IN (SELECT id FROM table ORDER BY id, desc LIMIT 0, 10)

Ответ 10

Это также должно работать:

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]

Ответ 11

Как насчет:

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

Он возвращает строки с более чем N строками ранее. Может быть полезно?

Ответ 12

Использование id для этой задачи во многих случаях не является опцией. Например - таблица с состояниями твиттера. Вот вариант с указанным полем метки времени.

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

Ответ 13

Просто хотел бросить это в микс для всех, кто использует Microsoft SQL Server вместо MySQL. Ключевое слово "Limit" не поддерживается MSSQL, поэтому вам нужно использовать альтернативу. Этот код работал в SQL 2008 и основывается на этом сообщении SO. fooobar.com/questions/25107/...

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

По общему признанию, это не изящно. Если вы можете оптимизировать это для Microsoft SQL, поделитесь своим решением. Спасибо!

Ответ 14

Если вам нужно удалить записи на основе другого столбца, то вот решение:

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

Ответ 15

Почему бы не

DELETE FROM table ORDER BY id DESC LIMIT 1, 123456789

Просто удалите все, кроме первой строки (порядок DESC!), используя очень большой nummber как второй LIMIT-аргумент. Смотрите здесь

Ответ 16

Отвечая на это через долгое время... Пришедшая через ту же ситуацию и вместо использования упомянутых ответов, я пришел с ниже -

DELETE FROM table_name order by ID limit 10

Это приведет к удалению 1-го 10 записей и сохранению последних записей.