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

90

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

Как то так, только не работает :)

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

Спасибо.

серг
источник

Ответы:

140

Вы не можете удалить записи таким образом, основная проблема заключается в том, что вы не можете использовать подзапрос для указания значения предложения 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.

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


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

Алекс Барретт
источник
4
Хорошо, это работает, но для меня это неэлегантно и неудовлетворительно прибегать к подобным тайным уловкам. +1 все же за ответ.
Билл Карвин,
1
Я отмечаю это как принятый ответ, потому что он выполняет то, о чем я просил. Но я лично сделаю это, вероятно, в два запроса, чтобы было проще :) Я подумал, может быть, есть какой-то быстрый и простой способ.
serg
1
Спасибо, Алекс, мне помог твой ответ. Я вижу, что требуется промежуточный подзапрос, но не понимаю почему. У вас есть объяснение этому?
Sv1
8
вопрос: для чего "foo"?
Себастьян Брайт
9
Perroloco, я пробовал без foo и получил эту ошибку: ОШИБКА 1248 (42000): каждая производная таблица должна иметь свой собственный псевдоним. Итак, их наш ответ, каждая производная таблица должна иметь свой собственный псевдоним!
codygman
106

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

Решениями, которые действительно сработали, были двойной подзапрос /NOT IN метод Алекса Барретта (аналогичный методу Билла Карвина ) и метод КвасснойLEFT JOIN .

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

То, что я выбрал, использует двойной подзапрос Алекса Барретта (спасибо!), Но <=вместо этого использует 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- й записи и удаляет эту запись и все предыдущие записи.

Поскольку порядок уже является условием этой проблемы ( 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 |
+-----------+------------------------+----------------------+

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

Николь
источник
11
Я снова читаю эту ветку через 4,5 года. Хорошее дополнение!
Alex Barrett
Вау, это выглядит великолепно, но не работает в Microsoft SQL 2008. Я получаю следующее сообщение: «Неправильный синтаксис рядом с 'Limit'. Приятно, что он работает в MySQL, но мне нужно найти альтернативное решение.
Кен Палмер
1
@KenPalmer Вы все еще сможете найти определенное смещение строки, используя ROW_NUMBER(): stackoverflow.com/questions/603724/…
Николь
3
@KenPalmer использует SELECT TOP вместо LIMIT при переключении между SQL и mySQL
Alpha G33k
1
Приветствую вас. Это сократило запрос к моему (очень большому) набору данных с 12 минут до 3,64 секунды!
Lieuwe
10

К сожалению, для всех ответов, данных другими людьми, вы не можете 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, но в этом случае значения не поступают из ненадежного источника, они, как известно, являются целочисленными значениями из самой базы данных.)

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

Билл Карвин
источник
Но вы можете выполнять внутренние соединения между удалением и выбором. То, что я сделал ниже, должно работать.
achinda99
Вам нужно использовать промежуточный подзапрос, чтобы LIMIT работал в подзапросе.
Alex Barrett,
@ achinda99: Я не вижу ответа от вас в этой теме ...?
Билл Карвин,
Меня потянули на встречу. Виноват. У меня сейчас нет тестовой среды для тестирования написанного мной sql, но я сделал то же, что и Алекс Баррет, и заставил его работать с внутренним соединением.
achinda99
Это глупое ограничение MySQL. С PostgreSQL DELETE FROM mytable WHERE id NOT IN (SELECT id FROM mytable ORDER BY id DESC LIMIT 3);работает нормально.
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
источник
5

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

delete from table where id < (select max(id) from table)-N
Джастин Уигнал
источник
2
Одна большая проблема в этом красивом трюке: серийные номера не всегда смежны (например, при откатах).
bortzmeyer
5

Чтобы удалить все записи, кроме последних 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 на i7 MacBookPro конца 2011 года с SSD

Паоло
источник
2
DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)
Дэйв Сверски
источник
1
Остается только одна последняя строка
Джастин Уигнал,
я считаю это лучшим решением!
attaboyabhipro
1

попробуйте ниже запрос:

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

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

Нишант Наир
источник
1
Некоторое объяснение того, как это работает, было бы полезно для тех, кто столкнется с этим ответом. Сброс кода обычно не рекомендуется.
Rayryeng
Это неверно с непоследовательным идентификатором
Слава Рожнев
0

Что о :

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

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

Адриен
источник
0

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

delete from table 
where access_time >= 
(
    select access_time from  
    (
        select access_time from table 
            order by access_time limit 150000,1
    ) foo    
)
Александр Демьяненко
источник
0

Просто хотел добавить это в смесь для тех, кто использует Microsoft SQL Server вместо MySQL. Ключевое слово Limit не поддерживается MSSQL, поэтому вам нужно использовать альтернативу. Этот код работал в SQL 2008 и основан на этом сообщении 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)

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

Кен Палмер
источник
0

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

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
источник
0

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

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]
ачинда99
источник
0
DELETE FROM table WHERE id NOT IN (
    SELECT id FROM table ORDER BY id, desc LIMIT 0, 10
)
Майк Риделл
источник
-1

Почему нет

DELETE FROM table ORDER BY id DESC LIMIT 1, 123456789

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

Craesh
источник
2
DELETEне поддерживает [offset],или OFFSET: dev.mysql.com/doc/refman/5.0/en/delete.html
Николь
-1

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

DELETE FROM table_name order by ID limit 10

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

Нитеш
источник
Был задан вопрос «все, кроме последних N записей» и «в одном запросе». Но, похоже, вам все еще нужен первый запрос для подсчета всех записей в таблице, а затем ограничение на общее количество - N
Паоло
@Paolo Нам не нужен запрос для подсчета всех записей, поскольку приведенный выше запрос удаляет все, кроме последних 10 записей.
Нитеш 03
1
Нет, этот запрос удаляет 10 самых старых записей. OP хочет удалить все, кроме n самых последних записей. Ваше базовое решение будет связано с запросом подсчета, в то время как OP спрашивает, есть ли способ объединить все в один запрос.
ChrisMoll 07
@ChrisMoll Я согласен. Должен ли я отредактировать / удалить этот ответ сейчас, чтобы пользователи не проголосовали за меня, или оставить его как есть?
Nitesh