18

You cannot delete a row if any row is referencing the row to delete via a FK.

Is it possible to know if any row is referencing the row to delete before executing a DELETE statement?

Alex Aza
  • 76,499
  • 26
  • 155
  • 134
Yeonho
  • 3,629
  • 4
  • 39
  • 61
  • possible duplicate of [Help on SQL Server trigger](http://stackoverflow.com/questions/6244077/help-on-sql-server-trigger) – gbn Jun 10 '11 at 04:46
  • My goal is not to cause an cascading delete, but to know beforehand if the record is deletable. If it is not deletable, the user will be notified that it's not deletable. – Yeonho Jun 10 '11 at 06:07
  • 2
    I know you've already marked an answer, but if this is a multi-user system, the best answer may be to just attempt the delete and cope with an error occurring - otherwise there are all kinds of race conditions possible here, unless you wrap everything in a transaction with high isolation. – Damien_The_Unbeliever Jun 10 '11 at 06:43
  • @Damien_The_Unbeliever Thank you for your advice. I am adding the warning as an addition. I am also handling the error from SQL when the delete is attempted. – Yeonho Jun 13 '11 at 02:29

4 Answers4

26

This script will show all the tables that have rows that reference the row you are trying to delete:

declare @RowId int = 1
declare @TableName sysname = 'ParentTable'

declare @Command varchar(max) 

select @Command = isnull(@Command + ' union all ', '') + 'select ''' + object_name(parent_object_id) + 
    ''' where exists(select * from ' + object_name(parent_object_id) + ' where ' + col.name+ ' = ' + cast(@RowId as varchar) + ')' 
from sys.foreign_key_columns fkc
    join sys.columns col on
        fkc.parent_object_id = col.object_id and fkc.parent_column_id = col.column_id
where object_name(referenced_object_id) = @TableName

execute (@Command)

Assumption that foreign key is not composite.

Alex Aza
  • 76,499
  • 26
  • 155
  • 134
  • Slight improvement for SQL Server would be to surround each object_name(...) and col.name with square brackets in the dynamic SQL to avoid reserved word problems. – Jtbs Aug 11 '20 at 20:38
  • Tip: The best practice when assembling object names into dynamic SQL statements is to use [`QuoteName()`](https://learn.microsoft.com/en-us/sql/t-sql/functions/quotename-transact-sql) to avoid problems with odd names, e.g. `New Table` with a space or reserved words like `From`. – HABO Dec 14 '21 at 15:22
3

Option 1 (Detection):

You perform a Select Statement to determine if any records are referencing the Record-to-be-deleted -- and, if you wish, manually delete those records that do reference it. This can also be accomplished using a trigger, although I recommend against triggers, because they tend to surprise people (and yourself) down the road.

Option 2 (Automation):

You can look into Cascading Deletes which, if configured correctly, will cause all records referencing the Record-to-be-deleted to also be deleted.

When to use Cascading Deletes (Paraphrased from text written by Joel Coehoorn)

  • Cascade Delete may make sense when the semantics of the relationship can involve an "is part of" description. Example: Web Order, Web Order Line Items
  • You should not use Cascade Delete if you are preserving history or using a soft delete where you only set a deleted bit column
  • Cascading can get you into trouble if you set up your foreign keys wrong.
  • It's not wise to use cascading before you understand it thoroughly. However, it is a useful feature and therefore worth taking the time to understand.

Here's a great discussion on Cascading Deletes on stackoverflow.

Community
  • 1
  • 1
Brian Webster
  • 30,033
  • 48
  • 152
  • 225
  • when you try to delete a record with a FK, SQL Server throws an exception. IS SQL Server internally performing a SELECT statement to know if there's any record referencing it? – Yeonho Jun 10 '11 at 04:42
  • 2
    SQL keeps track of relationships and constraints in a variety of ways, some of those ways are similar to select statements. However, I think of it being more like garbage collection. "Nothing is referencing this chunk of memory any longer, therefore I can delete it" – Brian Webster Jun 10 '11 at 04:44
0

nobody has mentioned it, but just for the record I use a lot the procedure

sp_helpconstraint 'dbo.mytable'

in order to find all the constraints related to dbo.mytable, and which tables reference dbo.mytable. I find it very useful and handy.

Marcello Miorelli
  • 3,368
  • 4
  • 44
  • 67
0

I improved the Alex Aza's solution.

I use softdelete, so It's necessary add a column "delete" in the condition "where" . And more I made a function in TSQL that it discovers when the table represents the object inherited in NHibernate, and the PK is the FK from parent table.

Follow:

declare @RowId int = 4
declare @TableName sysname = 'TABLE'

declare @Command varchar(max) 

select @Command = isnull(@Command + ' union all ', '') + 'select ''' + object_name(parent_object_id) + 
    ''' where exists(select * from ' + object_name(parent_object_id) + 
    CASE
        WHEN EXISTS(select object_name(object_id) from sys.columns col where name = 'deleted' and object_id = parent_object_id) 
            THEN ' where ' + col.name+ ' = ' + cast(@RowId as varchar) +' and deleted = 0 '
        when dbo.ParentIdFromTable(object_name(parent_object_id)) <> ''
            then ' inner join ' + dbo.ParentIdFromTable(object_name(parent_object_id)) + ' on id = ' + dbo.PrimaryKey(object_name(parent_object_id))
                +' where ' + col.name+ ' = ' + cast(@RowId as varchar) +' and deleted = 0 '
        else 
            ' where ' + col.name+ ' = ' + cast(@RowId as varchar) 
      END
    + ')' 
from sys.foreign_key_columns fkc
    join sys.columns col on
        fkc.parent_object_id = col.object_id and fkc.parent_column_id = col.column_id
where object_name(referenced_object_id) = @TableName

PRINT @Command
execute (@Command)

Depedencies Functions:

CREATE FUNCTION dbo.ParentIdFromTable(@Table varchar(255))
RETURNS varchar(255) 
AS 
BEGIN
    declare @tableParent varchar(255) = ''

    if exists(select pk.TABLE_NAME, pk.COLUMN_NAME, col.name, object_name(referenced_object_id) Referenced, object_name(parent_object_id) as Parent
        from sys.columns col
            inner join sys.foreign_key_columns fkc on
                fkc.parent_object_id = col.object_id and fkc.parent_column_id = col.column_id
            inner join INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk on
                pk.TABLE_NAME = object_name(object_id) and pk.COLUMN_NAME = col.name
        WHERE OBJECTPROPERTY(OBJECT_ID(constraint_name), 'IsPrimaryKey') = 1
        AND table_name = @table)
    begin

        while exists(select *
            from sys.columns col
            inner join sys.foreign_key_columns fkc on
                fkc.parent_object_id = col.object_id and fkc.parent_column_id = col.column_id
            inner join INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk on
                pk.TABLE_NAME = object_name(object_id) and pk.COLUMN_NAME = col.name
            WHERE OBJECTPROPERTY(OBJECT_ID(constraint_name), 'IsPrimaryKey') = 1 AND table_name = @table)
        begin
            -- Descobrir o parent, column
            select  @tableParent = object_name(referenced_object_id)
            from sys.columns col
            inner join sys.foreign_key_columns fkc on
                fkc.parent_object_id = col.object_id and fkc.parent_column_id = col.column_id
            inner join INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk on
                pk.TABLE_NAME = object_name(object_id) and pk.COLUMN_NAME = col.name
            WHERE OBJECTPROPERTY(OBJECT_ID(constraint_name), 'IsPrimaryKey') = 1
            AND table_name = @table

            --print @tableParent
            set @table = @tableParent
        end
    end

    return @tableParent;
END;
GO


CREATE FUNCTION dbo.PrimaryKey(@Table varchar(255))
RETURNS varchar(255) 
AS 
BEGIN
    declare @columnName varchar(255) = ''
    -- Descobrir o parent, column
    select @columnName = COLUMN_NAME
    from INFORMATION_SCHEMA.KEY_COLUMN_USAGE 
    WHERE OBJECTPROPERTY(OBJECT_ID(constraint_name), 'IsPrimaryKey') = 1
    AND table_name = @Table

    return @columnName
end;
julienc
  • 19,087
  • 17
  • 82
  • 82