일반적인 개발 언어처럼 SQL에도 절차 지향적인 프로그램이 가능하도록 DBMS 벤더별로 PL(Procedural Language)/SQL(Oracle), SQL/PL(DB2), T-SQL(SQL Server) 등의 절차형 SQL을 제공하고 있다.
절차형 SQL을 이용하면 SQL문의 연속적인 실행이나 조건에 따른 분기처리를 이용하여 특정 기능을 수행하는 저장 모듈을 생성할 수 있다.
Oracle의 PL/SQL은 Block 구조로 되어있고 Block 내에는 DML 문장과 QUERY 문장, 그리고 절차형 언어(IF, LOOP) 등을 사용할 수 있으며, 절차적 프로그래밍을 가능하게 하는 트랜잭션 언어이다.
이런 PL/SQL을 이용하여 다양한 저장 모듈(Stored Module)을 개발할 수 있다.
저장 모듈이란 PL/SQL 문장을 데이터베이스 서버에 저장하여 사용자와 애플리케이션 사이에서 공유할 수 있도록 만든 일종의 SQL 컴포넌트 프로그램이며, 독립적으로 실행되거나 다른 프로그램으로부터 실행될 수 있는 완전한 실행 프로그램이다.
Oracle의 저장 모듈에는 Procedure, User Defined Function, Trigger가 있다.
PL/SQL의 특징
위 그림은 PL/SQL Architecture이다. PL/SQL Block 프로그램을 입력받으면 SQL 문장과 프로그램 문장을 구분하여 처리한다.
즉, 프로그램 문장은 PL/SQL 엔진이 처리하고 SQL 문장은 Oracle 서버의 SQL Statement Executor가 실행하도록 작업을 분리하여 처리한다.
다음은 PL/SQL의 블록 구조를 표현한 내용이다.
User Defined Function이나 Trigger의 생성 방법이나 사용 목적은 다르지만 기본적인 문법은 비슷하기 때문에 여기에서는 Stored Procedure를 통해서 PL/SQL에 대한 기본적인 문법을 정리한다.
CREATE [OR REPLACE] Procedure [Procedure_name]
( argument1 [mode] data_type1,
argument2 [mode] data_type2,
… … )
IS [AS]
… …
BEGIN
… …
EXCEPTION
… …
END;
/
-- 생성된 프로시저를 삭제하는 명령어
DROP Procedure [Procedure_name];
CREATE TABLE 명령어로 테이블을 생성하듯 CREATE 명령어로 데이터베이스 내에 프로시저를 생성할 수 있다.
이렇게 생성한 프로시저는 데이터베이스 내에 저장된다. 프로시저는 개발자가 자주 실행해야 하는 로직을 절차적인 언어를 이용하여 작성한 프로그램 모듈이기 때문에 필요할 때 호출하여 실행할 수 있다.
OR REPLACE
데이터베이스 내에 같은 이름의 프로시저가 있을 경우, 기존의 프로시저를 무시하고 새로운 내용으로 덮어쓰기 하겠다는 의미이다.
Argument
프로시저가 호출될 때 프로시저 안으로 어떤 값이 들어오거나 혹은 프로시저에서 처리한 결과값을 운영 체제로 리턴시킬 매게 변수를 지정할 때 사용한다.
mode
3가지 유형의 변수를 지정할 수 있다.
/
데이터 베이스에게 프로시저를 컴파일하라는 명령어이다.
T-SQL은 근본적으로 SQL Server를 제어하기 위한 언어로서, T-SQL은 엄격히 말하면, MS사에서 ANSI/ISO 표준의 SQL에 약간의 기능을 더 추가해 보완적으로 만든 것이다.
T-SQL을 이용하여 다양한 저장 모듈(Stored Module)을 개발할 수 있는데, T-SQL의 프로그래밍 기능은 아래와 같다.
T-SQL과 타 DBMS가 제공하는 SQL은 약간만 다를 뿐 그 맥락은 같이 하기 때문에, 조금의 변경 사항만 적용하면 같은 기능을 수행할 수 있다.
다음은 T-SQL의 구조를 표현한 내용이다. PL/SQL과 유사하다.
앞으로 살펴볼 User Defined Function이나 Trigger의 생성 방법과 사용 목적은 Stored Procedure와 다르지만 기본적인 문법은 비슷하기 때문에 여기서는 Stored Procedure를 통해서 T-SQL에 대한 기본적인 문법을 정리한다.
CREATE Procedure [schema_name.]Procedure_name
@parameter1 data_type1 [mode],
@parameter2 data_type2 [mode],
… …
WITH
AS
… …
BEGIN
… …
ERROR 처리
… …
END;
-- 생성된 프로시저를 삭제하는 명령어
DROP Procedure [schema_name.]Procedure_name;
CREATE TABLE 명령어로 테이블을 생성하듯 CREATE 명령어로 데이터베이스 내에 프로시저를 생성할 수 있다. 이렇게 생성한 프로시저는 데이터베이스 내에 저장된다. 프로시저는 개발자가 자주 실행해야 하는 로직을 절차적인 언어를 이용하여 작성한 프로그램 모듈이기 때문에 필요할 때 호출하여 실행할 수 있다.
프로시저의 변경이 필요한 경우 Oracle은 [CREATE OR REPLACE]와 같이 하나의 구문으로 처리하지만 SQL Server는 CREATE 구문을 ALTER 구문으로 변경하여야 한다.
@parameter
프로시저가 호출될 때 프로시저 안으로 어떤 값이 들어오거나 혹은 프로시저에서 처리한 결과 값을 리턴 시킬 매개 변수를 지정할 때 사용한다.
mode
4가지 유형의 변수를 지정할 수 있다.
WITH
3가지 옵션을 지정할 수 있다.
SCOTT 유저가 소유하고 있는 DEPT 테이블에 새로운 부서를 등록하는 Procedure를 작성한다. SCOTT 유저가 기본적으로 소유한 DEPT 테이블 구조는 아래와 같다
[Oracle]
CREATE OR REPLACE Procedure p_DEPT_insert ( ---- ①
v_DEPTNO in number,
v_dname in varchar2,
v_loc in varchar2,
v_result out varchar2)
IS
cnt number := 0;
BEGIN
SELECT COUNT(*) INTO CNT ---- ②
FROM DEPT
WHERE DEPTNO = v_DEPTNO
AND ROWNUM = 1;
if cnt >0 then ---- ③
v_result := '이미 등록된 부서번호이다.';
else
INSERT INTO DEPT (DEPTNO, DNAME, LOC) -- ④
VALUES (v_DEPTNO, V_dname, v_loc);
COMMIT; ---- ⑤
v_result := '입력 완료!!';
end if;
EXCEPTION ---- ⑥
WHEN OTHERS THEN
ROLLBACK;
v_result := 'ERROR 발생';
END;
/
[SQL Server]
CREATE Procedure dbo.p_DEPT_insert ---- ①
@v_DEPTNO int,
@v_dname varchar(30),
@v_loc varchar(30),
@v_result varchar(100) OUTPUT
AS
DECLARE @cnt int
SET @cnt = 0
BEGIN
SELECT @cnt=COUNT(*) ---- ②
FROM DEPT
WHERE DEPTNO = @v_DEPTNO
IF @cnt >0 ---- ③
BEGIN
SET @v_result = '이미 등록된 부서번호이다.'
RETURN
END
ELSE
BEGIN
BEGIN TRAN
INSERT INTO DEPT (DEPTNO, DNAME, LOC) -- ④
VALUES (@v_DEPTNO, @v_dname, @v_loc)
IF @@ERROR<>0
BEGIN
ROLLBACK ---- ⑥
SET @v_result = 'ERROR 발생'
RETURN
END
ELSE
BEGIN
COMMIT ---- ⑤
SET @v_result = '입력 완료!!'
RETURN
END
END
END
DEPT 테이블은 DEPTNO 칼럼이 PRIMARY KEY로 설정되어 있으므로, DEPTNO 칼럼에는 유일한 값을 넣어야만 한다.
위 예제에 대한 설명은 다음과 같다.
앞에 있는 프로시저를 작성하면서 주의해야 할 몇가지 문법적 요소가 있다.
첫째, PL/SQL 및 T-SQL에서는 다양한 변수가 있다. 예제에서 나온 cnt라는 변수를 SCALAR 변수라고 한다. SCALAR 변수는 사용자의 임시 데이터를 하나만 저장할 수 있는 변수이며 거의 모든 형태의 데이터 유형을 지정할 수 있다.
둘째, PL/SQL에서 사용하는 SQL 구문은 대부분 지금까지 살펴본 것과 동일하게 사용할 수 있지만 SELECT 문장은 다르다. PL/SQL에서 사용하는 SELECT 문장은 결과값이 반드시 있어야 하며, 그 결과 역시 반드시 하나여야 한다. 조회 결과가 없거나 하나 이상인 경우에는 에러를 발생시킨다. T-SQL에서는 결과 값이 없어도 에러가 발생하지 않는다.
셋재, T-SQL을 비롯하여 일반적으로 대입 연산자는 "="을 사용하지만 PL/SQL에서는 ":="를 사용한다.
넷째, 에러 처리를 담당하는 EXCEPTION에는 WHEN ~ THEN 절을 사용하여 에러의 종류별로 적절히 처리한다. OTHERS를 이용하여 모든 에러를 처리할 수 있지만 정확하게 에러를 처리하는 것이 좋다. T-SQL에서는 에러 처리를 다양하게 처리할 수 있으며 위의 예제는 그 한 예이다.
다음은 지금까지 작성한 프로시저를 실행하여 기능을 테스트한 과정이다.
T-SQL로 작성한 프로시저를 실행하기 위해서는 일반적으로 SQL Server에서 제공하는 기본 클라이언트 프로그램인 SQL Server MANAGEMENT STUDIO를 사용한다.
User Defined Function은 Procedure처럼 절차형 SQL을 로직과 함께 데이터베이스 내에 저장해 놓은 명령문의 집합을 의미한다. 앞에서 학습한 SUM, SUBSTR, NVL 등의 함수는 벤더에서 미리 만들어둔 내장 함수이고, 사용자가 별도의 함수를 만들 수도 있다.
Function이 Procedure와 다른 점은 RETURN을 사용해서 하나의 값을 반드시 되돌려 줘야 한다는 것이다.
즉 Function은 Procedure와는 달리 SQL 문장에서 특정 작업을 수행하고 반드시 수행결과 값을 리턴한다.
K-리그 8월 경기결과와 두 팀간의 점수차를 ABS 함수를 사용하여 절대값으로 출력한다.
[Oracle]
SELECT
SCHE_DATE 경기일자,
HOMETEAM_ID || ' - ' || AWAYTEAM_ID 팀들,
HOME_SCORE || ' - ' || AWAY_SCORE SCORE,
ABS(HOME_SCORE - AWAY_SCORE) 점수차
FROM SCHEDULE
WHERE GUBUN = 'Y'
AND SCHE_DATE BETWEEN '20120801' AND '20120831'
ORDER BY SCHE_DATE;
[SQL Server]
SELECT
SCHE_DATE 경기일자,
HOMETEAM_ID || ' - ' || AWAYTEAM_ID AS 팀들,
HOME_SCORE || ' - ' || AWAY_SCORE AS SCORE,
ABS(HOME_SCORE - AWAY_SCORE) AS 점수차
FROM SCHEDULE
WHERE GUBUN = 'Y'
AND SCHE_DATE BETWEEN '20120801' AND '20120831'
ORDER BY SCHE_DATE;
ABS 함수를 만드는데, INPUT 값으로 숫자만 들어온다고 가정한다.
[Oracle]
CREATE OR REPLACE Function UTIL_ABS
(v_input in number) ---- ①
return NUMBER
IS
v_return number := 0; ---- ②
BEGIN
if v_input <0 then ---- ③
v_return := v_input * -1;
else v_return := v_input;
end if;
RETURN v_return; ---- ④
END;
/
[SQL Server]
CREATE Function dbo.UTIL_ABS
(@v_input int) ---- ①
RETURNS int
AS
BEGIN
DECLARE @v_return int ---- ②
SET @v_return=0
IF @v_input <0 ---- ③
SET @v_return = @v_input * -1
ELSE
SET @v_return = @v_input
RETURN @v_return; ---- ④
END
UTIL_ABS Function의 처리 과정은 다음과 같다.
함수를 이용하여 앞의 SQL을 수정하여 실행한다.
[Oracle]
SELECT
SCHE_DATE 경기일자,
HOMETEAM_ID || ' - ' || AWAYTEAM_ID 팀들,
HOME_SCORE || ' - ' || AWAY_SCORE SCORE,
UTIL_ABS(HOME_SCORE - AWAY_SCORE) 점수차
FROM SCHEDULE
WHERE GUBUN = 'Y'
AND SCHE_DATE BETWEEN '20120801' AND '20120831'
ORDER BY SCHE_DATE;
[SQL Server]
SELECT
SCHE_DATE 경기일자,
HOMETEAM_ID || ' - ' || AWAYTEAM_ID AS 팀들,
HOME_SCORE || ' - ' || AWAY_SCORE AS SCORE,
UTIL_ABS(HOME_SCORE - AWAY_SCORE) AS 점수차
FROM SCHEDULE
WHERE GUBUN = 'Y'
AND SCHE_DATE BETWEEN '20120801' AND '20120831'
ORDER BY SCHE_DATE;
Trigger란 특정한 테이블에 INSERT, UPDATE, DELETE와 같은 DML문이 수행되었을 때, 데이터베이스에서 자동으로 동작하도록 작성된 프로그램이다.
즉 사용자가 직접 호출하여 사용하는 것이 아니고 데이터베이스에서 자동적으로 수행하게 된다.
Trigger는 테이블과 뷰, 데이터베이스 작업을 대상으로 정의할 수 있으며, 전체 트랜잭션 작업에 대해 발생되는 Trigger와 각 행에 대해서 발생되는 Trigger가 있다.
요구 사항은 다음과 같다고 가정한다.
어떤 쇼핑몰에 하루에 수만 건의 주문이 들어온다. 주문 데이터는 주문일자, 주문상품, 수량, 가격이 있으며, 사장을 비롯한 모든 임직원이 일자별, 상품별 총 판매수량과 총 판매가격으로 구성된 주문 실적을 온라인상으로 실시간 조회한다고 했을 때, 한 사람의 임직원이 조회할 때마다 수만 건의 데이터를 읽어 계산해야 한다. 가끔 한 번씩 조회한다면 문제가 없을 수도 있으나 번번하게 조회작업이 일어난다면 조회작업에 많은 시간을 허비할 수 있다.
트리거(Trigger)를 사용하여 주문한 건이 입력될 때마다, 일자별 상품별로 판매수량과 판매금액을 집계하여 집계자료를 보관하도록 한다.
[Oracle]
CREATE TABLE ORDER_LIST (
ORDER_DATE CHAR(8) NOT NULL,
PRODUCT VARCHAR2(10) NOT NULL,
QTY NUMBER NOT NULL,
AMOUNT NUMBER NOT NULL);
CREATE TABLE SALES_PER_DATE (
SALE_DATE CHAR(8) NOT NULL,
PRODUCT VARCHAR2(10) NOT NULL,
QTY NUMBER NOT NULL,
AMOUNT NUMBER NOT NULL);
[SQL Server]
CREATE TABLE ORDER_LIST (
ORDER_DATE CHAR(8) NOT NULL,
PRODUCT VARCHAR(10) NOT NULL,
QTY INT NOT NULL,
AMOUNT INT NOT NULL);
CREATE TABLE SALES_PER_DATE (
SALE_DATE CHAR(8) NOT NULL,
PRODUCT VARCHAR(10) NOT NULL,
QTY INT NOT NULL,
AMOUNT INT NOT NULL);
Trigger를 작성한다.
Trigger의 역할은 ORDER_LIST에 주문 정보가 입력되면 주문 정보의 주문 일자(ORDER_LIST.ORDER_DATE)와 주문 상품(ORDER_LIST.PRODUCT)을 기준으로 판매 집계 테이블(SALES_PER_DATE)에 해당 주문 일자의 주문 상품 레코드가 존재하면 판매 수량과 판매 금액을 더하고 존재하지 않으면 새로운 레코드를 입력한다.
[Oracle]
CREATE OR REPLACE Trigger SUMMARY_SALES ---- ①
ALTER INSERT
ON ORDER_LIST
FOR EACH ROW
DECLARE ---- ②
o_date ORDER_LIST.order_date%TYPE;
o_prod ORDER_LIST.product%TYPE;
BEGIN
o_date := :NEW.order_date;
o_prod := :NEW.product;
UPDATE SALES_PER_DATE ---- ③
SET qty = qty + :NEW.qty
amount = amount + :NEW.amount
WHERE sale_date = o_date
AND product = o_prod;
if SQL%NOTFOUND then ---- ④
INSERT INTO SALES_PER_DATE
VALUES(o_date, o_prod, :NEW.qty, :NEW.amount);
end if;
END;
/
SUMMARY_SALES Trigger의 처리절차를 설명하면 다음과 같다.
[SQL Server]
CREATE Trigger dbo.SUMMARY_SALES ---- ①
ON ORDER_LIST
AFTER INSERT
AS
DECLARE
@o_date DATETIME,@o_prod INT,@qty int, @amount int
BEGIN
SELECT
@o_date=order_date,
@o_prod=product,
@qty=qty,
@amount=amount
FROM inserted ---- ②
UPDATE SALES_PER_DATE ---- ③
SET qty = qty + @qty,
amount = amount + @amount
WHERE sale_date = @o_date
AND product = @o_prod;
IF @@ROWCOUNT=0 ---- ④
INSERT INTO SALES_PER_DATE
VALUES(@o_date, @o_prod, @qty, @amount)
END
SUMMARY_SALES Trigger의 처리절차를 설명하면 다음과 같다.
ORDER_LIST 테이블에 주문 정보를 입력한다.
[Oracle]
SQL> SELECT * FROM ORDER_LIST;
선택된 레코드가 없다.
SQL> SELECT * FROM SALES_PER_DATE; 선택된 레코드가 없다.
SQL> INSERT INTO ORDER_LIST VALUES('20120901', 'MONOPACK', 10, 300000);
1개의 행이 만들어졌다.
SQL> COMMIT;
커밋이 완료되었다.
[SQL Server]
SELECT * FROM ORDER_LIST;
선택된 레코드가 없다.
SELECT * FROM SALES_PER_DATE;
선택된 레코드가 없다.
INSERT INTO ORDER_LIST VALUES('20120901', 'MONOPACK', 10, 300000);
1개의 행이 만들어졌다.
주문 정보와 판매 집계 테이블에 같은 데이터가 들어왔는지 확인한다.
다시 한 번 같은 데이터를 입력해보고, 두 테이블의 데이터를 확인한다.
이번에는 다른 상품으로 주문 데이터를 입력한 후 두 테이블의 결과를 조회해 보고 트랜잭션을 ROLLBACK 수행한다. 판매 데이터의 입력 취소가 일어나면, 주문 정보 테이블과 판매 집계 테이블에 동시에 입력(수정) 취소가 일어나는지 확인해본다.
ROLLBACK을 하면 하나의 트랜잭션이 취소가 되어 Trigger로 입력된 정보까지 하나의 트랜잭션으로 인식하여 두 테이블 모두 입력 취소가 되는 것을 보여 주고 있다. Trigger는 데이터베이스에 의해 자동 호출되지만 결국 INSERT, UPDATE, DELETE 문과 하나의 트랜잭션 안에서 일어나는 일련의 작업들이라 할 수 있다. Trigger는 데이터베이스 보안의 적용, 유효하지 않은 트랜잭션의 예방, 업무 규칙 자동 적용 제공 등에 사용될 수 있다.
프로시저는 BEGIN ~ END 절 내에 COMMIT, ROLLBACK과 같은 트랜잭션 종료 명령어를 사용할 수 있지만, 데이터베이스 트리거는 BEGIN ~ END 절 내에 사용할 수 없다.