> For the complete documentation index, see [llms.txt](https://docs.tibero.com/tibero-manuals/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tibero.com/tibero-manuals/7.2.6.manuals/tbpsm-guide/trigger.md).

# 트리거

트리거 본문을 tbPSM으로 작성하는 방법과 트리거 본문에서 지켜야 할 제약을 설명합니다.

## **개요**

트리거(trigger)는 테이블이나 뷰의 DML 이벤트, 스키마나 데이터베이스의 DDL 이벤트와 시스템 이벤트가 발생할 때 자동으로 실행되도록 데이터베이스에 저장해 두는 tbPSM 블록입니다.

본 장은 트리거 본문을 tbPSM으로 작성하는 방법을 설명합니다. 트리거의 종류와 활용은 "Tibero 개발자 안내서"의 [트리거의 사용](/tibero-manuals/7.2.6.manuals/development-guide/trigger.md)을, 생성·변경·삭제 문법은 "Tibero SQL 참조 안내서"의 [CREATE TRIGGER](/tibero-manuals/7.2.6.manuals/sql-reference-guide/data-definition-language/create-trigger.md), [ALTER TRIGGER](/tibero-manuals/7.2.6.manuals/sql-reference-guide/data-definition-language/alter-trigger.md), [DROP TRIGGER](/tibero-manuals/7.2.6.manuals/sql-reference-guide/data-definition-language/drop-trigger.md)를 참고합니다.

## **트리거 본문의 구조**

트리거 본문은 익명 블록과 같은 구조입니다. DECLARE 없이 선언부를 바로 쓰고, 실행부(BEGIN \~ END)와 예외 처리부(EXCEPTION)를 가질 수 있습니다. 트리거 안에서의 예외 처리는 [에러 처리](/tibero-manuals/7.2.6.manuals/tbpsm-guide/error-handling.md)를 참고합니다.

본문을 직접 쓰는 대신 **CALL** 문으로 이미 저장된 프러시저를 호출할 수도 있습니다.

트리거는 CREATE TRIGGER 문을 수행하는 시점에 컴파일되어 데이터베이스에 저장됩니다. 컴파일에 실패해도 트리거는 만들어지며, 오류 내용을 확인하고 다시 컴파일하는 방법은 [서브 프로그램](/tibero-manuals/7.2.6.manuals/tbpsm-guide/subprograms.md)의 "[컴파일 오류 확인](/tibero-manuals/7.2.6.manuals/tbpsm-guide/subprograms.md#undefined-14)"을 참고합니다. 무효 상태의 트리거가 남아 있으면 트리거 조건에 해당하는 DML 문장이 실패하므로, 오류를 수정해 다시 컴파일하거나 트리거를 삭제해야 합니다.

다음은 재고 수량이 변경될 때 변경 이력을 남기는 로우 트리거의 예입니다. 본 장의 예제는 상품별 재고를 담는 STOCK\_TBL 테이블과 변경 이력을 담는 STOCK\_HIST 테이블을 사용합니다.

```
CREATE TABLE stock_tbl (
    item_no NUMBER PRIMARY KEY,
    qty     NUMBER,
    price   NUMBER
);

CREATE TABLE stock_hist (
    item_no   NUMBER,
    old_qty   NUMBER,
    new_qty   NUMBER,
    logged_at TIMESTAMP
);

CREATE OR REPLACE TRIGGER stock_hist_trg
AFTER UPDATE OF qty ON stock_tbl
FOR EACH ROW
DECLARE
    v_now TIMESTAMP := SYSTIMESTAMP;
BEGIN
    INSERT INTO stock_hist VALUES (:OLD.item_no, :OLD.qty, :NEW.qty, v_now);
END;
/

INSERT INTO stock_tbl VALUES (1, 10, 1000);
UPDATE stock_tbl SET qty = 20 WHERE item_no = 1;

SELECT item_no, old_qty, new_qty FROM stock_hist;
```

다음은 실행 결과입니다.

```
   ITEM_NO    OLD_QTY    NEW_QTY
---------- ---------- ----------
         1         10         20

1 row selected.
```

UPDATE OF에 명시한 qty 컬럼이 변경될 때만 트리거가 실행됩니다. price 컬럼만 변경하는 UPDATE 문에서는 실행되지 않습니다.

## **상관 이름 :NEW와 :OLD**

로우 트리거의 본문에서는 트리거를 실행시킨 로우의 변경 전 값과 변경 후 값을 각각 **:OLD**와 **:NEW**로 참조합니다. 콜론 없이 NEW, OLD로 쓰면 컴파일 오류가 발생합니다. 단, WHEN 절 안에서는 콜론 없이 씁니다.

REFERENCING 절로 OLD와 NEW 대신 다른 이름을 지정할 수 있습니다.

:NEW와 :OLD를 쓸 수 있는 위치는 다음과 같습니다.

* 로우 트리거(FOR EACH ROW)와 컴파운드 트리거의 로우 섹션에서만 쓸 수 있습니다. 문장 트리거, DDL 트리거, DB 이벤트 트리거와 컴파운드 트리거의 문장 섹션에서 쓰면 컴파일 오류가 발생합니다.
* :NEW에 값을 할당할 수 있는 곳은 INSERT 또는 UPDATE 이벤트의 BEFORE 로우 트리거(컴파운드 트리거의 BEFORE EACH ROW 섹션 포함)뿐입니다. 할당한 값이 실제 로우에 반영됩니다. AFTER 로우 트리거에서는 읽기만 할 수 있습니다.

다음은 REFERENCING 절과 이벤트 판별 함수를 사용한 예입니다. 앞의 예제에서 만든 STOCK\_TBL 테이블을 사용합니다.

```
CREATE OR REPLACE TRIGGER stock_tbl_trg
BEFORE INSERT OR UPDATE OF qty OR DELETE ON stock_tbl
REFERENCING OLD AS old_row NEW AS new_row
FOR EACH ROW
BEGIN
    IF INSERTING THEN
        DBMS_OUTPUT.PUT_LINE('INSERTING: ' || :new_row.item_no);
    ELSIF UPDATING('QTY') THEN
        DBMS_OUTPUT.PUT_LINE('UPDATING(QTY): ' || :old_row.qty ||
                             ' -> ' || :new_row.qty);
    ELSIF DELETING THEN
        DBMS_OUTPUT.PUT_LINE('DELETING: ' || :old_row.item_no);
    END IF;
END;
/

SET SERVEROUTPUT ON
INSERT INTO stock_tbl VALUES (2, 10, 500);
UPDATE stock_tbl SET qty = 30 WHERE item_no = 2;
DELETE FROM stock_tbl WHERE item_no = 2;
```

다음은 실행 결과입니다.

```
INSERTING: 2

1 row inserted.

UPDATING(QTY): 10 -> 30

1 row updated.

DELETING: 2

1 row deleted.
```

다음은 BEFORE 로우 트리거에서 :NEW에 값을 할당하여 입력 값을 보정하는 예입니다. INSERT 문에는 앞의 예제에서 만든 stock\_tbl\_trg도 함께 실행됩니다.

```
CREATE OR REPLACE TRIGGER stock_tbl_fix
BEFORE INSERT ON stock_tbl
FOR EACH ROW
BEGIN
    IF :NEW.price IS NULL THEN
        :NEW.price := 0;
    END IF;
END;
/

INSERT INTO stock_tbl (item_no, qty) VALUES (3, 5);
SELECT * FROM stock_tbl WHERE item_no = 3;
```

다음은 실행 결과입니다.

```
INSERTING: 3

1 row inserted.


   ITEM_NO        QTY      PRICE
---------- ---------- ----------
         3          5          0

1 row selected.
```

## **이벤트 판별 함수**

하나의 트리거를 여러 DML 이벤트에 연결했을 때(예: INSERT OR UPDATE OR DELETE), 본문에서는 다음 함수로 어떤 이벤트가 트리거를 실행시켰는지 판별합니다. 세 함수 모두 BOOLEAN을 반환하므로 IF 문의 조건으로 바로 쓸 수 있습니다.

<table><thead><tr><th width="220">함수</th><th>설명</th></tr></thead><tbody><tr><td>INSERTING</td><td>INSERT 문이 트리거를 실행시켰으면 TRUE</td></tr><tr><td>UPDATING</td><td>UPDATE 문이 트리거를 실행시켰으면 TRUE</td></tr><tr><td>UPDATING('컬럼_이름')</td><td>UPDATE 문이 지정한 컬럼을 변경했으면 TRUE. 컬럼 이름은 대소문자를 구분하지 않음</td></tr><tr><td>DELETING</td><td>DELETE 문이 트리거를 실행시켰으면 TRUE</td></tr></tbody></table>

사용 예는 앞 절의 stock\_tbl\_trg 예제를 참고합니다.

## **컴파운드 트리거**

컴파운드 트리거(compound trigger)는 실행 시점이 다른 여러 섹션을 하나의 트리거 안에 함께 정의하는 트리거입니다. 트리거 선언부의 변수를 모든 섹션이 공유하므로, 로우 단위로 모은 정보를 문장이 끝난 뒤 한 번에 처리하는 작업을 트리거 하나로 작성할 수 있습니다.

컴파운드 트리거는 실행 시점(BEFORE, AFTER) 대신 **FOR** 키워드로 선언하고, 본문 자리에 **COMPOUND TRIGGER**와 섹션 목록을 씁니다.

```
CREATE [OR REPLACE] TRIGGER 트리거_이름
FOR 트리거_이벤트 ON 테이블_또는_뷰_이름
COMPOUND TRIGGER
    [선언부]
    섹션 [섹션 ...]
END;
```

각 섹션은 다음 형태로 씁니다. END 뒤에는 섹션을 시작한 키워드를 그대로 다시 씁니다.

```
시점_키워드 IS
    [선언부]
BEGIN
    문장
END 시점_키워드;
```

섹션의 종류는 다음과 같습니다.

<table><thead><tr><th width="240">섹션</th><th>설명</th></tr></thead><tbody><tr><td>BEFORE STATEMENT</td><td>트리거 이벤트 문장이 실행되기 전에 한 번 실행</td></tr><tr><td>BEFORE EACH ROW</td><td>각 로우가 변경되기 전에 로우마다 실행</td></tr><tr><td>AFTER EACH ROW</td><td>각 로우가 변경된 후에 로우마다 실행</td></tr><tr><td>AFTER STATEMENT</td><td>트리거 이벤트 문장이 끝난 후에 한 번 실행</td></tr><tr><td>INSTEAD OF EACH ROW</td><td>뷰를 대상으로 할 때 원래 작업 대신 로우마다 실행</td></tr></tbody></table>

섹션을 작성할 때의 제약은 다음과 같습니다. 어기면 트리거가 컴파일 오류 상태로 생성됩니다.

* 같은 섹션을 두 번 쓸 수 없습니다.
* 테이블을 대상으로 하는 컴파운드 트리거에는 INSTEAD OF EACH ROW 섹션을 쓸 수 없습니다.
* 뷰를 대상으로 하는 컴파운드 트리거에는 INSTEAD OF EACH ROW 섹션만 쓸 수 있습니다.
* :NEW와 :OLD는 로우 섹션(BEFORE EACH ROW, AFTER EACH ROW, INSTEAD OF EACH ROW)에서만 쓸 수 있습니다.

### **실행 순서와 상태 공유**

섹션은 BEFORE STATEMENT, BEFORE EACH ROW, AFTER EACH ROW, AFTER STATEMENT 순서로 실행되며, 로우 섹션 두 개는 변경되는 로우마다 반복됩니다. 트리거 선언부의 변수는 트리거 이벤트 문장이 시작될 때 초기화되어 문장이 끝날 때까지 유지되고, 모든 섹션이 공유합니다.

다음은 섹션의 실행 순서와 선언부 변수의 공유를 확인하는 예입니다.

```
CREATE TABLE cpd_emp (
    emp_no NUMBER PRIMARY KEY,
    salary NUMBER
);

CREATE OR REPLACE TRIGGER cpd_emp_trg
FOR INSERT OR UPDATE ON cpd_emp
COMPOUND TRIGGER
    v_row_cnt NUMBER := 0;

    BEFORE STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('BEFORE STATEMENT: v_row_cnt = ' || v_row_cnt);
    END BEFORE STATEMENT;

    BEFORE EACH ROW IS
    BEGIN
        v_row_cnt := v_row_cnt + 1;
        DBMS_OUTPUT.PUT_LINE('BEFORE EACH ROW: emp_no = ' || :NEW.emp_no);
    END BEFORE EACH ROW;

    AFTER EACH ROW IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('AFTER EACH ROW: emp_no = ' || :NEW.emp_no);
    END AFTER EACH ROW;

    AFTER STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('AFTER STATEMENT: v_row_cnt = ' || v_row_cnt);
    END AFTER STATEMENT;
END;
/

INSERT INTO cpd_emp
SELECT LEVEL, LEVEL * 1000 FROM DUAL CONNECT BY LEVEL <= 3;
```

다음은 실행 결과입니다.

```
BEFORE STATEMENT: v_row_cnt = 0
BEFORE EACH ROW: emp_no = 1
AFTER EACH ROW: emp_no = 1
BEFORE EACH ROW: emp_no = 2
AFTER EACH ROW: emp_no = 2
BEFORE EACH ROW: emp_no = 3
AFTER EACH ROW: emp_no = 3
AFTER STATEMENT: v_row_cnt = 3

3 rows inserted.
```

선언부 변수는 문장 단위로 초기화됩니다. 이어서 다른 문장을 실행하면 v\_row\_cnt는 0부터 다시 셉니다.

```
UPDATE cpd_emp SET salary = salary + 100 WHERE emp_no <= 2;
```

다음은 실행 결과입니다.

```
BEFORE STATEMENT: v_row_cnt = 0
BEFORE EACH ROW: emp_no = 1
AFTER EACH ROW: emp_no = 1
BEFORE EACH ROW: emp_no = 2
AFTER EACH ROW: emp_no = 2
AFTER STATEMENT: v_row_cnt = 2

2 rows updated.
```

### **변경 중인 테이블 접근**

로우 트리거의 본문에서 트리거가 걸린 테이블을 조회하면 TBR-15095 오류가 발생합니다. 컴파운드 트리거를 사용하면 로우 섹션에서는 선언부 변수에 정보만 모으고, 테이블 조회는 문장 섹션에서 수행하는 방식으로 이 제약을 피할 수 있습니다. 문장 섹션에는 이 제약이 적용되지 않습니다.

다음은 로우마다 변경 건수를 세고, 문장이 끝난 뒤 대상 테이블을 집계하는 예입니다.

```
CREATE TABLE pay_tbl (emp_no NUMBER, salary NUMBER);
INSERT INTO pay_tbl VALUES (1, 1000);
INSERT INTO pay_tbl VALUES (2, 2000);
COMMIT;

CREATE OR REPLACE TRIGGER pay_tbl_cpd
FOR UPDATE ON pay_tbl
COMPOUND TRIGGER
    v_updated NUMBER := 0;

    AFTER EACH ROW IS
    BEGIN
        v_updated := v_updated + 1;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
        v_total NUMBER;
    BEGIN
        SELECT SUM(salary) INTO v_total FROM pay_tbl;
        DBMS_OUTPUT.PUT_LINE('updated rows = ' || v_updated ||
                             ', total salary = ' || v_total);
    END AFTER STATEMENT;
END;
/

UPDATE pay_tbl SET salary = salary * 1.1;
```

다음은 실행 결과입니다.

```
updated rows = 2, total salary = 3300

2 rows updated.
```

## **트리거 본문의 제약**

트리거 본문을 작성할 때 다음 제약을 지켜야 합니다. 트리거 본문에서 처리되지 않은 예외가 발생하면 트리거를 실행시킨 문장이 실패합니다.

### **변경 중인 테이블 조회 불가**

로우 트리거의 본문에서 트리거가 걸린 테이블을 조회하거나 변경하면 실행 시점에 TBR-15095 오류가 발생합니다. 앞 절에서 설명한 것처럼 컴파운드 트리거의 문장 섹션을 활용하면 피할 수 있습니다.

다음은 앞의 예제에서 만든 PAY\_TBL 테이블에 이 제약을 어기는 로우 트리거를 만들었을 때의 예입니다.

```
CREATE OR REPLACE TRIGGER pay_tbl_cnt
AFTER INSERT ON pay_tbl
FOR EACH ROW
DECLARE
    v_cnt NUMBER;
BEGIN
    SELECT COUNT(*) INTO v_cnt FROM pay_tbl;
END;
/

INSERT INTO pay_tbl VALUES (3, 3000);
```

다음은 실행 결과입니다.

```
TBR-15095: Trigger/function cannot access table 'PAY_TBL' while it is being modified.
TBR-15163: Unhandled exception at SYS.PAY_TBL_CNT,  line 7.
```

### **트랜잭션 제어 문장과 DDL 사용 불가**

트리거 본문에서 COMMIT, ROLLBACK 등의 트랜잭션 제어 문장이나 DDL을 실행하면 TBR-15097 오류가 발생합니다. 트리거를 자율 트랜잭션으로 선언하면 트리거 안에서 독립적으로 커밋할 수 있습니다. 자세한 내용은 [SQL 문장의 실행](/tibero-manuals/7.2.6.manuals/tbpsm-guide/tbpsm-sql-execution.md)의 "[자율 트랜잭션](/tibero-manuals/7.2.6.manuals/tbpsm-guide/tbpsm-sql-execution.md#undefined-2)"을 참고합니다.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.tibero.com/tibero-manuals/7.2.6.manuals/tbpsm-guide/trigger.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
