ลบล้านแถวออกจากตาราง SQL


9

ฉันต้องลบมากกว่า 16 ล้านระเบียนจากตารางแถว 221 ล้านและมันจะช้ามาก

ฉันขอขอบคุณถ้าคุณแบ่งปันคำแนะนำในการทำโค้ดด้านล่างให้เร็วขึ้น:

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

แผนปฏิบัติการ(จำกัด ไว้เพียง 2 ครั้งเท่านั้น)

ป้อนคำอธิบายรูปภาพที่นี่

VendorIdคือPKและไม่ทำคลัสเตอร์โดยที่ดัชนีคลัสเตอร์ไม่ได้ถูกใช้งานโดยสคริปต์นี้ มีดัชนีอื่น ๆ ที่ไม่ซ้ำกันและไม่ทำคลัสเตอร์ 5 ตัว

ภารกิจคือ "การนำผู้ขายออกซึ่งไม่มีอยู่ในตารางอื่น" และสำรองข้อมูลนั้นลงในตารางอื่น ฉันมี 3 vendors, SpecialVendors, SpecialVendorBackupsตาราง พยายามที่จะลบSpecialVendorsซึ่งไม่มีอยู่ในVendorsตารางและมีการสำรองข้อมูลของระเบียนที่ถูกลบในกรณีที่สิ่งที่ฉันทำผิดและฉันต้องนำพวกเขากลับมาในหนึ่งหรือสองสัปดาห์


ฉันจะพยายามหาคำตอบให้ดีที่สุดและลองใช้การเข้าร่วมซ้ายโดยไม่มีค่า
paparazzo

คำตอบ:


8

แผนการดำเนินการแสดงให้เห็นว่ากำลังอ่านแถวจากดัชนีที่ไม่เป็นคลัสเตอร์ในบางคำสั่งจากนั้นดำเนินการค้นหาสำหรับแต่ละแถวด้านนอกที่อ่านเพื่อประเมินค่า NOT EXISTS

ป้อนคำอธิบายรูปภาพที่นี่

คุณกำลังลบ 7.2% ของตาราง 16,000,000 แถวใน 3,556 แบทช์ 4,500

สมมติว่าแถวที่ผ่านการรับรองนั้นมีการกระจายไปทั่วดัชนีอย่างชัดเจนนั่นหมายความว่ามันจะลบประมาณ 1 แถวทุก ๆ 13.8 แถว

ดังนั้นการวนซ้ำ 1 จะอ่านแถว 62,156 แถวและดำเนินการกับดัชนีจำนวนมากที่ค้นหาก่อนที่จะพบ 4,500 ลบ

การวนซ้ำ 2 จะอ่านแถว 57,656 (62,156 - 4,500) ซึ่งแน่นอนว่าจะไม่ผ่านการเพิกเฉยต่อการอัปเดตพร้อมกัน

การวนซ้ำ 3 จะอ่าน (2 * 57,656) + 62,156 แถวและต่อไปเรื่อย ๆ จนในที่สุดการวนซ้ำ 3,556 จะอ่าน (3,555 * 57,656) + 62,156 แถวและทำการค้นหาหลายครั้ง

ดังนั้นจำนวนดัชนีพยายามดำเนินการในทุกกระบวนการคือ SUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)

อันไหน((3555 * 3556 / 2) * 57656) + (3556 * 62156)- หรือ364,652,494,976

ฉันขอแนะนำให้คุณสร้างแถวให้เป็นรูปธรรมเพื่อลบลงในตารางชั่วคราวก่อน

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) 

และเปลี่ยนการDELETEลบWHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)คุณอาจยังต้องรวม a NOT EXISTSในDELETEแบบสอบถามเพื่อรองรับการอัปเดตเนื่องจากตาราง temp นั้นมีการบรรจุ แต่ควรมีประสิทธิภาพมากกว่าเนื่องจากจะต้องดำเนินการ 4,500 ครั้งต่อชุดเท่านั้น


เมื่อคุณพูดว่า "ทำให้แถวเป็นรูปเป็นร่างเพื่อลบลงในตารางชั่วคราวก่อน" คุณแนะนำให้วางระเบียนเหล่านั้นทั้งหมดด้วยคอลัมน์ทั้งหมดลงในตารางชั่วคราวหรือไม่ หรือเฉพาะPKคอลัมน์? (ฉันเชื่อว่าคุณกำลังแนะนำให้ฉันย้ายคนเหล่านั้นไปยังตารางชั่วคราวอย่างสมบูรณ์ แต่ต้องการตรวจสอบอีกครั้ง)
cilerler

@cilerler - เพียงคอลัมน์สำคัญ
Martin Smith

คุณสามารถแสดงความคิดเห็นได้อย่างรวดเร็วนี้ถ้าผมได้รับสิ่งที่คุณกล่าวว่าถูกต้องหรือไม่โปรด?
cilerler

@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTableเพียงแค่ควรจะจัดทำDELETE FROM MySourceTable ดัชนีตาราง temp CREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );และVendorIdแน่นอน PK ด้วยตัวเอง? คุณมีผู้จำหน่ายมากกว่า 22 ล้านคนใช่ไหม
Martin Smith

ขอบคุณ Martin จะทดสอบหลัง 18.00 น. และคำตอบของคุณคือแน่นอนว่ามีเพียง PK เท่านั้นที่มีอยู่ในตารางนั้น
cilerler

4

แผนการดำเนินการแสดงให้เห็นว่าแต่ละวงที่ต่อเนื่องจะทำงานได้มากกว่าวงที่ผ่านมา สมมติว่าแถวที่จะลบกระจายอย่างเท่าเทียมกันทั่วทั้งตารางวงแรกจะต้องสแกนประมาณ 4500 * 221000000/16000000 = 62156 แถวเพื่อหา 4500 แถวที่จะลบ นอกจากนี้ยังจะทำดัชนีดัชนีกลุ่มเดียวกันกับvendorตาราง อย่างไรก็ตามการวนซ้ำครั้งที่สองจะต้องอ่านผ่านแถวเดียวกัน 62156 - 4500 = 57656 ที่คุณไม่ได้ลบในครั้งแรก เราอาจคาดว่าวงที่สองจะสแกน 120000 แถวจากMySourceTableและเพื่อทำ 120000 ค้นหากับvendorตาราง ปริมาณงานที่ต้องการต่อการวนซ้ำเพิ่มขึ้นในอัตราเชิงเส้น ในการประมาณค่าเราสามารถพูดได้ว่าลูปเฉลี่ยจะต้องอ่าน 102516868 แถวจากMySourceTableและต้องทำ 102516868 ค้นหากับvendorโต๊ะ. หากต้องการลบ 16 ล้านแถวที่มีขนาดชุด 4500 รหัสของคุณจำเป็นต้องทำ 16000000/4500 = 3556 ลูปดังนั้นจำนวนการทำงานทั้งหมดสำหรับรหัสของคุณที่จะเสร็จสมบูรณ์นั้นอยู่ที่ประมาณ 364.5 พันล้านแถวอ่านจากMySourceTableและดัชนีค้นหา 364.5 พันล้านครั้ง

ปัญหาที่เล็กกว่าคือคุณใช้ตัวแปรโลคอล@BATCHSIZEในนิพจน์ TOP โดยไม่มีRECOMPILEคำใบ้อื่น ๆ เครื่องมือเพิ่มประสิทธิภาพการสืบค้นจะไม่ทราบค่าของตัวแปรท้องถิ่นนั้นเมื่อสร้างแผน มันจะสมมติว่ามันเท่ากับ 100 ในความเป็นจริงคุณกำลังลบ 4500 แถวแทนที่จะเป็น 100 และคุณอาจจะจบลงด้วยแผนการที่มีประสิทธิภาพน้อยลงเนื่องจากความคลาดเคลื่อนนั้น การคาดคะเนความมีระดับต่ำเมื่อแทรกเข้าไปในตารางอาจทำให้ประสิทธิภาพการทำงานลดลงเช่นกัน SQL Server อาจเลือก API ภายในที่แตกต่างกันเพื่อแทรกหากคิดว่าจำเป็นต้องแทรก 100 แถวเมื่อเทียบกับ 4500 แถว

ทางเลือกหนึ่งคือเพียงใส่คีย์หลัก / คีย์คลัสเตอร์ของแถวที่คุณต้องการลบลงในตารางชั่วคราว ขึ้นอยู่กับขนาดของคอลัมน์หลักของคุณสิ่งนี้สามารถพอดีกับ tempdb ได้อย่างง่ายดาย คุณสามารถเข้าสู่ระบบในกรณีที่น้อยที่สุดซึ่งหมายความว่าบันทึกธุรกรรมจะไม่ระเบิด นอกจากนี้คุณยังจะได้รับการเข้าสู่ระบบน้อยที่สุดเมื่อเทียบกับฐานข้อมูลใด ๆ SIMPLEที่มีรูปแบบการฟื้นตัวของ ดูลิงค์สำหรับข้อมูลเพิ่มเติมเกี่ยวกับข้อกำหนด

หากไม่ใช่ตัวเลือกคุณควรเปลี่ยนรหัสของคุณเพื่อให้สามารถใช้ประโยชน์จากดัชนีคลัสเตอร์MySourceTableได้ สิ่งสำคัญคือการเขียนรหัสของคุณเพื่อให้คุณทำงานประมาณต่อวงเท่ากัน คุณสามารถทำได้โดยใช้ประโยชน์จากดัชนีแทนที่จะสแกนตารางตั้งแต่ต้นทุกครั้ง ฉันเขียนโพสต์บล็อกที่ใช้วิธีการวนลูปต่างกัน ตัวอย่างในโพสต์นั้นแทรกลงในตารางแทนที่จะลบ แต่คุณควรจะสามารถปรับเปลี่ยนรหัสได้

MySourceTableในรหัสตัวอย่างด้านล่างฉันคิดว่าคีย์หลักและที่สำคัญคลัสเตอร์ของคุณ ฉันเขียนโค้ดนี้ค่อนข้างเร็วและไม่สามารถทดสอบได้:

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

ส่วนสำคัญอยู่ที่นี่:

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

แต่ละวงจะอ่าน 60000 MySourceTableแถวจาก ซึ่งควรส่งผลให้มีขนาดการลบเฉลี่ย 4500 แถวต่อธุรกรรมและขนาดลบสูงสุด 60000 แถวต่อธุรกรรม ถ้าคุณต้องการที่จะอนุรักษ์มากขึ้นด้วยขนาดแบทช์ที่เล็กลงก็ใช้ได้เช่นกัน @STARTIDก้าวหน้าตัวแปรหลังจากที่แต่ละวงเพื่อให้คุณสามารถหลีกเลี่ยงการอ่านแถวเดียวกันมากกว่าหนึ่งครั้งจากตารางแหล่งที่มา


ขอบคุณสำหรับข้อมูลรายละเอียด ฉันตั้งค่าขีด จำกัด นั้นไว้ที่ 4,500 ไม่ล็อคตาราง ถ้าฉันไม่เข้าใจผิด SQL มีขีด จำกัด ที่ยากที่ล็อกทั้งตารางถ้าจำนวนลบลบเกิน 5,000 และเนื่องจากนี่จะเป็นกระบวนการที่ยาวนานฉันจึงไม่สามารถล็อกตารางนั้นเป็นเวลานานได้ หากฉันตั้งค่านั้นไว้ที่ 60000 ถึง 4500 คุณคิดว่าฉันจะได้รับประสิทธิภาพเดียวกันหรือไม่
cilerler

@cilerler หากคุณกังวลเรื่องการเลื่อนล็อคคุณสามารถปิดการใช้งานที่ระดับตาราง ไม่มีอะไรผิดปกติเมื่อใช้ขนาดแบทช์เป็น 4,500 คีย์คือแต่ละลูปจะทำงานในปริมาณเท่ากัน
โจ Obbish

ฉันต้องยอมรับคำตอบอื่น ๆ เนื่องจากความแตกต่างของความเร็ว ฉันทดสอบโซลูชันของคุณและโซลูชันของ @ Martin-Smith และเวอร์ชันของเขาได้รับข้อมูลเพิ่มขึ้น ~ 2% เป็นเวลา 10 นาทีในการทดสอบ ทางออกของคุณดีกว่าของฉันมากและฉันขอขอบคุณสำหรับเวลาของคุณ ... -
cilerler

2

ความคิดสองประการเกิดขึ้นในใจ:

ความล่าช้าอาจเกิดจากการจัดทำดัชนีด้วยปริมาณข้อมูลนั้น ลองวางดัชนีลบและสร้างดัชนีขึ้นใหม่

หรือ..

อาจเร็วกว่าการคัดลอกแถวที่คุณต้องการเก็บไว้ในตารางชั่วคราววางตารางด้วย 16 ล้านแถวและเปลี่ยนชื่อตารางชั่วคราว (หรือคัดลอกไปที่อินสแตนซ์ใหม่ของตารางต้นฉบับ)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.