Hapus jutaan baris dari tabel SQL

9

Saya harus menghapus 16+ jutaan rekaman dari tabel baris 221+ juta dan ini berjalan sangat lambat.

Saya menghargai jika Anda berbagi saran untuk membuat kode di bawah ini lebih cepat:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

DECLARE @BATCHSIZE INT,
        @ITERATION INT,
        @TOTALROWS INT,
        @MSG VARCHAR(500);
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;

BEGIN TRY
    BEGIN TRANSACTION;

    WHILE @BATCHSIZE > 0
        BEGIN
            DELETE TOP (@BATCHSIZE) FROM MySourceTable
            OUTPUT DELETED.*
            INTO MyBackupTable
            WHERE NOT EXISTS (
                                 SELECT NULL AS Empty
                                 FROM   dbo.vendor AS v
                                 WHERE  VendorId = v.Id
                             );

            SET @BATCHSIZE = @@ROWCOUNT;
            SET @ITERATION = @ITERATION + 1;
            SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
            SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);             
            PRINT @MSG;
            COMMIT TRANSACTION;
            CHECKPOINT;
        END;
END TRY
BEGIN CATCH
    IF @@ERROR <> 0
       AND @@TRANCOUNT > 0
        BEGIN
            PRINT 'There is an error occured.  The database update failed.';
            ROLLBACK TRANSACTION;
        END;
END CATCH;
GO

Rencana Eksekusi (terbatas untuk 2 iterasi)

masukkan deskripsi gambar di sini

VendorIdadalah PK dan non-clustered , di mana indeks clustered tidak digunakan oleh skrip ini. Ada 5 indeks non-unik dan non-cluster lainnya.

Tugas adalah "menghapus vendor yang tidak ada di tabel lain" dan mencadangkannya ke tabel lain. Saya punya 3 tabel vendors, SpecialVendors, SpecialVendorBackups,. Mencoba untuk menghapus SpecialVendorsyang tidak ada dalam Vendorstabel, dan memiliki cadangan catatan yang terhapus kalau-kalau apa yang saya lakukan salah dan saya harus mengembalikannya dalam satu atau dua minggu.

cilerler
sumber
Saya akan bekerja pada mengoptimalkan permintaan itu dan mencoba bergabung dengan kiri di mana null
paparazzo

Jawaban:

8

Rencana pelaksanaan menunjukkan bahwa ia membaca baris dari indeks yang tidak dikelompokkan dalam beberapa urutan kemudian melakukan pencarian untuk setiap baris luar yang dibaca untuk mengevaluasi NOT EXISTS

masukkan deskripsi gambar di sini

Anda menghapus 7,2% dari tabel. 16.000.000 baris dalam 3.556 bets 4.500

Dengan asumsi bahwa baris-baris yang memenuhi syarat akhirnya didistribusikan ke seluruh indeks maka ini berarti akan menghapus kira-kira 1 baris setiap 13,8 baris.

Jadi iterasi 1 akan membaca 62.156 baris dan melakukan banyak pencarian sebelum menemukan 4.500 untuk dihapus.

iterasi 2 akan membaca 57.656 (62.156 - 4.500) baris yang pasti tidak akan memenuhi syarat mengabaikan pembaruan bersamaan (karena mereka sudah diproses) dan kemudian 62.156 baris lain untuk mendapatkan 4.500 untuk dihapus.

iterasi 3 akan membaca (2 * 57.656) + 62.156 baris dan seterusnya hingga akhirnya iterasi 3.556 akan membaca (3.555 * 57.656) + 62.156 baris dan melakukan banyak pencarian.

Jadi jumlah indeks yang dilakukan di semua batch adalah SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Yang mana ((3555 * 3556 / 2) * 57656) + (3556 * 62156)- atau364,652,494,976

Saya akan menyarankan Anda mematerialisasi baris untuk menghapus ke tabel temp pertama

INSERT INTO #MyTempTable
SELECT MySourceTable.PK,
       1 + ( ROW_NUMBER() OVER (ORDER BY MySourceTable.PK) / 4500 ) AS BatchNumber
FROM   MySourceTable
WHERE  NOT EXISTS (SELECT *
                   FROM   dbo.vendor AS v
                   WHERE  VendorId = v.Id) 

Dan ubah DELETEuntuk menghapus WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Anda mungkin masih perlu memasukkan a NOT EXISTSdalam DELETEkueri itu sendiri untuk memenuhi pembaruan karena tabel temp telah diisi tetapi ini harus jauh lebih efisien karena hanya perlu melakukan 4,500 pencarian per batch.

Martin Smith
sumber
Ketika Anda mengatakan "mematerialisasikan baris untuk menghapus ke tabel temp pertama" Anda menyarankan untuk menempatkan semua catatan dengan semua kolom ke dalam tabel temp? atau hanya PKkolom? (Saya percaya Anda menyarankan saya untuk memindahkan mereka ke temp table sepenuhnya tetapi ingin memeriksa ulang)
cilerler
@cilerler - Just the key kolom (s)
Martin Smith
dapatkah Anda dengan cepat meninjau ini jika saya mendapatkan apa yang Anda katakan dengan benar atau tidak, tolong?
cilerler
@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTableseharusnya hanya DELETE FROM MySourceTable juga mengindeks tabel temp CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );dan VendorIdpasti PK sendiri? Anda memiliki> 221 juta vendor yang berbeda?
Martin Smith
Terima kasih Martin, Akan mengujinya setelah jam 6 sore. Dan jawaban Anda adalah, Ini jelas satu-satunya PK yang ada di tabel itu
cilerler
4

Rencana pelaksanaan menunjukkan bahwa setiap loop berturut-turut akan melakukan lebih banyak pekerjaan daripada loop sebelumnya. Dengan asumsi bahwa baris yang dihapus dihapus secara merata di seluruh tabel, loop pertama harus memindai sekitar 4500 * 221000000/16000000 = 62156 baris untuk menemukan 4500 baris yang akan dihapus. Itu juga akan melakukan jumlah yang sama dari pencarian indeks berkerumun terhadap vendortabel. Namun, loop kedua harus membaca melewati 62156 - 4500 = 57656 baris yang sama yang Anda tidak hapus pertama kali. Kita mungkin mengharapkan loop kedua untuk memindai 1.200 baris dari MySourceTabledan untuk melakukan 1.200 berusaha melawan vendortabel. Jumlah pekerjaan yang dibutuhkan per loop meningkat pada tingkat linier. Sebagai perkiraan, kita dapat mengatakan bahwa loop rata-rata perlu membaca 102516868 baris dari MySourceTabledan untuk melakukan 102516868 berusaha melawanvendormeja. Untuk menghapus 16 juta baris dengan ukuran batch 4.500 kode Anda perlu melakukan 16000000/4500 = 3556 loop, sehingga jumlah total pekerjaan untuk menyelesaikan kode Anda adalah sekitar 364,5 miliar baris yang dibaca dari MySourceTabledan 364,5 miliar indeks yang dicari.

Masalah yang lebih kecil adalah bahwa Anda menggunakan variabel lokal @BATCHSIZEdalam ekspresi TOP tanpa RECOMPILEatau beberapa petunjuk lainnya. Pengoptimal kueri tidak akan tahu nilai variabel lokal itu saat membuat paket. Itu akan menganggap bahwa itu sama dengan 100. Pada kenyataannya Anda menghapus 4.500 baris, bukan 100, dan Anda mungkin bisa berakhir dengan rencana yang kurang efisien karena perbedaan itu. Perkiraan kardinalitas rendah ketika memasukkan ke dalam tabel dapat menyebabkan kinerja juga. SQL Server mungkin memilih API internal yang berbeda untuk melakukan sisipan jika dianggap perlu memasukkan 100 baris dibandingkan dengan 4500 baris.

Salah satu alternatif adalah dengan cukup memasukkan kunci utama / kunci berkerumun dari baris yang ingin Anda hapus ke tabel sementara. Bergantung pada ukuran kolom kunci Anda, ini dapat dengan mudah masuk ke tempdb. Anda bisa mendapatkan log minimal dalam kasus itu yang berarti bahwa log transaksi tidak akan meledak. Anda juga bisa mendapatkan pencatatan minimum terhadap basis data apa pun dengan model pemulihan SIMPLE. Lihat tautan untuk informasi lebih lanjut tentang persyaratan.

Jika itu bukan opsi maka Anda harus mengubah kode Anda sehingga Anda dapat memanfaatkan indeks berkerumun di MySourceTable. Kuncinya adalah menulis kode Anda sehingga Anda melakukan kira-kira jumlah pekerjaan yang sama per loop. Anda dapat melakukannya dengan memanfaatkan indeks alih-alih hanya memindai tabel dari awal setiap kali. Saya menulis posting blog yang membahas beberapa metode pengulangan yang berbeda. Contoh-contoh dalam postingan tersebut memasukkan ke dalam tabel alih-alih menghapus tetapi Anda harus dapat mengadaptasi kode.

Dalam contoh kode di bawah ini saya menganggap bahwa kunci utama dan kunci cluster Anda MySourceTable. Saya menulis kode ini dengan cepat dan tidak dapat mengujinya:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

DECLARE @BATCHSIZE INT,
        @ITERATION INT,
        @TOTALROWS INT,
        @MSG VARCHAR(500)
        @STARTID BIGINT,
        @NEXTID BIGINT;
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;

SELECT @STARTID = ID
FROM MySourceTable
ORDER BY ID
OFFSET 0 ROWS
FETCH FIRST 1 ROW ONLY;

SELECT @NEXTID = ID
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
OFFSET (60000) ROWS
FETCH FIRST 1 ROW ONLY;

BEGIN TRY
    BEGIN TRANSACTION;

    WHILE @STARTID IS NOT NULL
        BEGIN
            WITH MySourceTable_DELCTE AS (
                SELECT TOP (60000) *
                FROM MySourceTable
                WHERE ID >= @STARTID
                ORDER BY ID
            )           
            DELETE FROM MySourceTable_DELCTE
            OUTPUT DELETED.*
            INTO MyBackupTable
            WHERE NOT EXISTS (
                                 SELECT NULL AS Empty
                                 FROM   dbo.vendor AS v
                                 WHERE  VendorId = v.Id
                             );

            SET @BATCHSIZE = @@ROWCOUNT;
            SET @ITERATION = @ITERATION + 1;
            SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
            SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);             
            PRINT @MSG;
            COMMIT TRANSACTION;

            CHECKPOINT;

            SET @STARTID = @NEXTID;
            SET @NEXTID = NULL;

            SELECT @NEXTID = ID
            FROM MySourceTable
            WHERE ID >= @STARTID
            ORDER BY ID
            OFFSET (60000) ROWS
            FETCH FIRST 1 ROW ONLY;

        END;
END TRY
BEGIN CATCH
    IF @@ERROR <> 0
       AND @@TRANCOUNT > 0
        BEGIN
            PRINT 'There is an error occured.  The database update failed.';
            ROLLBACK TRANSACTION;
        END;
END CATCH;
GO

Bagian kuncinya ada di sini:

WITH MySourceTable_DELCTE AS (
    SELECT TOP (60000) *
    FROM MySourceTable
    WHERE ID >= @STARTID
    ORDER BY ID
)   

Setiap loop hanya akan membaca 60000 baris dari MySourceTable. Itu akan menghasilkan ukuran penghapusan rata-rata 4.500 baris per transaksi dan ukuran penghapusan maksimum 60000 baris per transaksi. Jika Anda ingin lebih konservatif dengan ukuran batch yang lebih kecil, itu bagus juga. The @STARTIDkemajuan variabel setelah setiap loop sehingga Anda dapat menghindari membaca baris yang sama lebih dari sekali dari tabel sumber.

Joe Obbish
sumber
Terima kasih atas info terperinci. Saya menetapkan batas 4500 untuk tidak mengunci tabel. Jika saya tidak salah SQL memiliki batas keras yang mengunci seluruh tabel jika jumlah hapusnya di atas 5.000. Dan karena ini akan menjadi proses yang panjang saya tidak dapat berupaya untuk mengunci tabel itu untuk jangka waktu yang lama. Jika saya mengatur 60000 menjadi 4500, apakah Anda pikir saya akan mendapatkan kinerja yang sama?
cilerler
@cilerler Jika Anda khawatir tentang eskalasi kunci, Anda dapat menonaktifkannya di tingkat tabel. Tidak ada yang salah dengan menggunakan ukuran batch 4.500. Kuncinya adalah bahwa setiap loop akan melakukan kira-kira jumlah pekerjaan yang sama.
Joe Obbish
Saya harus menerima jawaban lain karena perbedaan kecepatan. Saya menguji solusi Anda dan solusi @ Martin-Smith dan versinya mendapatkan lebih banyak data ~ 2% selama 10 menit tes. Solusi Anda jauh lebih baik daripada milik saya dan saya sangat menghargai waktu Anda ... -
cilerler
2

Dua pemikiran muncul di benak:

Penundaan mungkin karena pengindeksan dengan volume data itu. Coba jatuhkan indeks, hapus, dan bangun kembali indeks.

Atau..

Mungkin lebih cepat untuk menyalin baris yang ingin Anda simpan ke tabel sementara, jatuhkan tabel dengan 16 juta baris, dan ganti nama tabel sementara (atau salin ke instance baru dari tabel sumber).

Jon
sumber