DELETE deyimi REFERENCE kısıtlamasıyla çakıştı


10

Durumum şöyle:

STOCK_ARTICLES tablosu:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

Tablo YER:

ID *[PK]*
LOCATION_NAME

Tablo WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

INVENTORY_ITEMS tablosu:

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

INVENTORY_ITEMS içindeki 3 FK, açıkça diğer ilgili tablolardaki "ID" sütunlarına referansta bulunuyor.

Buradaki ilgili tablolar STOCK_ARTICLE ve INVENTORY_ITEMS.

Şimdi, yukarıda belirtilen veritabanını başka bir veritabanı (OTHER_DB) ile "senkronize eden" birkaç adımdan (SQL komut dosyaları) oluşan bir SQL işi var . Bu işin içindeki adımlardan biri "temizlik" içindir. Aynı kimliğe sahip diğer veritabanında karşılık gelen kayıt bulunmayan STOCK_ITEMS içindeki tüm kayıtları siler. Şöyle görünüyor:

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)

Ancak bu adım her zaman başarısız olur:

DELETE deyimi "FK_INVENTORY_ITEMS_STOCK_ARTICLES" REFERENCE kısıtlamasıyla çakıştı. Çakışma "FIRST_DB", "dbo.INVENTORY_ITEMS" tablosu, 'STOCK_ARTICLES' sütununda meydana geldi. [SQLSTATE 23000] (Hata 547) Açıklama sonlandırıldı. [SQLSTATE 01000] Hatası (Hata 3621). Adım başarısız oldu.

Dolayısıyla sorun, INVENTORY_ITEMS tarafından referans alındığında STOCK_ARTICLES'ten kayıtları silememesidir. Ancak bu temizliğin çalışması gerekiyor. Bu, muhtemelen STOP_ITEMS'den silinmesi gereken kayıtları tanımlaması için temizleme komut dosyasını genişletmem gerektiği anlamına gelir, ancak karşılık gelen kimliğe INVENTORY_ITEMS içinden başvurulduğu için yapamazsınız. Daha sonra önce bu kayıtları INVENTORY_ITEMS içindeki ve ardından STOCK_ARTICLES içindeki kayıtları silin. Haklı mıyım? SQL kodu nasıl görünürdü?

Teşekkür ederim.

Yanıtlar:


13

Yabancı anahtar kısıtlamalarının bütün mesele budur: referans bütünlüğünü korumak için başka bir yerde atıfta bulunulan verileri silmenizi durdururlar.

İki seçenek vardır:

  1. Satırları sil INVENTORY_ITEMS, ilk sonra gelen satırlar STOCK_ARTICLES.
  2. ON DELETE CASCADEAnahtar tanımında için kullanın .

1: Doğru Sırayla Silme

Bunu yapmanın en etkili yolu, hangi satırların silineceğine karar veren sorgunun karmaşıklığına bağlı olarak değişir. Genel bir örüntü şunlar olabilir:

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

Bu, basit sorgular için veya tek bir stok öğesini silmek için uygundur, ancak delete deyiminiz WHERE NOT EXISTSiçinde WHERE INçok verimsiz bir plan oluşturabilecek bir iç içe yerleştirme içerir, bu nedenle gerçekçi bir veri kümesi boyutuyla test edin ve gerekirse sorguyu yeniden düzenleyin.

Ayrıca işlem ifadelerine dikkat edin: her iki silme işleminin de tamamlandığından veya hiçbirinin olmadığından emin olmak istersiniz. İşlem zaten bir işlem içinde gerçekleşiyorsa, bunu geçerli işlem ve hata işleme işleminize uyacak şekilde değiştirmeniz gerekir.

2: Kullanım ON DELETE CASCADE

Basamak seçeneğini yabancı anahtarınıza eklerseniz, SQL Server bunu sizin için otomatik olarak yapar INVENTORY_ITEMSve hiçbir şeyin sildiğiniz satırlara başvurmaması gereken kısıtlamayı karşılamak için satırları kaldırır . Sadece ON DELETE CASCADEFK tanımına şöyle ekleyin :

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

Buradaki bir avantaj, silme işlem ve kilit ayarları hakkında endişelenme ihtiyacını azaltan (her zamanki gibi% 100 kaldırılmadan) bir atom ifadesidir. Kaskad hatta birden ebeveyn / çocuk / büyük-çocuk üzerinde çalışabilir / ... seviyeler ise ebeveyn ve tüm soyundan arasında yalnızca bir yol (nerede bu olabilir değil işin örnekler için "çoklu kaskad yolları" için arama) bulunmaktadır.

NOT: Ben ve diğerleri, basamaklı silme işlemlerinin tehlikeli olduğunu düşünüyoruz, bu nedenle bu seçeneği kullanırsanız, veritabanı tasarımınızda düzgün bir şekilde belgelendirmek için çok dikkatli olun, böylece siz ve diğer geliştiriciler tehlikeyi daha sonra atlamazsınız . Bu nedenle mümkün olan her yerde basamaklı silmelerden kaçınırım.

Basamaklı silme işlemlerinin neden olduğu yaygın bir sorun, birisinin UPDATEveya yerine satırları bırakarak ve yeniden oluşturarak verileri güncellemesidir MERGE. Bu genellikle "zaten var olan satırları güncelleyin, olmayan satırları ekleyin" (bazen UPSERT işlemi olarak da adlandırılır) gerektiğinde ve MERGEifadeden habersiz kişilerin yapmayı daha kolay bulduğu durumlarda görülür :

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

göre

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

Buradaki sorun, delete ifadesinin alt satırlara basamaklanacağı ve insert ifadesinin bunları yeniden oluşturmayacağıdır, bu nedenle üst tabloyu güncellerken yanlışlıkla alt tablolardan veri kaybedersiniz.

özet

Evet, önce alt satırları silmeniz gerekir.

Başka bir seçenek daha var: ON DELETE CASCADE.

Ancak ON DELETE CASCADEtehlikeli olabilir , bu yüzden dikkatli kullanın.

Yan Not: Kullanım MERGE(veya UPDATE-ve- INSERTnerede MERGEkullanılamaz) Bir gerektiğinde UPSERToperasyon, değil DELETE -daha sonra-replace-Kurun- INSERTkullanan diğer kişilerle tuzaklarına düşmekten kaçınmak için ON DELETE CASCADE.


2

Kimliklerin yalnızca bir kez silinmesini sağlayabilir, geçici tabloda depolayabilir ve işlemleri silmek için kullanabilirsiniz. Sonra ne sildiğini daha iyi kontrol.

Bu işlem başarısız olmamalıdır:

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
Çok sayıda STOCK_ARTICLES satırı silinirse, geçici tabloyu oluşturması nedeniyle diğer seçeneklerden daha kötü performans göstermesi muhtemeldir (az sayıda satır için farkın önemli olması olası değildir). Ayrıca, eşzamanlı erişim imkansız değilse, üç ifadenin bir atom birimi olarak yürütülmesini sağlamak için uygun işlem yönergelerini kullanmaya dikkat edin, aksi takdirde hataları INVENTORY_ITEMSiki DELETEs arasına yeni eklenen olarak görebilirsiniz .
David Spillett

1

Ben de bu sorunu yaşadım ve çözebildim. İşte benim durumum:

Benim durumumda, bir kaynak sisteminden (MYSOURCE_DB) alınan bir analitiği (MYTARGET_DB) raporlamak için kullanılan bir veritabanım var. Bazı 'MYTARGET_DB' tabloları bu sisteme özgüdür ve veriler orada oluşturulur ve yönetilir; Tabloların çoğu 'MYSOURCE_DB' den ve verileri 'MYSOURCE_DB' den 'MYTARGET_DB' e silen / ekleyen bir iş var.

Arama tablolarından [PRODUCT] biri SOURCE kaynaklıdır ve HEDEF'te depolanan bir veri tablosu [InventoryOutsourced] vardır. Tablolarda tasarlanmış referans bütünlüğü vardır. Sil / ekle komutunu çalıştırmaya çalıştığımda bu mesajı alıyorum.

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

Oluşturduğum geçici çözüm, [InventoryOutsourced] öğesinden [@tempTable] tablo değişkenine veri eklemek, [InventoryOutsourced] içindeki verileri silmek, senkronizasyon işlerini çalıştırmak, [@tempTable] 'dan [InventoryOutsourced]' e eklemek. Bu bütünlüğü yerinde tutar ve benzersiz veri toplama da korunur. Bu her iki dünyanın da en iyisi. Bu yardımcı olur umarım.

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

Tam olarak test etmedim, ancak böyle bir şey işe yaramalı.

--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);
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.