Pernyataan DELETE bertentangan dengan batasan REFERENSI

10

Situasi saya terlihat seperti ini:

Tabel STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

Tabel LOKASI:

ID *[PK]*
LOCATION_NAME

Tabel WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

Tabel INVENTORY_ITEMS:

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

3 FK di INVENTORY_ITEMS mereferensikan kolom "ID" di masing-masing tabel lainnya, jelas.

Tabel yang relevan di sini adalah STOCK_ARTICLE dan INVENTORY_ITEMS.

Sekarang ada pekerjaan SQL yang terdiri dari beberapa langkah (skrip SQL) yang "menyinkronkan" database yang disebutkan di atas dengan database lain (OTHER_DB). Salah satu langkah di dalam pekerjaan ini adalah untuk "pembersihan". Itu menghapus semua catatan dari STOCK_ITEMS di mana tidak ada catatan yang sesuai di database lain dengan ID yang sama. Ini terlihat seperti ini:

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)

Tetapi langkah ini selalu gagal dengan:

Pernyataan DELETE bertentangan dengan batasan REFERENSI "FK_INVENTORY_ITEMS_STOCK_ARTICLES". Konflik terjadi di basis data "FIRST_DB", tabel "dbo.INVENTORY_ITEMS", kolom 'STOCK_ARTICLES'. [SQLSTATE 23000] (Kesalahan 547) Pernyataan telah diakhiri. [SQLSTATE 01000] (Kesalahan 3621). Langkah itu gagal.

Jadi masalahnya adalah ia tidak bisa menghapus catatan dari STOCK_ARTICLES ketika direferensikan oleh INVENTORY_ITEMS. Tapi pembersihan ini perlu dilakukan. Yang berarti bahwa saya mungkin harus memperluas skrip pembersihan sehingga pertama-tama mengidentifikasi catatan yang harus dihapus dari STOCK_ITEMS, tetapi tidak bisa karena ID yang sesuai dirujuk dari dalam INVENTORY_ITEMS. Maka itu harus terlebih dahulu menghapus catatan-catatan itu di dalam INVENTORY_ITEMS, dan setelah itu menghapus catatan-catatan di dalam STOCK_ARTICLES. Apakah saya benar? Bagaimana kode SQL akan terlihat seperti itu?

Terima kasih.

derwodaso
sumber

Jawaban:

13

Itulah inti dari batasan kunci asing: mereka menghentikan Anda menghapus data yang dirujuk ke tempat lain untuk menjaga integritas referensial.

Ada dua opsi:

  1. Hapus baris dari INVENTORY_ITEMSpertama, lalu baris dari STOCK_ARTICLES.
  2. Gunakan ON DELETE CASCADEuntuk definisi kunci.

1: Menghapus Secara Benar

Cara paling efisien untuk melakukan ini bervariasi tergantung pada kompleksitas kueri yang menentukan baris mana yang akan dihapus. Pola umum mungkin:

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

Ini bagus untuk permintaan sederhana atau untuk menghapus satu stok barang, tetapi mengingat pernyataan penghapusan Anda berisi WHERE NOT EXISTSklausa bersarang yang di dalamnya WHERE INmungkin menghasilkan rencana yang sangat tidak efisien, maka uji dengan ukuran set data yang realistis dan atur ulang kueri jika diperlukan.

Perhatikan juga laporan transaksi: Anda ingin memastikan semua penghapusan selesai atau tidak ada yang dilakukan. Jika operasi sudah terjadi dalam transaksi, Anda jelas perlu mengubahnya untuk mencocokkan transaksi Anda saat ini dan proses penanganan kesalahan.

2: Gunakan ON DELETE CASCADE

Jika Anda menambahkan opsi kaskade ke kunci asing Anda maka SQL Server akan secara otomatis melakukan ini untuk Anda, menghapus baris dari INVENTORY_ITEMSuntuk memenuhi batasan bahwa tidak ada yang merujuk pada baris yang Anda hapus. Tambahkan saja ON DELETE CASCADEdefinisi FK seperti:

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

Keuntungan di sini adalah bahwa penghapusan adalah satu pernyataan atom yang mengurangi (meskipun, seperti biasa, tidak menghilangkan 100%) kebutuhan untuk khawatir tentang pengaturan transaksi dan kunci. Kaskade bahkan dapat beroperasi di beberapa level induk / anak / cucu / ... jika hanya ada satu jalur antara induk dan semua keturunan (cari "beberapa jalur kaskade" untuk contoh di mana ini mungkin tidak berfungsi).

CATATAN: Saya, dan banyak orang lain, menganggap penghapusan bertingkat menjadi berbahaya sehingga jika Anda menggunakan opsi ini, berhati-hatilah untuk mendokumentasikannya dengan benar dalam desain basis data Anda sehingga Anda dan pengembang lainnya tidak tersandung bahaya nantinya . Saya menghindari penghapusan cascading sedapat mungkin karena alasan ini.

Masalah umum yang disebabkan oleh penghapusan bertingkat adalah ketika seseorang memperbarui data dengan menjatuhkan dan membuat ulang baris alih-alih menggunakan UPDATEatau MERGE. Ini sering terlihat di mana "perbarui baris yang sudah ada, masukkan yang tidak" (kadang-kadang disebut operasi UPSERT) diperlukan dan orang yang tidak mengetahui MERGEpernyataan tersebut merasa lebih mudah untuk melakukan:

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

dari

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

Masalahnya di sini adalah bahwa pernyataan penghapusan akan mengalir ke baris anak, dan pernyataan insert tidak akan membuatnya kembali, jadi saat memperbarui tabel induk Anda secara tidak sengaja kehilangan data dari tabel anak.

Ringkasan

Ya, Anda harus menghapus baris anak terlebih dahulu.

Ada pilihan lain: ON DELETE CASCADE.

Tapi ON DELETE CASCADEbisa berbahaya , jadi gunakan dengan hati-hati.

Catatan samping: gunakan MERGE(atau UPDATE-dan- di INSERTmana MERGEtidak tersedia) ketika Anda membutuhkan UPSERToperasi, bukan DELETE -kemudian ganti dengan- INSERTuntuk menghindari jatuh ke dalam perangkap yang diletakkan oleh orang lain menggunakan ON DELETE CASCADE.

David Spillett
sumber
2

Anda bisa mendapatkan ID untuk dihapus hanya sekali, menyimpannya di tabel sementara dan digunakan untuk menghapus operasi. Maka Anda memiliki kontrol yang lebih baik apa yang Anda hapus.

Operasi ini seharusnya tidak gagal:

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
Tajuk Paweł
sumber
2
Meskipun jika menghapus sejumlah besar baris STOCK_ARTICLES ini kemungkinan berkinerja lebih buruk daripada opsi lain karena membangun tabel temp (untuk sejumlah kecil baris perbedaannya tidak mungkin signifikan). Juga berhati-hatilah untuk menggunakan arahan transaksi yang sesuai untuk memastikan bahwa ketiga pernyataan dieksekusi sebagai unit atom jika akses bersamaan tidak mustahil, jika tidak, Anda dapat melihat kesalahan seperti baru INVENTORY_ITEMSditambahkan di antara keduanya DELETE.
David Spillett
1

Saya juga mengalami masalah ini, dan saya bisa mengatasinya. Inilah situasi saya:

Dalam kasus saya, saya memiliki database yang digunakan untuk melaporkan analitik (MYTARGET_DB), yang menarik dari sistem sumber (MYSOURCE_DB). Beberapa tabel 'MYTARGET_DB' unik untuk sistem itu, dan data dibuat & dikelola di sana; Sebagian besar tabel berasal dari 'MYSOURCE_DB' dan ada pekerjaan yang menghapus / memasukkan data ke 'MYTARGET_DB' dari 'MYSOURCE_DB'.

Salah satu tabel pencarian [PRODUK] adalah dari SUMBER, dan ada tabel data [Persediaan] yang disimpan di TARGET. Ada integritas referensial yang dirancang ke dalam tabel. Jadi ketika saya mencoba menjalankan delete / insert saya mendapatkan pesan ini.

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

Solusi yang saya buat adalah memasukkan data ke [@tempTable] variabel tabel dari [InventoryOutsourced], hapus data di [InventoryOutsourced], jalankan pekerjaan sinkronisasi, masukkan ke [InventoryOutsourced] dari [@tempTable]. Ini menjaga integritas di tempat, dan pengumpulan data yang unik juga dipertahankan. Yang terbaik dari kedua dunia. Semoga ini membantu.

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
Lembar kerja Sherlock
sumber
0

Saya belum sepenuhnya diuji, tetapi sesuatu seperti ini seharusnya berhasil.

--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);
Scott Hodgin
sumber