Self-referencing constraint in MS SQL - sql

Is it true that MS SQL restrict self-referencing constraints with ON DELETE CASCADE option?
I have a table with parent-child relation, PARENT_ID column is foreign key for ID. Creating it with ON DELETE CASCADE option causes error
"Introducing FOREIGN KEY constraint
may cause cycles or multiple cascade
paths. Specify ON DELETE NO ACTION or
ON UPDATE NO ACTION, or modify other
FOREIGN KEY constraints."
I can't believe that I have to delete this hierarchy in recursive mode. Is there any issue except triggers?

It is the case that you cannot set up ON DELETE CASCADE on a table with self-referencing constraints. There is a potential of cyclical logic problems, hence it won't allow it.
There's a good article here - though it's for version 8 rather than 9 of SQL - though the same rules apply.

I just answered another question where this question was bound as duplicate. I think it's worth to place my answer here too:
This is not possible. You can solve this with an INSTEAD OF TRIGGER
create table locations
(
id int identity(1, 1),
name varchar(255) not null,
parent_id int,
constraint pk__locations
primary key clustered (id)
)
GO
INSERT INTO locations(name,parent_id) VALUES
('world',null)
,('Europe',1)
,('Asia',1)
,('France',2)
,('Paris',4)
,('Lyon',4);
GO
--This trigger will use a recursive CTE to get all IDs following all ids you are deleting. These IDs are deleted.
CREATE TRIGGER dbo.DeleteCascadeLocations ON locations
INSTEAD OF DELETE
AS
BEGIN
WITH recCTE AS
(
SELECT id,parent_id
FROM deleted
UNION ALL
SELECT nxt.id,nxt.parent_id
FROM recCTE AS prv
INNER JOIN locations AS nxt ON nxt.parent_id=prv.id
)
DELETE FROM locations WHERE id IN(SELECT id FROM recCTE);
END
GO
--Test it here, try with different IDs. You can try WHERE id IN(4,3) also...
SELECT * FROM locations;
DELETE FROM locations WHERE id=4;
SELECT * FROM locations
GO
--Clean-Up (Carefull with real data!)
if exists(select 1 from INFORMATION_SCHEMA.TABLES where TABLE_NAME='locations')
---DROP TABLE locations;

CREATE TRIGGER MyTable_OnDelete ON MyTable
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
DELETE FROM mt
FROM deleted AS D
JOIN MyTable AS mt
ON d.Id = mt.ParentId
DELETE FROM mt
FROM deleted AS D
JOIN MyTable AS mt
ON d.Id = mt.Id
END

Related

Delete records of table which has 2 foreign keys that reference to same table

I have 2 tables, first one is Compartment and second one is AboveCompartment. Please see the below. Above compartment has 2 columns which are foreign keys and reference to the Compartment table. When I set the delete and update action as cascade for 2 foreign keys, I get the error below.
Introducing FOREIGN KEY constraint 'FK_AboveCompartment_Compartment1' on table 'AboveCompartment' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Below CompId and AboveCompId are foreign keys and reference to the Compartment table. Which way should I follow to add delete cascading? I used the trigger below but it also didn't work and get error added below.
AboveCompartment
Compartment
Trigger
ALTER TRIGGER [dbo].[delFromCompartment] on [dbo].[Compartment]
FOR DELETE
AS
DELETE FROM dbo.AboveCompartment
WHERE CompId IN(SELECT deleted.Id FROM deleted)
Error
You cannot implement this using cascades, as SQL Server does not let you.
You also cannot implement it using triggers, because the foreign key is enforced before you get to the trigger.
You need to write a stored procedure that first deletes the parent table rows, then the child table
CREATE OR ALTER PROCEDURE dbo.Delete_Compartment
#CompartmentId int
AS
SET XACT_ABORT, NOCOUNT ON; -- always use XACT_ABORT if you have a transaction
BEGIN TRAN;
DELETE AboveCompartment
WHERE CompId = #CompartmentId;
DELETE AboveCompartment
WHERE AboveCompId = #CompartmentId;
DELETE Compartment
WHERE Id = #CompartmentId;
COMMIT;
I must say, this table design is somewhat suspect. AboveCompId as a column name implies that it represents a single parent for multiple children, rather than multiple parents for multiple children.
If so then you should instead implement this as a self-referencing foreign key. Drop the AboveCompartment table, and add a column
ALTER TABLE Compartment
ADD AboveCompId int NULL REFERENCES Compartment (Id);
This foreign key also cannot be cascading. But now the delete is only on one table, but you can do it in a recursive fashion. As long as you delete all rows in one go, you shouldn't have an issue with foreign key conflicts.
CREATE OR ALTER PROCEDURE dbo.Delete_Compartment
#CompartmentId int
AS
SET NOCOUNT ON;
-- no transaction needed as it's one statement
WITH cte AS (
SELECT #CompartmentId AS Id
UNION ALL
SELECT c.Id
FROM Compartment c
JOIN cte ON cte.Id = c.AboveCompId;
)
DELETE c
FROM Compartment c
JOIN cte ON cte.Id = c.Id;

On delete cascade for self-referencing table

I have a comment table that is self-referencing.
I tried to write on delete cascade but it take some exception
Introducing FOREIGN KEY constraint 'FK_Comments_Comments' on table 'Comments' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
and then try to write a trigger but it take exception again
CREATE TRIGGER [dbo].[T_comment_Trigger]
ON [dbo].[Comments]
FOR DELETE
AS
DELETE FROM Comments
WHERE ParentId =(SELECT deleted.id FROM deleted)
couldn't delete rows that have children
how can I do on delete cascade for my self-referencing table?
Assuming you're keeping your FOREIGN KEY constraint in place, you cannot fix the issue in a FOR DELETE trigger. FOR triggers (also known as AFTER triggers) fire after the activity has taken place. And a foreign key will prevent a row from being deleted if it has references. Foreign key checks occur before deletion.
What you need is an INSTEAD OF trigger. You also need to bear in mind that your current trigger only tried to deal with one "level" of referencing. (So, if row 3 references row 2 and row 2 references row 1, and you delete row 1, your trigger only tried to remove row 2)
So, something like:
CREATE TRIGGER [dbo].[T_comment_Trigger]
ON [dbo].[Comments]
INSTEAD OF DELETE
AS
;WITH IDs as (
select id from deleted
union all
select c.id
from Comments c
inner join
IDs i
on
c.ParentID = i.id
)
DELETE FROM Comments
WHERE id in (select id from IDs);
If there are other (non-self-referencing) cascading foreign key constraints, they all have to be replaced by actions in this trigger. In such a case, I'd recommend introducing a table variable to hold the list of all IDs that will eventually be deleted from the Comments table:
CREATE TRIGGER [dbo].[T_comment_Trigger]
ON [dbo].[Comments]
INSTEAD OF DELETE
AS
declare #deletions table (ID varchar(7) not null);
;WITH IDs as (
select id from deleted
union all
select c.id
from Comments c
inner join
IDs i
on
c.ParentID = i.id
)
insert into #deletions(ID)
select ID from IDs
-- Delete from FK referenced table
DELETE FROM OtherTable
WHERE CommentID in (select ID from #deletions)
--This delete comes last
DELETE FROM Comments
WHERE id in (select ID from #deletions);

Is it safe to drop and then create the foreign key constraints inside a transaction?

I have a table A that references a table B. Table B needs to be populated with updated data from an external source and for efficiency I use TRUNCATE followed by a COPY. This is done even when the application is live.
To further improve efficiency, as suggested in the documentation, I want to drop and then recreate the foreign keys.
However I have some doubts.
If I drop the FKs, COPY and then recreate FKs inside the same transaction, can I be sure that the constraint is preserved even on data inserted in table A during the transaction? I ask this because in theory a transaction is atomic, but in the docs, about the temporary removal of FKs say:
there is a trade-off between data load speed and loss of error checking while the constraint is missing.
If there's a chance that a wrong reference is inserted in the meantime, what happens when you try to recreate the FK constraints?
TRUNCATE is not allowed on any table referenced by a foreign key, unless you use TRUNCATE CASCADE, which will also truncate the referencing tables. The DEFERRABLE status of the constraint does not affect this. I don't think there is any way around this; you will need to drop the constraint.
However, there is no risk of an integrity violation in doing so. ALTER TABLE ... ADD CONSTRAINT locks the table in question (as does TRUNCATE), so your import process is guaranteed to have exclusive access to the table for the duration of its transaction. Any attempts at concurrent inserts will simply hang until the import has committed, and by the time they are allowed to proceeed, the constraint will be back in place.
You can make the foreign key constraint deferrable (initially deferred). That way it will be checked just once at the end of the transaction.
ALTER TABLE
xxx
ADD CONSTRAINT
xxx_yyy_id_fk FOREIGN KEY (yyy_id)
REFERENCES
yyy
DEFERRABLE INITIALLY DEFERRED;
In all the cases, transactions are fully atomic in PostgreSQL (not only in theory), including DDL statements (such as CREATE/DROP constraint), so even if you drop a foreign key, then insert data, then create the foreign key and do everything in one transaction, then you are safe - if the recreation of the foreign key constraint fails, then the inserted data will also be dismissed.
Still, it is better to switch to deferred foreign keys, rather than dropping and then creating them.
Analytic answer: measure the number of new/same/updated/deleted records.
There are four cases:
The key in the B table is not present in the b_import: delete
The key in the b_import is not present on the old B: insert
The key is present in both old B and new B, but the contents are the same: ignore
The keys are the same, but the attribete values differ: Update
-- some test data for `A`, `B` and `B_import`:
CREATE TABLE b
( id INTEGER NOT NULL PRIMARY KEY
, payload varchar
);
INSERT INTO b(id,payload) SELECT gs, 'bb_' || gs::varchar
FROM generate_series(1,20) gs;
CREATE TABLE b_import
( id INTEGER NOT NULL PRIMARY KEY
, payload varchar
);
INSERT INTO b_import(id,payload) SELECT gs, 'bb_' || gs::varchar
FROM generate_series(10,15) gs;
-- In real life this table will be filled by a `COPY b_import FROM ...`
INSERT INTO b_import(id,payload) SELECT gs, 'b2_' || gs::varchar
FROM generate_series(16,25) gs;
CREATE TABLE a
( id SERIAL NOT NULL PRIMARY KEY
, b_id INTEGER references b(id) ON DELETE SET NULL
, aaaaa varchar
);
INSERT INTO a(b_id,aaaaa)
SELECT gs,'aaaaa_' || gs::text FROM generate_series(1,20) gs;
CREATE INDEX ON a(b_id); -- index supporting the FK
-- show it
SELECT a.id, a.aaaaa
,b.id, b.payload AS oldpayload
FROM a
FULL JOIN b ON a.b_id=b.id
ORDER BY a.id;
-- Do the actual I/U/D and report the numbers of affected rows
-- EXPLAIN
WITH ins AS ( -- INSERTS
INSERT INTO b(id, payload)
SELECT b_import.id, b_import.payload
FROM b_import
WHERE NOT EXISTS (
SELECT 1 FROM b
WHERE b.id = b_import.id
)
RETURNING b.id
)
, del AS ( -- DELETES
DELETE FROM b
WHERE NOT EXISTS (
SELECT 2 FROM b_import
WHERE b_import.id = b.id
)
RETURNING b.id
)
, upd AS ( -- UPDATES
UPDATE b
SET payload=b_import.payload
FROM b_import
WHERE b_import.id = b.id
AND b_import.payload IS DISTINCT FROM b.payload -- exclude idempotent updates
-- AND NOT EXISTS ( -- exclude deleted records
-- SELECT 3 FROM del
-- WHERE del.id = b_import.id
-- )
-- AND NOT EXISTS ( -- avoid touching freshly inserted rows
-- SELECT 4 FROM ins
-- WHERE ins.id = b_import.id
-- )
RETURNING b.id
)
SELECT COUNT(*) AS orgb
, (SELECT COUNT(*) FROM b_import) AS newb
, (SELECT COUNT(*) FROM ins) AS ninserted
, (SELECT COUNT(*) FROM del) AS ndeleted
, (SELECT COUNT(*) FROM upd) AS nupdated
FROM b
;
Dropping a constraint and rebuilding it after the import is expensive: all the records in both A and B are involved.
temporally ignoring the constraint is dangerous: the new B table could miss some rows that are still referenced by A's FK.
ergo: You could end up with a crippled model, and you'd have to rebuild As references (which is basically impossible, without additional information (which would be redundant, BTW))

SQL Server using triggers and geting rid of ON DELETE CASCADE

I have 2 tables, A and B.
Table B has a foreign key pointing to primary key of table A. The foreign key on table B has ON DELETE CASCADE, so the corresponding records from B is deleted when a record from A is deleted.
My requirement is to track all added/updated/deleted records in history tables. I have trigger on each table to insert the records into history tables(AHistories and BHistories tables).
I do not like the order ON DELETE CASCADE deletes the records. Trigger A is executed after trigger B, so I have to work around to get the ID of AHistory into BHistory record.
I am wanting to get rid of ON DELETE CASCADE and perform Delete on the records of B in trigger A then insert the deleted record of B into BHistories there.
To demonstrate the idea, I made the case simple, but I have a few more tables that have a foreign key pointing to the primary key in table A. Personally, I would like if I can specify the order and what I do on delete cascade.
Does this stink as an approach? Any comments are appreciated.
As bad as triggers are but sometimes they are the only way to implement complex business requirements. I would do something as follows in the following example PK_ID refers to Primary Key Column.
CREATE TRIGGER tr_Table_A_InsteadOfDelete
ON dbo.TableA
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION;
-- Insert into History Table from TableB
INSERT INTO TABLE_B_History
SELECT B.*
FROM TableB B INNER JOIN deleted d
ON B.PK_ID = d.PK_ID
-- Delete rows from TableB
DELETE FROM TableB
WHERE EXISTS (SELECT 1
FROM deleted
WHERE PK_ID = TableB.PK_ID)
-- Insert into History Table from TableA
INSERT INTO TABLE_A_History
SELECT A.*
FROM TableA A INNER JOIN deleted d
ON A.PK_ID = d.PK_ID
-- Delete rows from TableA
DELETE FROM TableA
WHERE EXISTS (SELECT 1
FROM deleted
WHERE PK_ID = TableA.PK_ID)
COMMIT TRANSACTION;
END
Using triggers for audit purposes is as obvious as it's wrong. If you have sufficient permissions, they can be disabled too easily.
Nevertheless, what you want to do can be achieved with the combination of:
INSTEAD OF DELETE triggers, where you can delete or move rows around in any order;
Disabling nested triggers. This might has unpleasant side effects, if you use triggers for more than just the audit, because it's a database-wide option.
Just don't forget that in such a trigger, you have to manually delete rows from its underlying table, as well.

Self referencing foreign-key constraints and delete

what is the recommended way to handle self-referencing foreignkey constraints in SQL-Server?
Table-Model:
fiData references a previous record in tabData. If i delete a record that is referenced by fiData, the database throws an exception:
"The DELETE statement conflicted with the SAME TABLE REFERENCE
constraint "FK_tabDataPrev_tabDataNext". The conflict occurred in
database "MyDataBase", table "dbo.tabData", column 'fiData'"
if Enforce Foreignkey Constraint is set to "Yes".
I don't need to cascade delete records that are referenced but i would need to set fiData=NULL where it's referenced. My idea is to set Enforce Foreignkey Constraint to "No" and create a delete-trigger. Is this recommendable or are there better ways?
Thank you.
Unlike Andomar, I'd be happy using a trigger - but I wouldn't remove the constraint checking. If you implement it as an instead of trigger, you can reset the other rows to null before performing the actual delete:
CREATE TRIGGER T_tabData_D
on tabData
instead of delete
as
set nocount on
update tabData set fiData = null where fiData in (select idData from deleted)
delete from tabData where idData in (select idData from deleted)
It's short, it's succinct, it wouldn't be necessary if SQL Server could handle foreign key cascades to the same table (in other RDBMS', you may be able to just specify ON DELETE SET NULL for the foreign key constraint, YMMV).
Triggers add implicit complexity. In a database with triggers, you won't know what a SQL statement does by looking at it. In my experience triggers are a bad idea with no exceptions.
In your example, setting the enforced constrained to "No" means you could add a nonexistent ID. And the query optimizer will be less effective because it can't assume the key is valid.
Consider creating a stored procedure instead:
create procedure dbo.NukeTabData(
#idData int)
as
begin transaction
update tabData set fiData = null where fiData = #idData
delete from tabData where idData = #idData
commit transaction
go
This very late to answer.
But for some one who is searching like me.
and want to cascade
here is very good explanation
http://devio.wordpress.com/2008/05/23/recursive-delete-in-sql-server/
The Problem
Although you can define a foreign key with CASCADE DELETE in SQL Server, recursive cascading deletes are not supported (i.e. cascading delete on the same table).
If you create an INSTEAD OF DELETE trigger, this trigger only fires for the first DELETE statement, and does not fire for records recursively deleted from this trigger.
This behavior is documented on MSDN for SQL Server 2000 and SQL Server 2005.
The Solution
Suppose you have a table defined like this:
CREATE TABLE MyTable (
OID INT, -- primary key
OID_Parent INT, -- recursion
... other columns
)
then the delete trigger looks like this:
CREATE TRIGGER del_MyTable ON MyTable INSTEAD OF DELETE
AS
CREATE TABLE #Table(
OID INT
)
INSERT INTO #Table (OID)
SELECT OID
FROM deleted
DECLARE #c INT
SET #c = 0
WHILE #c <> (SELECT COUNT(OID) FROM #Table) BEGIN
SELECT #c = COUNT(OID) FROM #Table
INSERT INTO #Table (OID)
SELECT MyTable.OID
FROM MyTable
LEFT OUTER JOIN #Table ON MyTable.OID = #Table.OID
WHERE MyTable.OID_Parent IN (SELECT OID FROM #Table)
AND #Table.OID IS NULL
END
DELETE MyTable
FROM MyTable
INNER JOIN #Table ON MyTable.OID = #Table.OID
GO