คำสั่ง DELETE ขัดแย้งกับข้อ จำกัด การอ้างอิง


10

สถานการณ์ของฉันเป็นดังนี้:

ตาราง STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

ตาราง LOCATION:

ID *[PK]*
LOCATION_NAME

ตาราง WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

ตาราง INVENTORY_ITEMS:

ID *[PK]*
ITEM_NAME
STOCK_ARTICLE *[FK]*
LOCATION *[FK]*
WORK_PLACE *[FK]*

3 FKs ใน INVENTORY_ITEMS อ้างอิงคอลัมน์ "ID" ในตารางอื่น ๆ ที่เกี่ยวข้องอย่างชัดเจน

ตารางที่เกี่ยวข้องที่นี่คือ STOCK_ARTICLE และ INVENTORY_ITEMS

ขณะนี้มีงาน SQL ประกอบด้วยหลายขั้นตอน (สคริปต์ SQL) ที่ "ประสาน" ฐานข้อมูลที่กล่าวถึงข้างต้นกับฐานข้อมูลอื่น (OTHER_DB) หนึ่งในขั้นตอนในงานนี้คือ "การล้างข้อมูล" มันจะลบระเบียนทั้งหมดจาก STOCK_ITEMS ที่ไม่มีระเบียนที่สอดคล้องกันในฐานข้อมูลอื่นที่มี ID เดียวกัน ดูเหมือนว่านี้:

DELETE FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID)

แต่ขั้นตอนนี้มักจะล้มเหลวด้วย:

คำสั่ง DELETE ขัดแย้งกับข้อ จำกัด REFERENCE "FK_INVENTORY_ITEMS_STOCK_ARTICLES" ความขัดแย้งเกิดขึ้นในฐานข้อมูล "FIRST_DB", ตาราง "dbo.INVENTORY_ITEMS", คอลัมน์ 'STOCK_ARTICLES' [SQLSTATE 23000] (ข้อผิดพลาด 547) คำสั่งถูกยกเลิก [SQLSTATE 01000] (ข้อผิดพลาด 3621) ขั้นตอนล้มเหลว

ดังนั้นปัญหาคือมันไม่สามารถลบบันทึกจาก STOCK_ARTICLES เมื่อพวกเขาถูกอ้างอิงโดย INVENTORY_ITEMS แต่การล้างข้อมูลนี้จำเป็นต้องทำงาน ซึ่งหมายความว่าฉันอาจต้องขยายสคริปต์การล้างข้อมูลเพื่อให้ระบุระเบียนที่ควรลบออกจาก STOCK_ITEMS ก่อน แต่ไม่สามารถทำได้เนื่องจากรหัสที่เกี่ยวข้องนั้นอ้างอิงจากภายใน INVENTORY_ITEMS จากนั้นจึงควรลบระเบียนเหล่านั้นภายใน INVENTORY_ITEMS และหลังจากนั้นลบระเบียนภายใน STOCK_ARTICLES ฉันถูกไหม? รหัส SQL จะเป็นอย่างไร

ขอบคุณ.

คำตอบ:


13

นั่นคือจุดรวมของข้อ จำกัด ของคีย์ต่างประเทศ: พวกเขาหยุดคุณลบข้อมูลที่อ้างถึงที่อื่นเพื่อรักษาความสมบูรณ์ของการอ้างอิง

มีสองตัวเลือก:

  1. ลบแถวจากINVENTORY_ITEMSก่อนจากนั้นแถวจากSTOCK_ARTICLESแถวจาก
  2. ใช้ON DELETE CASCADEสำหรับในคำนิยามที่สำคัญ

1: การลบในลำดับที่ถูกต้อง

วิธีที่มีประสิทธิภาพมากที่สุดในการทำเช่นนี้แตกต่างกันไปขึ้นอยู่กับความซับซ้อนของแบบสอบถามที่ตัดสินใจว่าจะลบแถวใด รูปแบบทั่วไปอาจเป็น:

BEGIN TRANSACTION
SET XACT_ABORT ON
DELETE INVENTORY_ITEMS WHERE STOCK_ARTICLE IN (<select statement that returns stock_article.id for the rows you are about to delete>)
DELETE STOCK_ARTICLES WHERE <the rest of your current delete statement>
COMMIT TRANSACTION

นี่เป็นเรื่องปกติสำหรับการสืบค้นแบบง่าย ๆ หรือการลบรายการสต็อคเดียว แต่เนื่องจากคำสั่งลบของคุณมีWHERE NOT EXISTSประโยคที่ซ้อนอยู่ภายในWHERE INอาจสร้างแผนที่ไม่มีประสิทธิภาพมากดังนั้นทดสอบด้วยขนาดชุดข้อมูลที่เป็นจริงและจัดเรียงแบบสอบถามใหม่หากจำเป็น

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

2: ใช้ ON DELETE CASCADE

หากคุณเพิ่มตัวเลือกการเรียงซ้อนลงใน foreign key ของคุณแล้ว SQL Server จะทำสิ่งนี้ให้คุณโดยอัตโนมัติการลบแถวออกINVENTORY_ITEMSเพื่อให้เป็นไปตามข้อ จำกัด ที่ไม่มีสิ่งใดที่ควรอ้างถึงแถวที่คุณกำลังลบ เพียงเพิ่มON DELETE CASCADEคำจำกัดความของ FK ดังนี้:

ALTER TABLE <child_table> WITH CHECK 
ADD CONSTRAINT <fk_name> FOREIGN KEY(<column(s)>)
REFERENCES <parent_table> (<column(s)>)
ON DELETE CASCADE

ข้อได้เปรียบของที่นี่คือการลบคือการลดคำสั่งแบบอะตอมมิก (แต่ตามปกติไม่ใช่การลบแบบ 100%) ที่คุณต้องกังวลเกี่ยวกับการตั้งค่าธุรกรรมและการล็อก การเรียงซ้อนยังสามารถทำงานบนหลายพาเรนต์ / child / grand-child / ... หลายระดับหากมีเพียงพา ธ เดียวระหว่างพาเรนต์และสืบทอดทั้งหมด (ค้นหา "หลายพา ธ การเรียงซ้อน" สำหรับตัวอย่างของตำแหน่งที่อาจไม่ทำงาน)

หมายเหตุ: ฉันและคนอื่น ๆ พิจารณาว่าการลบเรียงซ้อนเป็นอันตรายดังนั้นหากคุณใช้ตัวเลือกนี้ให้ระมัดระวังในการจัดทำเอกสารอย่างถูกต้องในการออกแบบฐานข้อมูลของคุณเพื่อให้คุณและนักพัฒนาอื่น ๆไม่สะดุดอันตรายในภายหลัง ฉันหลีกเลี่ยงการลบซ้อนเมื่อทำได้ด้วยเหตุผลนี้

ปัญหาที่พบบ่อยเกิดจากการที่มีการลบลดหลั่นกันคือเมื่อมีคนอัปเดตข้อมูลโดยการวางและสร้างแถวแทนการใช้หรือUPDATE MERGEสิ่งนี้มักจะเห็นเมื่อต้องการ "อัปเดตแถวที่มีอยู่แล้วแทรกแถวที่ไม่มี" (บางครั้งเรียกว่าการดำเนินการของ UPSERT) และผู้ที่ไม่ทราบMERGEคำสั่งจะพบว่าทำได้ง่ายขึ้น:

DELETE <all rows that match IDs in the new data>
INSERT <all rows from the new data>

กว่า

-- updates
UPDATE target 
SET    <col1> = source.<col1>
  ,    <col2> = source.<col2>
       ...
  ,    <colN> = source.<colN>
FROM   <target_table> AS target JOIN <source_table_or_view_or_statement> AS source ON source.ID = target.ID
-- inserts
INSERT  <target_table>
SELECT  *
FROM    <source_table_or_other> AS source
LEFT OUTER JOIN
        <target_table> AS target
        ON target.ID = source.ID
WHERE   target.ID IS NULL

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

สรุป

ใช่คุณต้องลบแถวลูกก่อน

มีตัวเลือกอื่น: ON DELETE CASCADE.

แต่ON DELETE CASCADEอาจเป็นอันตรายได้ได้ดังนั้นควรใช้ด้วยความระมัดระวัง

บันทึก Side: การใช้งานMERGE(หรือUPDATEโทโพโลยีINSERTที่MERGEไม่สามารถใช้ได้) เมื่อคุณจำเป็นต้องมีUPSERTการดำเนินการไม่ DELETE -then แทนที่-with- INSERTเพื่อหลีกเลี่ยงการตกอยู่ในกับดักวางโดยคนอื่น ๆ ON DELETE CASCADEที่ใช้


2

คุณสามารถรับ ID เพื่อลบเพียงครั้งเดียวเก็บไว้ในตารางชั่วคราวและใช้เพื่อลบการดำเนินการ จากนั้นคุณสามารถควบคุมสิ่งที่คุณลบได้ดีขึ้น

การดำเนินการนี้ไม่ควรล้มเหลว:

SELECT sa.ID INTO #StockToDelete
FROM STOCK_ARTICLES sa
LEFT JOIN [OTHER_DB].[dbo].[OtherTable] other ON other.ObjectID = sa.OTHER_DB_ID
WHERE other.ObjectID IS NULL

DELETE ii
FROM INVENTORY_ITEMS ii
JOIN #StockToDelete std ON ii.STOCK_ARTICLE = std.ID

DELETE sa
FROM STOCK_ARTICLES sa
JOIN #StockToDelete std ON sa.ID = std.ID

2
แม้ว่าการลบแถวจำนวนมากของ STOCK_ARTICLES มันน่าจะมีประสิทธิภาพที่แย่กว่าตัวเลือกอื่น ๆ เนื่องจากการสร้างตารางชั่วคราว (สำหรับแถวจำนวนน้อยความแตกต่างก็ไม่น่าจะมีนัยสำคัญ) ระวังการใช้คำสั่งการทำธุรกรรมที่เหมาะสมเพื่อให้แน่ใจว่าคำสั่งทั้งสามจะถูกดำเนินการเป็นหน่วยอะตอมหากการเข้าถึงพร้อมกันนั้นเป็นไปไม่ได้มิฉะนั้นคุณอาจเห็นข้อผิดพลาดว่าINVENTORY_ITEMSเป็นการเพิ่มใหม่ระหว่างสองDELETEรายการ
David Spillett

1

ฉันพบปัญหานี้ด้วยและฉันก็สามารถแก้ไขได้ นี่คือสถานการณ์ของฉัน:

ในกรณีของฉันฉันมีฐานข้อมูลที่ใช้สำหรับการรายงานการวิเคราะห์ (MYTARGET_DB) ซึ่งดึงมาจากระบบต้นทาง (MYSOURCE_DB) ตาราง 'MYTARGET_DB' บางตารางไม่ซ้ำกันสำหรับระบบนั้นและข้อมูลถูกสร้างและจัดการที่นั่น ตารางส่วนใหญ่มาจาก 'MYSOURCE_DB' และมีงานที่ลบ / แทรกข้อมูลลงใน 'MYTARGET_DB' จาก 'MYSOURCE_DB'

หนึ่งในตารางการค้นหา [PRODUCT] มาจากแหล่งที่มาและมีตารางข้อมูล [InventoryOutsourced] เก็บไว้ใน TARGET มี Referential Integrity ออกแบบลงในตาราง ดังนั้นเมื่อฉันพยายามเรียกใช้การลบ / ใส่ฉันได้รับข้อความนี้

Msg 50000, Level 16, State 1, Procedure uspJobInsertAllTables_AM, Line 249
The DELETE statement conflicted with the REFERENCE constraint "FK_InventoryOutsourced_Product". The conflict occurred in database "ProductionPlanning", table "dbo.InventoryOutsourced", column 'ProdCode'.

วิธีแก้ปัญหาที่ฉันสร้างขึ้นคือการแทรกข้อมูลลงในตัวแปรตาราง [@tempTable] จาก [InventoryOutsourced], ลบข้อมูลใน [InventoryOutsourced], เรียกใช้งานซิงก์, แทรกลงใน [InventoryOutsourced] จาก [@tempTable] สิ่งนี้จะช่วยรักษาความสมบูรณ์ในสถานที่และการเก็บรวบรวมข้อมูลที่ไม่ซ้ำใคร ซึ่งเป็นสิ่งที่ดีที่สุดของทั้งสองโลก หวังว่านี่จะช่วยได้

BEGIN TRY
    BEGIN TRANSACTION InsertAllTables_AM

        DECLARE
        @BatchRunTime datetime = getdate(),
        @InsertBatchId bigint
            select @InsertBatchId = max(IsNull(batchid,0)) + 1 from JobRunStatistic 

        --<DataCaptureTmp/> Capture the data tables unique to this database, before deleting source system reference tables
            --[InventoryOutsourced]
            DECLARE @tmpInventoryOutsourced as table (
                [ProdCode]      VARCHAR (12)    NOT NULL,
                [WhseCode]      VARCHAR (4)     NOT NULL,
                [Cases]          NUMERIC (8)     NOT NULL,
                [Weight]         NUMERIC (10, 2) NOT NULL,
                [Date] DATE NOT NULL, 
                [SourcedFrom] NVARCHAR(50) NOT NULL, 
                [User] NCHAR(50) NOT NULL, 
                [ModifiedDatetime] DATETIME NOT NULL
                )

            INSERT INTO @tmpInventoryOutsourced (
                [ProdCode]
               ,[WhseCode]
               ,[Cases]
               ,[Weight]
               ,[Date]
               ,[SourcedFrom]
               ,[User]
               ,[ModifiedDatetime]
               )
            SELECT 
                [ProdCode]
                ,[WhseCode]
                ,[Cases]
                ,[Weight]
                ,[Date]
                ,[SourcedFrom]
                ,[User]
                ,[ModifiedDatetime]
            FROM [dbo].[InventoryOutsourced]

            DELETE FROM [InventoryOutsourced]
        --</DataCaptureTmp> 

... Delete Processes
... Delete Processes    

        --<DataCaptureInsert/> Capture the data tables unique to this database, before deleting source system reference tables
            --[InventoryOutsourced]
            INSERT INTO [dbo].[InventoryOutsourced] (
                [ProdCode]
               ,[WhseCode]
               ,[Cases]
               ,[Weight]
               ,[Date]
               ,[SourcedFrom]
               ,[User]
               ,[ModifiedDatetime]
               )
            SELECT 
                [ProdCode]
                ,[WhseCode]
                ,[Cases]
                ,[Weight]
                ,[Date]
                ,[SourcedFrom]
                ,[User]
                ,[ModifiedDatetime]
            FROM @tmpInventoryOutsourced
            --</DataCaptureInsert> 

    COMMIT TRANSACTION InsertAllTables_AM
END TRY

0

ฉันยังไม่ได้ทดสอบอย่างสมบูรณ์ แต่สิ่งนี้ควรได้ผล

--cte of Stock Articles to be deleted
WITH StockArticlesToBeDeleted AS
(
SELECT ID FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID)
)
--delete from INVENTORY_ITEMS where we have a match on deleted STOCK_ARTICLE
DELETE a FROM INVENTORY_ITEMS a join
StockArticlesToBeDeleted b on
    b.ID = a.STOCK_ARTICLE;

--now, delete from STOCK_ARTICLES
DELETE FROM STOCK_ARTICLES
 WHERE
    NOT EXISTS
     (SELECT OTHER_DB_ID FROM
     [OTHER_DB].[dbo].[OtherTable] AS other
               WHERE other.ObjectID = STOCK_ARTICLES.OTHER_DB_ID);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.