Запит SQL: Видалити з таблиці всі записи, крім останніх N?


90

Чи можна побудувати один запит mysql (без змінних), щоб видалити з таблиці всі записи, крім останньої N (відсортовано за ідентифікатором desc)?

Щось подібне, тільки не працює :)

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

Дякую.

Відповіді:


139

Ви не можете видалити записи таким чином, головна проблема полягає в тому, що ви не можете використовувати підзапит, щоб вказати значення речення 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): Ви не можете вказати цільову таблицю "таблиця" для оновлення в реченні 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

5

Якщо ваш ідентифікатор зростає, використовуйте щось на зразок

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

2
Одна велика проблема в цьому приємному трюку: серіали не завжди суміжні (наприклад, коли були відкати).
bortzmeyer

5

Щоб видалити всі записи, крім останніх N, ви можете скористатися наведеним нижче запитом.

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

Також вам потрібна змінна та вбудований (у запиті) підготовлений оператор через помилку в MySQL.

Сподіваюся, це все одно може стати в нагоді ...

NNN є рядками , щоб зберегти і таблиці , з це таблиця , ви працюєте.

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

Хороша річ у цьому підході - це продуктивність : я протестував запит на локальній БД із приблизно 13000 записами, зберігаючи останні 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 наприкінці 2011 року i7 MacBookPro з SSD



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

Використання ідентифікатора для цього завдання у багатьох випадках не є можливим. Наприклад - таблиця зі статусами Twitter. Ось варіант із зазначеним полем позначки часу.

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. Ключове слово "Обмеження" не підтримується 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

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]



-1

Відповівши на це через довгий час ... Натрапив на таку ж ситуацію, і замість того, щоб використовувати згадані відповіді, я прийшов нижче -

DELETE FROM table_name order by ID limit 10

Це призведе до видалення перших 10 записів та збереження останніх записів.


Задавали запитання "всі, крім останніх N записів" та "в одному запиті". Але, здається, вам все-таки потрібен перший запит, щоб порахувати всі записи в таблиці, а потім обмежити до загальної кількості - N
Паоло

@Paolo Нам не потрібен запит для підрахунку всіх записів, оскільки наведений вище запит видаляє всі, крім останніх 10 записів.
Nitesh

1
Ні, цей запит видаляє 10 найстаріших записів. OP хоче видалити все, крім найновіших записів. Твоє є основним рішенням, яке поєднується із запитом підрахунку, тоді як OP запитує, чи є спосіб об’єднати все в один запит.
ChrisMoll

@ChrisMoll Я згоден. Чи слід мені редагувати / видаляти цю відповідь зараз, щоб дозволити користувачам не голосувати за мене чи залишити її такою, яка вона є?
Nitesh
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.