테이블 외래키 트리 확인 및 데이터 삭제하기

 

  • Version : SQL Server 2005, 2008, 2008R2, 2012, 2014, 2016

 

SQL Server에서 데이터 무결성을 유지하기 위해 생성한 외래키 제약조건이 있는 경우 데이터를 삭제 할 때 제약 조건에 위배되지 않도록 순차적으로 작업을 해야 한다. 테이블에 생성되어있는 외래키 연결에 대해 트리 구조로 살펴보고 순차적으로 데이터를 삭제할 수 있는 방법에 대해서 알아본다.

 

실습을 위해 테이블을 생성한다. 스크립트를 실행 하면 그림과 같은 외래키로 구성된 테이블이 생성된다.

-- sample tables with FKs

create table dbo.M (id int primary key, M_c1 int not null, M_c2 datetime not null, M_c3 char(10) not null, M_c4 datetime);

create unique index ux1_A on dbo.M (M_c1);

create unique index ux2_A on dbo.M (M_c2, M_c3);

 

create table dbo.N1 (id int primary key, N1_c1 int, N1_c2 datetime, N1_c3 char(10), N1_c4 datetime);

create unique index ux1_N1 on dbo.N1 (N1_c1, N1_c3);

create unique index ux2_N1 on dbo.N1 ( N1_c2);

alter table dbo.N1 add constraint FK_N1_M foreign key (N1_c1) references dbo.M (M_c1);

 

create table dbo.N2 (id int primary key, N2_c1 int, N2_c2 datetime, N2_c3 char(10), N2_c4 datetime);

create unique index ux1_N2 on dbo.N2 (N2_c1);

create unique index ux2_N2 on dbo.N2 (N2_c3, N2_c4);

alter table dbo.N2 add constraint FK1_N2_M foreign key (N2_c2, N2_c3) references dbo.M (M_c2, M_c3);

 

create table dbo.P1 (id int primary key, P1_c1 int, P1_c2 datetime, P1_c3 char(10), P1_c4 datetime);

create unique index ux1_P1 on dbo.P1 (P1_c1);

alter table dbo.P1 add constraint FK_P1_N1 foreign key (P1_c1, P1_c3) references dbo.N1 (N1_c1, N1_c3);

 

create table dbo.P2 (id int primary key, P2_c1 int, P2_c2 datetime, P2_c3 char(10), P2_c4 datetime);

create unique index ux1_P2 on dbo.P2 (P2_c1);

create unique index ux2_P2 on dbo.P2 (P2_c2, P2_c3);

alter table dbo.P2 add constraint FK_P2_N1 foreign key (P2_c2) references dbo.N1 (N1_c2);

alter table dbo.P2 add constraint FK_P2_N2 foreign key (P2_c3, P2_c4) references dbo.N2 (N2_c3, N2_c4);

 

create table dbo.P3 (id int primary key, P3_c1 int, P3_c2 datetime, P3_c3 char(10), P3_c4 datetime);

alter table dbo.P3 add constraint FK_P3_N2 foreign key (P3_c1) references dbo.N2 (id);

 

create table dbo.Q1 (id int primary key, Q1_c1 int, Q1_c2 datetime, Q1_c3 char(10), Q1_C4 datetime);

alter table dbo.Q1 add constraint FK_Q1_P1 foreign key (Q1_c1) references dbo.P1 (P1_c1);

 

create table dbo.Q2 (id int primary key, Q2_c1 int, Q2_c2 datetime, Q2_c3 char(10), Q2_c4 datetime);

alter table dbo.Q2 add constraint FK_Q2_P2 foreign key (Q2_c1) references dbo.P2 (id);

 

create table dbo.Q3 (id int primary key, Q3_c1 int, Q3_c2 datetime, Q3_c3 char(10), Q3_c4 datetime);

alter table dbo.Q3 add constraint FK1_Q3_N2 foreign key (Q3_c1) references dbo.N2 (id);

alter table dbo.Q3 add constraint FK2_Q3_P2 foreign key (Q3_c2, Q3_c3) references dbo.P2 (P2_c2, P2_c3);

GO

 

-- populate all tables

insert into dbo.M (id, M_c1, M_c2, M_c3, M_c4)

select 1, 10, '2015-01-01', 'AB1', '2015-01-02'

union all

select 2, 20, '2015-01-02', 'AB2', '2015-01-03'

union all

select 3, 30, '2015-01-03', 'AB3', '2015-01-04';

 

insert into dbo.N1 (id, N1_c1, N1_c2, N1_c3, N1_c4)

select 11, 20, '2015-01-01', 'CD1', '2015-01-02'

union all

select 21, 30, '2015-01-02', 'CD2', '2015-01-03'

union all

select 31, 10, '2015-01-03', 'CD3', '2015-01-04';

 

insert into dbo.N2 (id, N2_c1, N2_c2, N2_c3, N2_c4)

select 11, 11, '2015-01-01', 'AB1', '2015-01-02'

union all

select 12, 22, '2015-01-02', 'AB2', '2015-01-03'

union all

select 13, 33, '2015-01-03', 'AB3', '2015-01-04';

 

insert into dbo.P1 (id, P1_c1, P1_c2, P1_c3, P1_c4)

select 100, 20, '2014-01-01', 'CD1', '2015-01-02'

union all

select 101, 30, '2014-01-02', 'CD2', '2015-01-03'

union all

select 102, 10, '2014-01-03', 'CD3', '2015-01-04'

 

insert into dbo.P2 (id, P2_c1, P2_c2, P2_c3, P2_c4)

select 200, 20, '2015-01-01', 'AB1', '2015-01-02'

union all

select 201, 30, '2015-01-02', 'AB2', '2015-01-03'

union all

select 202, 10, '2015-01-03', 'AB3', '2015-01-04'

 

insert into dbo.P3 (id, P3_c1, P3_c2, P3_c3, P3_c4)

select 301, 11, '2010-01-01', 'EF1', '2015-02-02'

union all

select 302, 13, '2010-01-02', 'EF2', '2015-02-03'

union all

select 303, 12, '2010-01-03', 'EF3', '2015-02-04'

 

 

아래 스크립트는 테이블의 외래키 연결을 트리 구조로 나타내는 프로시저를 생성한다.

use SW_TEST

go

 

if object_id('dbo.usp_searchFK', 'P') is not null

    drop proc dbo.usp_SearchFK;

go

create proc dbo.usp_SearchFK

@table varchar(256) -- use two part name convention

, @lvl int=0 -- do not change

, @ParentTable varchar(256)='' -- do not change

, @debug bit = 1

as

begin

    set nocount on;

    declare @dbg bit;

    set @dbg=@debug;

    if object_id('tempdb..#tbl', 'U') is null

        create table #tbl (id int identity, tablename varchar(256), lvl int, ParentTable varchar(256));

    declare @curS cursor;

    if @lvl = 0

        insert into #tbl (tablename, lvl, ParentTable)

        select @table, @lvl, Null;

    else

        insert into #tbl (tablename, lvl, ParentTable)

        select @table, @lvl,@ParentTable;

    if @dbg=1    

        print replicate('----', @lvl) + 'lvl ' + cast(@lvl as varchar(10)) + ' = ' + @table;

    

    if not exists (select * from sys.foreign_keys where referenced_object_id = object_id(@table))

        return;

    else

    begin -- else

        set @ParentTable = @table;

        set @curS = cursor for

        select tablename=object_schema_name(parent_object_id)+'.'+object_name(parent_object_id)

        from sys.foreign_keys

        where referenced_object_id = object_id(@table)

        and parent_object_id <> referenced_object_id; -- add this to prevent self-referencing which can create a indefinitive loop;

 

        open @curS;

        fetch next from @curS into @table;

 

        while @@fetch_status = 0

        begin --while

            set @lvl = @lvl+1;

            -- recursive call

            exec dbo.usp_SearchFK @table, @lvl, @ParentTable, @dbg;

            set @lvl = @lvl-1;

            fetch next from @curS into @table;

        end --while

        close @curS;

        deallocate @curS;

    end -- else

    if @lvl = 0

        select * from #tbl;

    return;

end

go

 

 

외래키 트리 구조를 나타내는 프로시저를 실행한다. 프로시저는 소유자를 포함한 테이블명을 사용한다. 결과 탭에서 메시지의 내용을 보면 부모 테이블에서 마지막 자식 테이블까지 외래키로 연결된 구조를 트리 구조로 확인할 수 있다.

exec dbo.usp_SearchFK 'dbo.M'

 

 

아래 스크립트는 트리 구조로 연결된 테이블에서 순차적으로 데이터를 삭제할 수 있도록 삭제 스크립트를 생성해주는 스크립트이다. 결과 탭에서 출력해주는 스크립트를 다른 SSMS창에서 실행하면 데이터 삭제가 가능하다.

if object_id('tempdb..#tmp') is not null

    drop table #tmp;

create table #tmp (id int, tablename varchar(256), lvl int, ParentTable varchar(256));

 

insert into #tmp

exec dbo.usp_SearchFK @table='dbo.M', @debug=0;

 

declare @where varchar(max) ='where M.id=2' -- if @where clause is null or empty, it will delete tables as a whole with the right order

declare @curFK cursor, @fk_object_id int;

declare @sqlcmd varchar(max)='', @crlf char(2)=char(0x0d)+char(0x0a);

declare @child varchar(256), @parent varchar(256), @lvl int, @id int;

declare @i int;

declare @t table (tablename varchar(128));

declare @curT cursor;

if isnull(@where, '')= ''

begin

    set @curT = cursor for select tablename, lvl from #tmp order by lvl desc

    open @curT;

    fetch next from @curT into @child, @lvl;

    while @@fetch_status = 0

    begin -- loop @curT

        if not exists (select 1 from @t where tablename=@child)

            insert into @t (tablename) values (@child);

        fetch next from @curT into @child, @lvl;

    end -- loop @curT

    close @curT;

    deallocate @curT;

 

    select @sqlcmd = @sqlcmd + 'delete from ' + tablename + @crlf from @t ;

    print @sqlcmd;

end

else

begin

    declare curT cursor for

    select lvl, id

    from #tmp

    order by lvl desc;

 

    open curT;

    fetch next from curT into @lvl, @id;

    while @@FETCH_STATUS =0

    begin

        set @i=0;

        if @lvl =0

        begin -- this is the root level

            select @sqlcmd = 'delete from ' + tablename from #tmp where id = @id;

        end -- this is the roolt level

 

        while @i < @lvl

        begin -- while

 

            select top 1 @child=TableName, @parent=ParentTable from #tmp where id <= @id-@i and lvl <= @lvl-@i order by lvl desc, id desc;

            set @curFK = cursor for

            select object_id from sys.foreign_keys

            where parent_object_id = object_id(@child)

            and referenced_object_id = object_id(@parent)

 

            open @curFK;

            fetch next from @curFk into @fk_object_id

            while @@fetch_status =0

            begin -- @curFK

 

                if @i=0

                    set @sqlcmd = 'delete from ' + @child + @crlf +

                    'from ' + @child + @crlf + 'inner join ' + @parent ;

                else

                    set @sqlcmd = @sqlcmd + @crlf + 'inner join ' + @parent ;

 

                ;with c as

                (

                    select child = object_schema_name(fc.parent_object_id)+'.' + object_name(fc.parent_object_id), child_col=c.name

                    , parent = object_schema_name(fc.referenced_object_id)+'.' + object_name(fc.referenced_object_id), parent_col=c2.name

                    , rnk = row_number() over (order by (select null))

                    from sys.foreign_key_columns fc

                    inner join sys.columns c

                    on fc.parent_column_id = c.column_id

                    and fc.parent_object_id = c.object_id

                    inner join sys.columns c2

                    on fc.referenced_column_id = c2.column_id

                    and fc.referenced_object_id = c2.object_id

                    where fc.constraint_object_id=@fk_object_id

                )

                    select @sqlcmd =@sqlcmd + case rnk when 1 then ' on ' else ' and ' end

                    + @child +'.'+ child_col +'=' + @parent +'.' + parent_col

                    from c;

                    fetch next from @curFK into @fk_object_id;

            end --@curFK

            close @curFK;

            deallocate @curFK;

            set @i = @i +1;

        end --while

        print @sqlcmd + @crlf + @where + ';';

        print '';

        fetch next from curT into @lvl, @id;

    end

    close curT;

    deallocate curT;

end

 

go

 

 

 

[참고자료]

https://www.mssqltips.com/sqlservertip/4059/script-to-delete-data-from-sql-server-tables-with-foreign-key-constraints/

 




강성욱 / jevida@naver.com
Microsoft SQL Server MVP
Blog : http://sqlmvp.kr
Facebook : http://facebook.com/sqlmvp





profile

강성욱 / jevida@naver.com
Microsoft SQL Server MVP
Blog : http://sqlmvp.kr
Facebook : http://facebook.com/sqlmvp

Kakao Talk : SQLMVP

Line : jevida


현재 LA에 거주하고 있으며 SQL에 관심있는 분이면 언제든 친추 환영합니다.