Bir SQL tablosundan milyonlarca satırı silme


9

221 milyon satırlık tablodan 16 milyondan fazla kaydı silmem gerekiyor ve son derece yavaş gidiyor.

Aşağıdaki kodu daha hızlı hale getirmek için önerileri paylaşırsanız teşekkür ederiz:

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

Yürütme Planı (2 yineleme ile sınırlıdır)

resim açıklamasını buraya girin

VendorIdolan PK ve olmayan kümelenmiş yerlerde, kümelenmiş dizin bu komut dosyası tarafından kullanılıyor değildir. Diğer benzersiz olmayan, kümelenmemiş 5 dizin vardır.

Görev "başka bir tabloda bulunmayan satıcıları kaldırmak" ve başka bir tabloya yedeklemek. 3 masam var vendors, SpecialVendors, SpecialVendorBackups. Tabloda SpecialVendorsolmayanları kaldırmaya Vendorsçalışıyorum ve yaptığımın yanlış olması durumunda silinmiş kayıtların yedeğini almaya çalışıyorum ve bunları bir iki hafta içinde geri koymam gerekiyor.


Bu sorguyu optimize etmeye çalışacağım ve soldaki birleşmeyi deneyeceğim
nara

Yanıtlar:


8

Yürütme planı, bir sırada kümelenmemiş bir dizinden satırları okuduğunu ve ardından okunan her dış satır için arama gerçekleştirdiğini gösterir. NOT EXISTS

resim açıklamasını buraya girin

Tablonun% 7.2'sini siliyorsunuz. 3.556 4.500 partide 16.000.000 satır

Nitelikli satırların dizin boyunca belirgin bir şekilde dağıtıldığı varsayılarak, her 13.8 satırda yaklaşık 1 satır silinir.

Bu nedenle, yineleme 1, 62.156 satırı okuyacak ve birçok indeksin silinecek 4.500 bulmadan önce gerçekleştireceğini gösterecektir.

yineleme 2, eşzamanlı güncellemeleri (zaten işlendikleri gibi) yok saymaya kesinlikle hak kazanmayacak 57.656 (62.156 - 4.500) satırı ve daha sonra 4.500'ü silmek için başka 62.156 satırı okuyacaktır.

yineleme 3 (2 * 57.656) + 62.156 satır vb. okuyacak ve yineleyene kadar 3.556 yineleme (3.555 * 57.656) + 62.156 satır okuyacak ve birçok arama gerçekleştirilecektir.

Dolayısıyla, tüm gruplar arasında yapılan dizin arama sayısı SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

Hangisi ((3555 * 3556 / 2) * 57656) + (3556 * 62156)- veya364,652,494,976

Öncelikle geçici tabloya silmek için satırları gerçekleştirmenizi öneririm

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) 

Ve değiştirmek DELETEsilmek WHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)Hala dahil etmek gerekebilir NOT EXISTSiçinde DELETEgeçici tablo doldurulur beri güncellemeler için hitap sorguda kendisi ama sadece 4,500 parti başına arar uygulamanız gerekir olarak bu çok daha verimli olmalıdır.


"Önce bir geçici tabloya silmek için satırları materyalize edin" dediğinizde, tüm bu sütunları içeren tüm kayıtları geçici tabloya yerleştirmeyi mi öneriyorsunuz? ya da sadece PKsütun? (Bunları geçici masaya tamamen
taşımamı

@cilerler - Sadece anahtar sütunlar
Martin Smith

hızlı gözden geçirebilir bu sana doğru ya da değil, lütfen söylediklerini alırsanız?
cilerler

@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTableSadece olmalıdır DELETE FROM MySourceTable da geçici tablo indeksi CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );ve olduğu VendorIdkesinlikle kendi başına PK? 221 milyondan fazla farklı satıcınız var mı?
Martin Smith

Teşekkürler Martin, 18:00 sonra test edecek. Ve cevabınız, Bu tabloda kesinlikle tek PK var
cilerler

4

Yürütme planı, birbirini izleyen her döngünün önceki döngüden daha fazla iş yapacağını gösterir. Silinecek satırların tablo boyunca eşit olarak dağıtıldığı varsayılarak, ilk döngü silinecek 4500 satırı bulmak için yaklaşık 4500 * 221000000/16000000 = 62156 satırı taramalıdır. Ayrıca, vendortabloya göre aynı sayıda kümelenmiş dizin arar . Ancak, ikinci döngünün ilk kez silmediğiniz 62156 - 4500 = 57656 satırlarını geçmesi gerekir. İkinci döngünün 120000 satırı taramasını MySourceTableve 120000 aramasını vendortabloya göre yapmasını bekleyebiliriz . Döngü başına gereken iş miktarı doğrusal bir oranda artar. Yaklaşık bir değer olarak biz ortalama döngü gelen gelen 102516868 satırları okumak gerekir söyleyebiliriz MySourceTableve 102516868 karşı istiyor yapmakvendortablo. 4500 toplu iş büyüklüğüne sahip 16 milyon satırı silmek için kodunuzun 16000000/4500 = 3556 döngü yapması gerekir, bu nedenle kodunuzun tamamlanması için gereken toplam çalışma miktarı 364,5 milyar satır okur MySourceTableve 364,5 milyar dizin arar.

Daha küçük bir sorun, @BATCHSIZETOP ifadesinde yerel bir değişkeni bir RECOMPILEveya başka ipucu olmadan kullanmanızdır . Sorgu iyileştirici, bir plan oluştururken bu yerel değişkenin değerini bilmeyecektir. Gerçekte 100'e eşit olduğunu varsayacaktır. Gerçekte 100 yerine 4500 satır siliyorsunuz ve muhtemelen bu tutarsızlık nedeniyle daha az verimli bir plan elde edebilirsiniz. Bir tabloya eklenirken düşük kardinalite tahmini performans performansına da neden olabilir. SQL Server, 4500 satır yerine 100 satır eklemesi gerektiğini düşünüyorsa, ekleme yapmak için farklı bir dahili API seçebilir.

Alternatiflerden biri, silmek istediğiniz satırların birincil anahtarlarını / kümelenmiş anahtarlarını geçici bir tabloya eklemektir. Anahtar sütunlarınızın boyutuna bağlı olarak bu kolaylıkla tempdb'ye sığabilir. Bu durumda minimum günlük kaydı alabilirsiniz , yani işlem günlüğü patlamaz. Ayrıca bir kurtarma modeli ile herhangi bir veritabanına karşı en az günlük alabilirsiniz SIMPLE. Gereksinimler hakkında daha fazla bilgi için bağlantıya bakın.

Bu bir seçenek değilse, kümelenmiş dizinden yararlanabilmeniz için kodunuzu değiştirmeniz gerekir MySourceTable. Önemli olan, her döngüde yaklaşık aynı miktarda iş yapabilmeniz için kodunuzu yazmaktır. Bunu, her seferinde tabloyu baştan taramak yerine dizinden yararlanarak yapabilirsiniz. Bazı farklı döngü yöntemlerinin üzerinden geçen bir blog yazısı yazdım . Bu gönderideki örnekler, silmek yerine tabloya eklenir, ancak kodu uyarlayabilmeniz gerekir.

Aşağıdaki örnek kodda, birincil anahtarın ve kümelenmiş anahtarınızın olduğunu varsayalım MySourceTable. Bu kodu oldukça hızlı bir şekilde yazdım ve test edemiyorum:

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

Anahtar kısım burada:

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

Her döngü yalnızca 60000 satırı okuyacaktır MySourceTable. Bu işlem başına ortalama 4500 satırlık silme boyutu ve işlem başına maksimum 60000 satırlık silme boyutu ile sonuçlanmalıdır. Daha küçük bir parti boyutu ile daha muhafazakar olmak istiyorsanız, bu da iyi. @STARTIDHer döngü sonra değişken gelişmeler daha kaynak tablodan birden fazla kez aynı satır okuma önlemek, böylece.


Detaylı bilgi için teşekkürler. 4500 limitini masa kilitlenmeyecek şekilde ayarladım. Yanılmıyorsam SQL, silme sayısı 5000'in üzerine çıkarsa tüm tabloyu kilitleyen sabit bir sınıra sahiptir. Ve bu uzun bir süreç olacağı için bu tabloyu uzun süre kilitlemek için çabalayamam. Eğer 60000'ü 4500'e ayarlarsam, aynı performansı alacağımı mı düşünüyorsun?
cilerler

@cilerler Kilit yükseltme konusunda endişeleriniz varsa, bunu tablo düzeyinde devre dışı bırakabilirsiniz. 4500'lük bir toplu iş boyutu kullanmanın yanlış bir yanı yoktur. Anahtar, her döngünün kabaca aynı miktarda iş yapmasıdır.
Joe Obbish

Hız farklılıkları nedeniyle başka bir cevabı kabul etmek zorundayım. Çözümünüzü ve @ Martin-Smith'in çözümünü test ettim ve sürümü 10 dakikalık bir test için ~% 2 daha fazla veri alıyor. Çözümleriniz benimkinden çok daha iyi ve zaman ayırdığınız için gerçekten minnettarım ... -
cilerler

2

İki düşünce akla geliyor:

Gecikme muhtemelen bu veri hacmi ile endekslenmeden kaynaklanmaktadır. Dizinleri bırakmayı, dizinleri silmeyi ve yeniden oluşturmayı deneyin.

Veya..

Saklamak istediğiniz satırları geçici bir tabloya kopyalamak, tabloyu 16 milyon satırla bırakmak ve geçici tabloyu yeniden adlandırmak (veya kaynak tablonun yeni örneğine kopyalamak) daha hızlı olabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.