본문 바로가기

DEV LOGS

[MSSQL] 트리거

트리거란 ?

테이블 또는 뷰와 관련되어 DML문의 이벤트가 발생될 때 작동하는 데이터베이스 개체



트리거 종류

- DML트리거 : 테이블 또는 뷰와 관련되어 DML문의 이벤트가 발생될 때 작동

- DDL트리거 : 서버나 데이터베이스에 DDL문의 이벤트가 발생될 때 작동

- LOGON트리거 : 사용자의 로그온 이벤트가 발생하면 작동



트리거 사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 간단한 테이블 생성
USE tempdb;
CREATE TABLE testTbl(id INT, txt NVARCHAR(5));
GO
INSERT INTO testTbl VALUES(1,N'싸이'),(2,N'빅뱅'),(3,N'투피엠');
 
-- testTbl에 트리거 부착
CREATE TRIGGER testTrg        -- 트리거 이름
ON testTbl                    -- 트리거를 부착할 테이블
AFTER DELETEUPDATE        -- 삭제, 수정 후에 작동하게 지정
AS
    PRINT(N'트리거가 작동했습니다');    -- 트리거를 실행할 때 작동되는 코드들
 
-- 데이터를 삭입,수정,삭제해 보자.
INSERT INTO testTbl VALUES(4,N'인피니티');
UPDATE testTbl SET txt = N'투에이엠' WHERE id = 3;
DELETE testTbl WHERE id = 4;
 
cs


- 결과

DML 트리거의 종류

(1) AFTER 트리거 : INSERT, UPDATE, DELETE 등의 작업이 일어났을 때 해당 작업 후에 작동한다. 테이블에만 작동하며 뷰에는 작동하지 않는다.

(2) INSTEAD OF 트리거 : INSERT, UPDATE, DELETE 등의 작업 이벤트가 발생하기 전에 작동하는 트리거, 테이블뿐 아니라 뷰에서도 작동되며, 주로 뷰에 업데이트가 가능하게 할 때 사용된다.

(3) CLR 트리거 : T-SQL 저장 프로시저 대신 .NET Framework에서 생성되는 트리거이다.



트리거 문법

1
2
3
4
5
6
7
CREATE TRIGGER 트리거이름
ON    { 테이블이름 | 뷰이름 }
[ WITH ENCRYPTION ]
{ FOR | AFTER | INSTEAD OF }
{ [ INSERT ] [ , ] [ UPDATE ] [ , ] [ DELETE ] }
AS
    실행할 SQL 
cs

·WITH ENCRYPTION : 트리거의 내용을 암호화시켜서 추후에 내용을 확인할 수 없게한다.

·FOR | AFTER | INTEAD OF : DML 트리거의 종류 지정 (FOR는 AFTER와 같다)

·INSERT | UPDATE | DELETE : 트리거가 작동될 이벤트, 하나 이상의 이벤트를 조합해서 작동시킬 수 있다.


(1) AFTER 트리거 사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
-- insert나 update가 일어나면 변경되기 전의 데이터를 저장할 테이블 생성
USE sqlDB;
DROP TABLE buyTbl;
CREATE TABLE backup_userTbl
(
    userID            char(8NOT NULL PRIMARY KEY,
    name            nvarchar(10NOT NULL,
    birthYear        int NOT NULL,
    addr            nchar(2NOT NULL,
    mobile1            char(3),
    mobile2            char(8),
    height            smallint,
    mDate            date,
    modType            nchar(2),    -- 변경된 타입. '수정' 또는 '삭제'
    modDate            date,        -- 변경된 날짜
    modUser            nvarchar(256)    -- 변경된 사용자
)
 
-- 트리거 생성 및 부착
CREATE TRIGGER trg_BackupUserTbl
ON userTbl
AFTER updatedelete
AS
    DECLARE @modType NCHAR(2)    -- 변경 타입
 
    IF (COLUMNS_UPDATED() >0)
        BEGIN
            SET @modType = N'수정'
        END
    ELSE
        BEGIN
            SET @modType = N'삭제'
        END
 
    INSERT INTO backup_userTbl
        SELECT userID,name,birthYear,addr,mobile1,mobile2,height,mDate,@modType,GETDATE(),USER_NAME()
        FROM deleted;
 
            API
        
-- deleted 테이블이란 update 또는 delete가 수행되기 전의 데이터가 잠시 저장되는 임시 테이블이다.
 
-- 데이터 변경하기
-- 업데이트 및 삭제
UPDATE userTbl SET addr = N'뭉고' WHERE userID = 'JKW';
DELETE userTbl WHERE height >= 177;
 
-- 업데이트 및 삭제 결과 확인
SELECT * FROM userTbl;
cs


- 결과



DROP TABLE VS TRUNCATE TABLE VS DELETE 문 : 링크가기

'inserted' 임시 테이블과 'deleted' 임시테이블 : 링크가기


(2) INSTEAD OF 트리거 사용 예시

 : 다음 예시는 배달 정보를 추가하면 자동으로 구매 테이블과 고객 테이블에 기록되는 트리거이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- 배송 정보 뷰 생성
CREATE VIEW uv_deliver
AS
    SELECT b.userID, u.name, b.prodName, b.price, b.amount, u.addr
    FROM buyTbl b
    INNER JOIN userTbl u
    ON b.userID = u.userID
 
-- 새로운 고객 추가
INSERT INTO uv_deliver VALUES('JBI',N'존밴이',N'구두',50,1,N'인천'); -- 에러 발생!
 
-- INSTEAD TRIGGER를 활용하여 배송 정보 뷰에 입력되는 정보 중에서 고객 테이블과 구매 테이블에 입력될 것을 분리해서 입력하게 지정한다.
CREATE TRIGGER trg_insert
ON uv_deliver
INSTEAD OF INSERT
AS
    BEGIN
        INSERT INTO userTbl(userID,name,birthYear,addr,mDate)
            SELECT userid,name,1900,addr,GETDATE() FROM inserted
        INSERT INTO buyTbl(userID,prodName,price,amount)
            SELECT userid,prodName,price,amount FROM inserted
    END
 
-- 새로운 고객 다시 추가
INSERT INTO uv_deliver VALUES('JBI',N'존밴이',N'구두',50,1,N'인천');
 
-- 결과 확인
SELECT * FROM userTbl WHERE userID = 'JBI';
SELECT * FROM buyTbl WHERE userID = 'JBI';
cs


- 결과

( 위는 고객 테이블 , 아래는 구매 테이블 )



트리거의 정보 확인

(1) 시스템 저장 프로시저 사용 ('sp_helptrigger' , sp_helptext)


- sp_helptrigger : 트리거 정보 확인

· 형식

1
EXEC sp_helptrigger 테이블이름 혹은 뷰이름
cs


· 결과


- sp_helptext : 트리거 내용 확인 ( WITH ENCRYPTION 옵션이 적용 중이라면 확인 불가능 )

· 형식

1
EXEC sp_helptext 트리거이름
cs


· 결과

(2) SSMS에서 확인하기



트리거 이름 변경

(1) 시스템 저장 프로시저 사용 ('sp_rename')

· 형식

EXEC sp_rename '기존 트리거이름', '새로운 트리거이름'


(2) DROP하고 다시 CREATE


- 결론 : 'sp_rename'을 사용하면 외부적으로 트리거의 이름은 변경되지만 변경된 이름으로 트리거를 삭제하는 것은 불가능 하다. 때문에 DROP하고 CREATE하는 것이 좋은 방법이다. ( 뷰, 저장 프로시저, 함수 이름 등도 같다. )



DML 트리거 분류

- 다중 트리거 (Multiple Triggers) : 하나의 테이블에 같은 트리거가 여러 개 부착되어 있는 것

- 중첩 트리거 (Nested Triggers) : 트리거가 또 다른 트리거를 작동하는 것 

(서버 구성 옵션인 'nested triggers'가 ON되어 있어야 한다. / 기본값이 ON이다.)

- 재귀 트리거 (Recursive Triggers) : 트리거가 작동해서 다시 자신의 트리거를 작동시키는 것 

( ALTER DATABASE를 사용하여 RECURSIVE_TRIGGERS 설정을 활성화 필요 )

( 두 테이블이 서로 트리거에 물려 호출 되는 간접 재귀 트리거와 자신의 테이블에 순환적으로 트리거가 발생 되는 직접 재귀 트리거가 있다.)



중첩 트리거 사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
-- DB 생성
USE tempdb; 
CREATE DATABASE triggerDB;
 
-- 테이블 생성
USE triggerDB;
CREATE TABLE orderTbl    -- 구매 테이블
(
    orderNo        INT IDENTITY,
    userID        NVARCHAR(5),
    prodName    NVARCHAR(5),
    orderAmount    INT
)
CREATE TABLE prodTbl    -- 물품 테이블
(
    prodName    NVARCHAR(5),
    account        INT
)
CREATE TABLE deliverTbl    -- 배송 테이블
(
    deliverNo    INT IDENTITY,
    prodName    NVARCHAR(5),
    amount        INT
);
 
-- 물품 정보 추가
INSERT INTO prodTbl VALUES('사과',100),('배',100),('귤',100);
 
-- 구매 테이블에 입력이 되면 물품 테이블의 정보가 수정되는 트리거
CREATE TRIGGER trg_order
ON orderTbl
AFTER INSERT
AS
PRINT '1. trg_order를 실행합니다.'
DECLARE @orderAmount INT
DECLARE @prodName NVARCHAR(5)
 
SELECT @orderAmount = orderAmount FROM inserted
SELECT @prodName = prodName FROM inserted
 
UPDATE prodTbl SET account -= @orderAmount
    WHERE prodName = @prodName;
 
-- 배송 테이블에 새 배송 건을 입력하는 트리거
CREATE TRIGGER trg_prod
ON prodTbl
AFTER UPDATE
AS
    PRINT '2. trg_prod를 실행합니다.'
    DECLARE @prodName NVARCHAR(5)
    DECLARE @amount INT
 
    SELECT @prodName = prodName FROM inserted
    SELECT @amount = D.account - I.account FROM inserted I,deleted D
 
    INSERT INTO deliverTbl(prodName,amount) VALUES(@prodName,@amount);
 
-- 고객이 물건을 구매한 INSERT 작업을 수행
INSERT INTO orderTbl VALUES ('JOHN','배',5);
 
-- 중첩 트리거가 잘 작동했는지 세 테이블을 모두 확인
SELECT * FROM orderTbl
SELECT * FROM prodTbl
SELECT * FROM deliverTbl
cs


- 결과

( 맨 위는 구매 테이블 , 가운데는 물품 테이블 , 맨 아래는 배송 테이블 )


간접 재귀 트리거

(두 테이블이 서로 트리거로 물려 있는 경우)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
-- 재귀 트리거 사용 가능 여부 확인
USE triggerDB;
SELECT name,is_recursive_triggers_on FROM sys.databases WHERE name = 'triggerDB';
 
-- 0(OFF)로 설정되어 있다면 1(ON)로 변경해 주자
ALTER DATABASE triggerDB SET RECURSIVE_TRIGGERS ON;
 
-- 테이블 생성
CREATE TABLE recuA                -- 간접 재귀 트리거용 테이블A
(
    id        INT IDENTITY,
    txt        NVARCHAR(10)
)
 
CREATE TABLE recuB                -- 간접 재귀 트리거용 테이블B
(
    id        INT IDENTITY,
    txt        NVARCHAR(10)
)
 
-- 간접 재귀 트리거 부착
CREATE TRIGGER trg_recuA
ON recuA
AFTER INSERT
AS
    DECLARE @id INT
    SELECT @id = trigger_nestlevel()    -- 현재 트리거 레벨 값
    PRINT '트리거 레벨 ===> ' + CONVERT(char(5),@id)
    INSERT INTO recuB VALUES('간접 재귀 트리거');
 
CREATE TRIGGER trg_recuB
ON recuB
AFTER INSERT
AS
    DECLARE @id INT
    SELECT @id = trigger_nestlevel()
    PRINT '트리거 레벨 ===> ' + CONVERT(char(5),@id)
    INSERT INTO recuA VALUES ('간접 재귀 트리거')
 
-- 데이터 입력
INSERT INTO recuA VALUES ('처음입력값');     -- 오류 발생!
 
-- 재귀 출은 최대 32번까지 호출을 허용하여 무한 루프를 방지한다.
 
-- 데이터 확인
SELECT * FROM recuA
SELECT * FROM recuB
-- 마지막에 오류 발생으로 트랜잭션을 취소시켰기 때문에 아무런 내용도 추가되지 않았다.
 
-- 재귀를 빠져 나올수 있는 루틴 추가
ALTER TRIGGER trg_recuA
ON recuA
AFTER INSERT
AS
    IF( (SELECT TRIGGER_NESTLEVEL()) >= 32)
        RETURN
 
    DECLARE @id INT
    SELECT @id = trigger_nestlevel()    -- 현재 트리거 레벨 값
    PRINT '트리거 레벨 ===> ' + CONVERT(char(5),@id)
    INSERT INTO recuB VALUES('간접 재귀 트리거');
 
ALTER TRIGGER trg_recuB
ON recuB
AFTER INSERT
AS
    IF( (SELECT TRIGGER_NESTLEVEL()) >= 32)
        RETURN
 
    DECLARE @id INT
    SELECT @id = trigger_nestlevel()
    PRINT '트리거 레벨 ===> ' + CONVERT(char(5),@id)
    INSERT INTO recuA VALUES ('간접 재귀 트리거')
 
-- 2-5. 다시 입력
INSERT INTO recuA VALUES ('처음입력값');
 
-- 2-6. 테이블 내용 확인
SELECT * FROM recuA
SELECT * FROM recuB
-- id 열의 값이 1부터 시작하지 않은 이유는 2-2에서 INSERT가 실패하더라도 IDENTITY값은 계속 증가하기 때문이다.
cs


- 결과

( 위는 recuA 테이블 , 아래 recuB 테이블 )


- 결론

· 재귀 트리거의 사용은 SSMS에서 기본적으로 허용하지 않는다. 때문에 사용하기 위해서는 데이터베이스의 'RECURSIVE_TRIGGERS' 옵션을 ON으로 해주어야 한다.

· 트리거 작동중에 에러가 발생하면 ROLLBACK되어 아무런 변경이 일어나지 않는다.

· 재귀 트리거를 사용시 빠져나올 수 있는 루틴을 만들도록 하자.






참고문헌


우재남, 『뇌를 자극하는 SQL Server 2012』 한빛미디어 (2013-03-02 초판발행)



'DEV LOGS' 카테고리의 다른 글

[GIT]윈도우 10 Git 설치  (0) 2018.07.18
[GIT] 버전관리시스템  (0) 2018.07.18
[MSSQL]트리거 사용시 임시 테이블  (0) 2018.07.17
[MSSQL] DROP TABLE VS TRUNCATE TABLE VS DELETE 문  (0) 2018.07.17
[MSSQL] 커서  (1) 2018.07.10