Tuyên bố XÓA mâu thuẫn với ràng buộc TÀI LIỆU THAM KHẢO


10

Tình hình của tôi như thế này:

Bảng STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

Bảng VỊ TRÍ:

ID *[PK]*
LOCATION_NAME

Bảng WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

Bảng INVENTORY_ITEMS:

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

Rõ ràng, 3 FK trong INVENTORY_ITEMS tham chiếu các cột "ID" trong các bảng khác tương ứng.

Các bảng có liên quan ở đây là STOCK_ARTICLE và INVENTORY_ITEMS.

Bây giờ có một công việc SQL bao gồm một số bước (tập lệnh SQL) "đồng bộ hóa" cơ sở dữ liệu được đề cập ở trên với cơ sở dữ liệu khác (OTHER_DB). Một trong những bước trong công việc này là "dọn dẹp". Nó xóa tất cả các bản ghi khỏi STOCK_ITEMS khi không có bản ghi tương ứng trong cơ sở dữ liệu khác có cùng ID. Nó trông như thế này:

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)

Nhưng bước này luôn thất bại với:

Câu lệnh XÓA đã xung đột với ràng buộc TÀI LIỆU THAM KHẢO "FK_INVENTORY_ITEMS_STOCK_ARTICLES". Xung đột xảy ra trong cơ sở dữ liệu "FIRST_DB", bảng "dbo.INVENTORY_ITEMS", cột 'STOCK_ARTICLES'. [SQLSTATE 23000] (Lỗi 547) Câu lệnh đã bị chấm dứt. [SQLSTATE 01000] (Lỗi 3621). Bước thất bại.

Vì vậy, vấn đề là nó không thể xóa các bản ghi khỏi STOCK_ARTICLES khi chúng được tham chiếu bởi INVENTORY_ITEMS. Nhưng dọn dẹp này cần phải làm việc. Điều đó có nghĩa là tôi có thể phải mở rộng tập lệnh dọn dẹp để trước tiên nó xác định các bản ghi cần xóa khỏi STOCK_ITEMS, nhưng không thể vì ID tương ứng được tham chiếu từ bên trong INVENTORY_ITEMS. Sau đó, trước tiên, nó sẽ xóa các bản ghi đó trong INVENTORY_ITEMS và sau đó xóa các bản ghi bên trong STOCK_ARTICLES. Tôi có đúng không Mã SQL sẽ trông như thế nào sau đó?

Cảm ơn bạn.

Câu trả lời:


13

Đó là toàn bộ điểm hạn chế của khóa ngoại: chúng ngăn bạn xóa dữ liệu được đề cập ở nơi khác để duy trì tính toàn vẹn tham chiếu.

Có hai lựa chọn:

  1. Xóa các hàng từ INVENTORY_ITEMSđầu tiên, sau đó các hàng từ STOCK_ARTICLES.
  2. Sử dụng ON DELETE CASCADEcho định nghĩa chính.

1: Xóa theo đúng thứ tự

Cách hiệu quả nhất để thực hiện việc này thay đổi tùy thuộc vào độ phức tạp của truy vấn quyết định hàng nào sẽ xóa. Một mô hình chung có thể là:

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

Điều này tốt cho các truy vấn đơn giản hoặc để xóa một mục chứng khoán, nhưng với câu lệnh xóa của bạn có chứa một WHERE NOT EXISTSmệnh đề lồng trong đó WHERE INcó thể tạo ra một kế hoạch rất không hiệu quả, vì vậy hãy kiểm tra kích thước tập dữ liệu thực tế và sắp xếp lại truy vấn nếu cần.

Cũng lưu ý các báo cáo giao dịch: bạn muốn đảm bảo cả hai quá trình xóa hoàn thành hoặc không thực hiện được. Nếu hoạt động đã xảy ra trong một giao dịch, rõ ràng bạn sẽ cần phải thay đổi điều này để phù hợp với quy trình xử lý lỗi và giao dịch hiện tại của bạn.

2: Sử dụng ON DELETE CASCADE

Nếu bạn thêm tùy chọn xếp tầng vào khóa ngoại thì SQL Server sẽ tự động làm điều này cho bạn, loại bỏ các hàng khỏi INVENTORY_ITEMSđể đáp ứng các ràng buộc mà không có gì nên tham chiếu đến các hàng bạn đang xóa. Chỉ cần thêm ON DELETE CASCADEvào định nghĩa FK như vậy:

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

Một lợi thế ở đây là việc xóa là giảm một câu lệnh nguyên tử (mặc dù, như thường lệ, không xóa 100%), cần phải lo lắng về các cài đặt giao dịch và khóa. Dòng thác thậm chí có thể hoạt động trên nhiều cấp độ cha / con / cháu / ... nếu chỉ có một đường dẫn giữa cha mẹ và tất cả con cháu (tìm kiếm "nhiều đường dẫn tầng" để biết ví dụ về nơi mà điều này có thể không hoạt động).

LƯU Ý: Tôi và nhiều người khác coi việc xóa xếp tầng là nguy hiểm vì vậy nếu bạn sử dụng tùy chọn này, hãy cẩn thận ghi lại đúng vào thiết kế cơ sở dữ liệu của bạn để bạn và các nhà phát triển khác không gặp phải nguy hiểm sau này . Tôi tránh xóa tầng bất cứ nơi nào có thể vì lý do này.

Một vấn đề phổ biến gây ra với xóa tầng là khi ai đó cập nhật dữ liệu bằng cách thả và tạo lại các hàng thay vì sử dụng UPDATEhoặc MERGE. Điều này thường được thấy trong đó "cập nhật các hàng đã tồn tại, chèn những hàng không" (đôi khi được gọi là hoạt động UPSERT) và mọi người không biết về MERGEtuyên bố thấy dễ thực hiện hơn:

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

hơn

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

Vấn đề ở đây là câu lệnh xóa sẽ xếp thành các hàng con và câu lệnh chèn sẽ không tạo lại chúng, vì vậy trong khi cập nhật bảng cha, bạn vô tình làm mất dữ liệu từ (các) bảng con.

Tóm lược

Có, bạn phải xóa các hàng con đầu tiên.

Có một lựa chọn khác : ON DELETE CASCADE.

Nhưng ON DELETE CASCADEcó thể nguy hiểm , vì vậy sử dụng cẩn thận.

Lưu ý bên lề: sử dụng MERGE(hoặc - UPDATEvà - INSERTnơi MERGEkhông có sẵn) khi bạn cần một UPSERTthao tác, không DELETE - thay thế - bằng - INSERTđể tránh rơi vào bẫy do người khác sử dụng ON DELETE CASCADE.


2

Bạn chỉ có thể nhận ID để xóa một lần, lưu trữ chúng trong bảng tạm thời và sử dụng để xóa các thao tác. Sau đó, bạn có kiểm soát tốt hơn những gì bạn đang xóa.

Thao tác này không nên thất bại:

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
Mặc dù nếu xóa số lượng lớn các hàng STOCK_ARTICLES, điều này có khả năng thực hiện kém hơn các tùy chọn khác do xây dựng bảng tạm thời (đối với số lượng nhỏ các hàng, sự khác biệt dường như không đáng kể). Ngoài ra, hãy cẩn thận sử dụng các chỉ thị giao dịch phù hợp để đảm bảo rằng ba câu lệnh được thực thi dưới dạng một đơn vị nguyên tử nếu không thể truy cập đồng thời, nếu không bạn có thể thấy các lỗi như mới INVENTORY_ITEMSđược thêm vào giữa hai DELETEs.
David Spillett

1

Tôi cũng gặp phải vấn đề này và tôi đã có thể giải quyết nó. Đây là tình huống của tôi:

Trong trường hợp của tôi, tôi có một cơ sở dữ liệu được sử dụng để báo cáo phân tích (MYTARGET_DB), lấy từ hệ thống nguồn (MYSOURCE_DB). Một số bảng 'MYTARGET_DB' là duy nhất cho hệ thống đó và dữ liệu được tạo & quản lý ở đó; Hầu hết các bảng là từ 'MYSOURCE_DB' và có một công việc xóa / chèn dữ liệu vào 'MYTARGET_DB' từ 'MYSOURCE_DB'.

Một trong các bảng tra cứu [SẢN PHẨM] là từ NGUỒN và có một bảng dữ liệu [InventoryOutsourced] được lưu trữ trong MỤC TIÊU. Có tính toàn vẹn tham chiếu được thiết kế vào các bảng. Vì vậy, khi tôi cố gắng chạy xóa / chèn, tôi nhận được thông báo này.

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'.

Cách giải quyết mà tôi đã tạo là chèn dữ liệu vào biến bảng [@tempTable] từ [InventoryOutsourced], xóa dữ liệu trong [InventoryOutsourced], chạy các công việc đồng bộ hóa, chèn vào [InventoryOutsourced] từ [@tempTable]. Điều này giữ cho tính toàn vẹn tại chỗ và bộ sưu tập dữ liệu duy nhất cũng được giữ lại. Đó là tốt nhất của cả hai thế giới. Hi vọng điêu nay co ich.

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

Tôi chưa thử nghiệm đầy đủ, nhưng một cái gì đó như thế này sẽ hoạt động.

--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);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.