Kueri SQL: Hapus semua rekaman dari tabel kecuali N terbaru?

90

Apakah mungkin untuk membangun kueri mysql tunggal (tanpa variabel) untuk menghapus semua catatan dari tabel, kecuali N terbaru (diurutkan berdasarkan id desc)?

Sesuatu seperti ini, hanya saja tidak berhasil :)

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

Terima kasih.

serg
sumber

Jawaban:

140

Anda tidak dapat menghapus record dengan cara itu, masalah utamanya adalah Anda tidak dapat menggunakan subquery untuk menentukan nilai klausa LIMIT.

Ini berfungsi (diuji di 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
);

Subquery menengah yang diperlukan. Tanpa itu kami akan mengalami dua kesalahan:

  1. Kesalahan SQL (1093): Anda tidak dapat menentukan tabel target 'tabel' untuk pembaruan di klausa FROM - MySQL tidak mengizinkan Anda untuk merujuk ke tabel yang Anda hapus dari dalam subkueri langsung.
  2. Kesalahan SQL (1235): Versi MySQL ini belum mendukung subkueri 'LIMIT & IN / ALL / ANY / SOME' - Anda tidak dapat menggunakan klausa LIMIT dalam subkueri langsung dari operator NOT IN.

Untungnya, menggunakan subkueri perantara memungkinkan kita melewati kedua batasan ini.


Nicole telah menunjukkan bahwa kueri ini dapat dioptimalkan secara signifikan untuk kasus penggunaan tertentu (seperti yang satu ini). Saya merekomendasikan membaca jawaban itu juga untuk melihat apakah itu cocok dengan Anda.

Alex Barrett
sumber
4
Oke itu berhasil - tetapi bagi saya, itu tidak elegan dan tidak memuaskan harus menggunakan trik misterius seperti itu. Namun demikian +1 untuk jawabannya.
Bill Karwin
1
Saya menandainya sebagai jawaban yang diterima, karena itu melakukan apa yang saya minta. Tetapi saya pribadi akan melakukannya mungkin dalam dua pertanyaan hanya untuk membuatnya sederhana :) Saya pikir mungkin ada cara yang cepat dan mudah.
serg
1
Terima kasih Alex, jawaban Anda membantu saya. Saya melihat bahwa subkueri perantara diperlukan tetapi saya tidak mengerti mengapa. Apakah Anda punya penjelasan untuk itu?
Sv1
8
sebuah pertanyaan: untuk apa "foo"?
Sebastian Breit
9
Perroloco, saya mencoba tanpa foo dan mendapatkan kesalahan ini: ERROR 1248 (42000): Setiap tabel turunan harus memiliki aliasnya sendiri. Jadi jawaban mereka, setiap tabel turunan pasti memiliki aliasnya sendiri!
codygman
106

Saya tahu saya membangkitkan pertanyaan yang cukup lama, tetapi saya baru-baru ini mengalami masalah ini, tetapi membutuhkan sesuatu yang berskala besar dengan baik . Tidak ada data kinerja yang ada, dan karena pertanyaan ini mendapat sedikit perhatian, saya pikir saya akan memposting apa yang saya temukan.

Solusi yang benar-benar berhasil adalah sub-query /NOT IN metode ganda Alex Barrett (mirip dengan Bill Karwin ), dan metode QuassnoiLEFT JOIN .

Sayangnya, kedua metode di atas membuat tabel sementara menengah yang sangat besar dan kinerja menurun dengan cepat karena jumlah record yang tidak dihapus menjadi besar.

Apa yang saya putuskan menggunakan sub-kueri ganda Alex Barrett (terima kasih!) Tetapi menggunakan <=alih-alih 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
  )

Ini digunakan OFFSETuntuk mendapatkan id dari record N dan menghapus record itu dan semua record sebelumnya.

Karena pengurutan sudah merupakan asumsi dari masalah ini ( ORDER BY id DESC), <=sangat cocok.

Ini jauh lebih cepat, karena tabel sementara yang dibuat oleh subkueri hanya berisi satu catatan, bukan catatan N.

Kasus cobaan

Saya menguji tiga metode kerja dan metode baru di atas dalam dua kasus pengujian.

Kedua kasus pengujian menggunakan 10.000 baris yang ada, sedangkan pengujian pertama menyimpan 9000 (menghapus 1000 terlama) dan pengujian kedua menyimpan 50 (menghapus 9950 terlama).

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

Yang menarik adalah bahwa <=metode ini melihat kinerja yang lebih baik secara keseluruhan, tetapi sebenarnya semakin baik semakin banyak Anda simpan, bukan semakin buruk.

Nicole
sumber
11
Saya membaca utas ini lagi 4,5 tahun kemudian. Tambahan yang bagus!
Alex Barrett
Wow, ini tampak hebat tetapi tidak berfungsi di Microsoft SQL 2008. Saya mendapatkan pesan ini: "Sintaks salah dekat 'Batas'. Sangat menyenangkan berfungsi di MySQL, tetapi saya harus mencari solusi alternatif.
Ken Palmer
1
@KenPalmer Anda masih dapat menemukan baris offset tertentu menggunakan ROW_NUMBER(): stackoverflow.com/questions/603724/…
Nicole
3
@KenPalmer menggunakan SELECT TOP daripada LIMIT ketika beralih antara SQL dan mySQL
Alpha G33k
1
Bersulang untuk itu. Ini mengurangi kueri pada kumpulan data saya (sangat besar) dari 12 menit menjadi 3,64 detik!
Lieuwe
10

Sayangnya untuk semua jawaban yang diberikan oleh orang lain, Anda tidak bisa DELETEdan SELECTdari tabel tertentu dalam kueri yang sama.

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 juga tidak dapat mendukung LIMITsubquery. Ini adalah batasan 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'

Jawaban terbaik yang dapat saya berikan adalah melakukan ini dalam dua tahap:

SELECT id FROM mytable ORDER BY id DESC LIMIT n; 

Kumpulkan id dan buat menjadi string yang dipisahkan koma:

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

(Biasanya menginterpolasi daftar yang dipisahkan koma ke dalam pernyataan SQL memperkenalkan beberapa risiko injeksi SQL, tetapi dalam kasus ini nilai tidak berasal dari sumber yang tidak tepercaya, nilai tersebut dikenal sebagai nilai integer dari database itu sendiri.)

catatan: Meskipun ini tidak menyelesaikan pekerjaan dalam satu kueri, terkadang solusi yang lebih sederhana dan selesaikan adalah yang paling efektif.

Bill Karwin
sumber
Tapi Anda bisa melakukan inner joins antara delete dan select. Apa yang saya lakukan di bawah ini seharusnya berhasil.
achinda99
Anda perlu menggunakan subkueri perantara agar LIMIT bekerja di subkueri.
Alex Barrett
@ achinda99: Saya tidak melihat jawaban dari Anda di utas ini ...?
Bill Karwin
Saya ditarik untuk rapat. Salahku. Saya tidak memiliki lingkungan pengujian sekarang untuk menguji sql yang saya tulis, tetapi saya telah melakukan apa yang dilakukan Alex Barret dan saya membuatnya bekerja dengan gabungan dalam.
achinda99
Itu adalah batasan MySQL yang bodoh. Dengan PostgreSQL, DELETE FROM mytable WHERE id NOT IN (SELECT id FROM mytable ORDER BY id DESC LIMIT 3);berfungsi dengan baik.
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
sumber
5

Jika id Anda bertambah, gunakan sesuatu seperti

delete from table where id < (select max(id) from table)-N
Justin Wignall
sumber
2
Satu masalah besar dalam trik bagus ini: serial tidak selalu bersebelahan (misalnya ketika ada rollback).
bortzmeyer
5

Untuk menghapus semua record kecuali N terakhir Anda dapat menggunakan query yang dilaporkan di bawah ini.

Ini adalah kueri tunggal tetapi dengan banyak pernyataan jadi sebenarnya ini bukan kueri tunggal seperti yang dimaksudkan dalam pertanyaan asli.

Anda juga memerlukan variabel dan pernyataan siap pakai (dalam kueri) karena ada bug di MySQL.

Semoga bermanfaat pula ...

nnn adalah baris yang harus disimpan dan Tabel adalah tabel yang sedang Anda kerjakan.

Saya berasumsi Anda memiliki data autoincrementing bernama 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;

Hal yang baik tentang pendekatan ini adalah kinerja : Saya telah menguji kueri pada DB lokal dengan sekitar 13.000 catatan, menyimpan 1.000 yang terakhir. Ini berjalan dalam 0,08 detik.

Naskah dari jawaban yang diterima ...

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

Butuh 0,55 detik. Sekitar 7 kali lebih banyak.

Lingkungan pengujian: mySQL 5.5.25 pada MacBookPro i7 2011 akhir dengan SSD

Paolo
sumber
2
DELETE FROM table WHERE ID NOT IN
(SELECT MAX(ID) ID FROM table)
Dave Swersky
sumber
1
Ini hanya akan menyisakan satu baris terakhir
Justin Wignall
ini adalah solusi terbaik yang menurut saya!
attaboyabhipro
1

coba kueri di bawah ini:

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

kueri bagian dalam akan mengembalikan nilai 10 teratas dan kueri luar akan menghapus semua rekaman kecuali 10 teratas.

Nishant Nair
sumber
1
Beberapa penjelasan tentang cara kerjanya akan bermanfaat bagi mereka yang menemukan jawaban ini. Pembuangan kode biasanya tidak disarankan.
rayryeng
Ini tidak benar dengan id tidak konsisten
Slava Rozhnev
0

Bagaimana dengan :

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

Ini mengembalikan baris dengan lebih dari N baris sebelumnya. Bisa bermanfaat?

Hadrien
sumber
0

Menggunakan id untuk tugas ini bukanlah pilihan dalam banyak kasus. Misalnya - tabel dengan status twitter. Berikut adalah varian dengan kolom stempel waktu yang ditentukan.

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

Hanya ingin memasukkan ini ke dalam campuran untuk siapa saja yang menggunakan Microsoft SQL Server, bukan MySQL. Kata kunci 'Limit' tidak didukung oleh MSSQL, jadi Anda harus menggunakan alternatif. Kode ini bekerja di SQL 2008, dan didasarkan pada posting SO ini. 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)

Memang, ini tidak elegan. Jika Anda dapat mengoptimalkan ini untuk Microsoft SQL, silakan bagikan solusi Anda. Terima kasih!

Ken Palmer
sumber
0

Jika Anda perlu menghapus catatan berdasarkan beberapa kolom lain juga, berikut solusinya:

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

Ini seharusnya bekerja dengan baik:

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

Kenapa tidak

DELETE FROM table ORDER BY id DESC LIMIT 1, 123456789

Hapus saja semua kecuali baris pertama (urutannya adalah DESC!), Menggunakan jumlah yang sangat besar sebagai argumen LIMIT kedua. Lihat disini

craesh
sumber
2
DELETEtidak mendukung [offset],atau OFFSET: dev.mysql.com/doc/refman/5.0/en/delete.html
Nicole
-1

Menjawab ini setelah waktu yang lama ... Datang ke situasi yang sama dan alih-alih menggunakan jawaban yang disebutkan, saya datang dengan di bawah -

DELETE FROM table_name order by ID limit 10

Ini akan menghapus 10 catatan pertama dan menyimpan catatan terbaru.

Nitesh
sumber
Pertanyaan menanyakan "semua kecuali catatan N terakhir" dan "dalam satu kueri". Tetapi tampaknya Anda masih membutuhkan kueri pertama untuk menghitung semua catatan dalam tabel kemudian membatasi menjadi total - N
Paolo
@Paolo Kami tidak memerlukan kueri untuk menghitung semua rekaman karena kueri di atas menghapus semua kecuali 10 rekaman terakhir.
Nitesh
1
Tidak, kueri itu menghapus 10 rekaman terlama. OP ingin menghapus semuanya kecuali n catatan terbaru. Solusi Anda adalah solusi dasar yang akan dipasangkan dengan kueri hitungan, sementara OP menanyakan apakah ada cara untuk menggabungkan semuanya ke dalam satu kueri.
ChrisMoll
@Riskyjr Saya setuju. Haruskah saya mengedit / menghapus jawaban ini sekarang agar pengguna tidak menolak saya atau membiarkannya apa adanya?
Nitesh