Sentencia DELETE en conflicto con la restricción REFERENCE

10

Mi situación se ve así:

Tabla STOCK_ARTICLES:

ID *[PK]*
OTHER_DB_ID
ITEM_NAME

Mesa UBICACIÓN:

ID *[PK]*
LOCATION_NAME

Tabla WORK_PLACE:

ID *[PK]*
WORKPLACE_NAME

Tabla INVENTORY_ITEMS:

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

Los 3 FK en INVENTORY_ITEMS hacen referencia a las columnas "ID" en las otras tablas respectivas, obviamente.

Las tablas relevantes aquí son STOCK_ARTICLE e INVENTORY_ITEMS.

Ahora hay un trabajo SQL que consta de varios pasos (scripts SQL) que "sincronizan" la base de datos mencionada anteriormente con otra base de datos (OTHER_DB). Uno de los pasos dentro de este trabajo es para la "limpieza". Elimina todos los registros de STOCK_ITEMS donde no hay un registro correspondiente en la otra base de datos con la misma ID. Se parece a esto:

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)

Pero este paso siempre falla con:

La instrucción DELETE está en conflicto con la restricción REFERENCE "FK_INVENTORY_ITEMS_STOCK_ARTICLES". El conflicto ocurrió en la base de datos "FIRST_DB", tabla "dbo.INVENTORY_ITEMS", columna 'STOCK_ARTICLES'. [SQLSTATE 23000] (Error 547) La declaración ha finalizado. [SQLSTATE 01000] (Error 3621). El paso falló.

Entonces, el problema es que no puede eliminar registros de STOCK_ARTICLES cuando INVENTORY_ITEMS hace referencia a ellos. Pero esta limpieza necesita funcionar. Lo que significa que probablemente tenga que extender el script de limpieza para que primero identifique los registros que deben eliminarse de STOCK_ITEMS, pero no puede porque el ID correspondiente se referencia desde dentro de INVENTORY_ITEMS. Luego, primero debe eliminar esos registros dentro de INVENTORY_ITEMS, y luego eliminar los registros dentro de STOCK_ARTICLES. Estoy en lo cierto? ¿Cómo se vería entonces el código SQL?

Gracias.

derwodaso
fuente

Respuestas:

13

Ese es el objetivo de las restricciones de clave externa: le impiden eliminar datos a los que se hace referencia en otros lugares para mantener la integridad referencial.

Hay dos opciones:

  1. Eliminar las filas de INVENTORY_ITEMSprimero, luego las filas de STOCK_ARTICLES.
  2. Use ON DELETE CASCADEpara el en la definición clave.

1: Eliminar en el orden correcto

La forma más eficiente de hacerlo varía según la complejidad de la consulta que decide qué filas eliminar. Un patrón general podría ser:

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

Esto está bien para consultas simples o para eliminar un solo artículo de stock, pero dado que su declaración de eliminación contiene una WHERE NOT EXISTScláusula de anidamiento que WHERE INpodría producir un plan muy ineficiente, pruebe con un tamaño de conjunto de datos realista y reorganice la consulta si es necesario.

También tenga en cuenta las declaraciones de transacción: desea asegurarse de que ambas eliminaciones se completen o que ninguna de ellas lo haga. Si la operación ya está ocurriendo dentro de una transacción, obviamente necesitará modificar esto para que coincida con su transacción actual y el proceso de manejo de errores.

2: uso ON DELETE CASCADE

Si agrega la opción en cascada a su clave externa, SQL Server lo hará automáticamente por usted, eliminando filas INVENTORY_ITEMSpara satisfacer la restricción de que nada debe referirse a las filas que está eliminando. Simplemente agregue ON DELETE CASCADEa la definición FK de esta manera:

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

Una ventaja aquí es que la eliminación es una declaración atómica que reduce (aunque, como de costumbre, no elimina al 100%) la necesidad de preocuparse por la configuración de la transacción y el bloqueo. La cascada incluso puede operar en varios niveles padre / hijo / nieto / ... si solo hay una ruta entre el padre y todos los descendientes (busque "rutas en cascada múltiples" para ver ejemplos de dónde esto podría no funcionar).

NOTA: Yo, y muchos otros, consideramos que las eliminaciones en cascada son peligrosas, por lo tanto, si usa esta opción, tenga mucho cuidado de documentarla adecuadamente en el diseño de su base de datos para que usted y otros desarrolladores no tropiecen con el peligro más adelante . Evito las eliminaciones en cascada siempre que sea posible por este motivo.

Un problema común causado con las eliminaciones en cascada es cuando alguien actualiza los datos al soltar y volver a crear filas en lugar de usar UPDATEo MERGE. Esto se ve a menudo cuando se necesita "actualizar las filas que ya existen, insertar las que no" (a veces llamada operación UPSERT) y las personas que desconocen la MERGEdeclaración encuentran más fácil hacerlo:

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

que

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

El problema aquí es que la declaración de eliminación se conectará en cascada a las filas secundarias, y la instrucción de inserción no las volverá a crear, por lo que al actualizar la tabla primaria, accidentalmente pierde datos de la (s) tabla (s) secundaria (s).

Resumen

Sí, primero debe eliminar las filas secundarias.

Hay otra opción: ON DELETE CASCADE.

Pero ON DELETE CASCADEpuede ser peligroso , así que úselo con cuidado.

Nota al margen: use MERGE(o UPDATE-y- INSERTdonde MERGEno esté disponible) cuando necesite una UPSERToperación, no DELETE -y luego reemplace- INSERTpara evitar caer en trampas colocadas por otras personas ON DELETE CASCADE.

David Spillett
fuente
2

Puede obtener ID para eliminar solo una vez, almacenarlas en una tabla temporal y usarlas para eliminar operaciones. Entonces tienes un mejor control de lo que estás eliminando.

Esta operación no debería fallar:

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
Paweł Tajs
fuente
2
Sin embargo, si se eliminan grandes cantidades de filas de STOCK_ARTICLES, es probable que esto funcione peor que las otras opciones debido a la construcción de la tabla temporal (para pequeñas cantidades de filas, es poco probable que la diferencia sea significativa). También tenga cuidado de usar las directivas de transacción apropiadas para garantizar que las tres declaraciones se ejecuten como una unidad atómica si el acceso concurrente no es imposible; de ​​lo contrario, podría ver errores como nuevos INVENTORY_ITEMSagregados entre los dos DELETEs.
David Spillett
1

También me encontré con este problema y pude resolverlo. Aquí está mi situación:

En mi caso, tengo una base de datos utilizada para informar un análisis (MYTARGET_DB), que se extrae de un sistema fuente (MYSOURCE_DB). Algunas de las tablas 'MYTARGET_DB' son exclusivas de ese sistema, y ​​los datos se crean y administran allí; La mayoría de las tablas son de 'MYSOURCE_DB' y hay un trabajo que elimina / inserta los datos en 'MYTARGET_DB' de 'MYSOURCE_DB'.

Una de las tablas de búsqueda [PRODUCT] es de SOURCE, y hay una tabla de datos [InventoryOutsourced] almacenada en TARGET. Hay integridad referencial diseñada en las tablas. Entonces, cuando intento ejecutar la eliminación / inserción, aparece este mensaje.

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

La solución alternativa que creé es insertar datos en la variable de tabla [@tempTable] de [InventoryOutsourced], eliminar datos en [InventoryOutsourced], ejecutar los trabajos de sincronización, insertarlos en [InventoryOutsourced] de [@tempTable]. Esto mantiene la integridad en su lugar, y la recopilación de datos única también se conserva. Que es lo mejor de ambos mundos. Espero que esto ayude.

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
SherlockHojas de cálculo
fuente
0

No lo he probado completamente, pero algo como esto debería funcionar.

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