기록하는 코더

[Oracle] PL/SQL Procedure 본문

DataBase/Oracle

[Oracle] PL/SQL Procedure

damda_di 2023. 1. 25. 11:47

PL/SQL

  • Procedure Language SQL
  • 표준 SQL에는 변수(상수), 분기(반복) 명령이 없음
  • 표준 SQL에 절차적 언어의 특징을 추가한 SQL
  • 익명블록(Anonymous Block), 저장프로시져(Stored Procedure), 함수(User Defined Function), 트리거(Trigger), 패키지(Package) 제공 -- 함수 반환값 o 프로시져 반환값 x -- 반환값이 없으면 SELECT문에 쓸 수 없다.

종류

  • Package
  • User Function
  • Stored Procedure
  • Stored Function
  • Trigger

익명블록(Anonymous Block)

  • PL/SQL의 기본 구조
  • 선언부(DECLARE)와 실행부(BEGIN ~ END)로 구성 -- 저장이 안됨 -> 재사용불가

 

(사용형식)
DECLARE
선언부 - 변수, 상수, 커서 선언
BEGIN
처리할 명령문을 절차적으로 기술
SQL문, 반복문,비교문, 입출력문 등


 [EXCEPTION
   예외처리문
 -- 예외처리 최후..? -> WHEN ORDERS..
 ]
 
 END;
 . 할당연산자 : ':='
 -- '='의 기능은 같다는 의미로만 사용됨 (할당 연산자는 ':='으로 사용) -- 파스칼 문법,,,
 . 실행영역에 사용되는 SQL문 중 SELECT문의 형식
 SELECT 컬럼list
      INTO 변수list
    FROM 테이블명
 [WHERE 조건절]

-- 서브쿼리에 있는 SELECT절은 바뀌지 않고 메인쿼리에 있는 SELECT문만 바뀜

  • '컬럼'의 갯수, 순서, 데이터타입과 '변수'의 갯수, 순서, 데이터타입은 일치해야 함
  • P_YEAR 변수 설정, &P_YEAR 변수 참조 --ACCEPT ~ PROMPT -> 입력 --DBMS_OUTPUT.PUT_LINE -> print

 

변수 -- 계산의 중간값을 일시적으로 저장

  • 개발언어의 변수와 동일 기능 제공
  • 종류 : SCLAR 변수, REFERENCE 변수, BIND 변수, COMPOSITE 변수
  • 변수 선언 형식 변수(상수) [CONSTANT] 데이터타입 [:=초기값]
  • 데이터타입 . SQL에서 사용하는 모든 자료형 사용 . BINARY_INTEGER, PLS_INTEGER, BOOLEAN 사용 가능 . 참조형 데이터타입 사용 형식
    • 컬럼참조형 : 테이블명.컬럼명%TYPE
    • 행참조형 : 테이블명%ROWTYPE
  • 숫자형 변수는 사용 전 반드시 초기화시켜야 함
  • -- 초기화 x => 오류
  • -- 마련된 기억장소에 값을 전달시키는 과정 -> Binding
  • -- 자바에서 매개변수와 비슷
  • -- COMPOSITE 변수 -> 배열형 변수
  • -- 자바에서 왼쪽값엔 상수 못쓰임 (변수만 사용가능) (ex. a = 1 (a는 기억장소,변수)(상수는 literal 값이 불변))
  • -- [CONSTANT]가 오면 상수가 됨 => 변수 선언시 사용 x -- 상수는 [:=초기값] 생략불가
  • -- BINARY_INTEGER, PLS_INTEGER, 4byte 빠르긴한데 값이 작아서 잘 안씀,,
  • -- Oracle BOOLEAN -> true, false, null

 


예시코드

/
-- PL/SQL : Procedual Language(절차적인 언어) / Structed Query Language
-- 절차적인 : 분기, 반복, 변수
-- DECLARE, EXCEPTION : 선택( 생략가능)
-- BEGIN, END : 필수

DECLARE
   
BEGIN
    -- I : 자동선언정수형 변수
    FOR I IN 1..30 LOOP
        INSERT INTO BOOK(BOOK_ID, TITLE, CATEGORY, PRICE, INSERT_DATE, CONTENT)
        VALUES(
            (SELECT NVL(MAX(BOOK_ID),0) + 1 FROM BOOK),
            '제목'||I,'카테고리'||I,10000,SYSDATE,'내용'||I   
        );
    END LOOP;
    COMMIT;
    EXCEPTION
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('오류 발생 : ' || SQLERRM);
END;

/
-- 널이 있으면 NVL 사용 => NULL처리
SELECT NVL(MAX(BOOK_ID),0) + 1 FROM BOOK;

 

사용예) 키보드로 년도를 입력 받아 윤년과 평년을 구별하는 익명 블록을 작성하시오
  ACCEPT P_YEAR PROMPT '년도입력 : '
  DECLARE
    V_YEAR NUMBER:=0;  
  BEGIN
    V_YEAR:=TO_NUMBER('&P_YEAR');
    IF(MOD(V_YEAR,4)=0 AND MOD(V_YEAR,100)!=0) OR MOD(V_YEAR,400)=0 THEN
      DBMS_OUTPUT.PUT_LINE(V_YEAR||'년도는 윤년입니다.');
    ELSE
      DBMS_OUTPUT.PUT_LINE(V_YEAR||'년도는 평년입니다.');    
    END IF;
    
    EXCEPTION
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('에러발생 : '||SQLERRM);     
  END;
사용예) 여성 회원 중 마일리지가 가장 많은 회원의 회원번호, 회원명, 직업, 마일리지를 조회하는
        익명 블록을 작성하시오        
  DECLARE
    V_MID MEMBER.MEM_ID%TYPE; -- 컬럼 참조형 (MEMBER테이블의 MEM_ID컬럼과 같은 컬럼으로 지정)
    V_NAME MEMBER.MEM_NAME%TYPE;
    V_JOB MEMBER.MEM_JOB%TYPE;
    V_MILE NUMBER:=0; -- 값 초기화
  BEGIN
    -- 여성 회원 중 마일리지가 가장 많은 회원
    SELECT MAX(MEM_MILEAGE) INTO V_MILE
      FROM MEMBER
     WHERE SUBSTR(MEM_REGNO2,1,1) IN('2','4');
     -- MEM_ID를 쓰면 24명 회원의 마일리지 최대값이 모두 나옴
     -- 여러개의 자료를 출력하고 싶음 -> INTO 다음으로 나오는 변수때문에 (한순간에 하나밖에 저장 못함) 커서를 이용해서 처리
           
    SELECT MEM_ID, MEM_NAME, MEM_JOB INTO V_MID, V_NAME, V_JOB
      FROM MEMBER
     WHERE MEM_MILEAGE=V_MILE
       AND SUBSTR(MEM_REGNO2,1,1) IN ('2','4')
       AND ROWNUM=1;
    -- 여러개의 자료를 출력하고 싶음 -> INTO 다음으로 나오는 변수때문에 (한순간에 하나밖에 저장 못함) 커서를 이용해서 처리
     DBMS_OUTPUT.PUT_LINE(V_MID||' '||V_NAME||'  '||V_JOB||'  '||V_MILE);     
  END;
  
사용예) 년월을 6자리 문자열로 받아 해당 기간동안 매출총액을 조회하시오
    SELECT TO_CHAR(SUM(A.CART_QTY*B.PROD_PRICE),'9,999,999,999') AS 매출액
      FROM CART A, PROD B
     WHERE SUBSTR(A.CART_NO,1,6) LIKE ('2020%')
       AND A.CART_PROD=B.PROD_ID;
-- 일반(내부) 변수는 V_, 바인딩(입력) 변수는 P_

  ACCEPT P_YEAR PROMPT '년도와 월을 6자리로 입력 : '
  DECLARE
   V_PERIOD CHAR(7):= '&P_YEAR'||'%';
   V_SUM NUMBER:=0;   
  BEGIN
        SELECT SUM(A.CART_QTY*B.PROD_PRICE) INTO V_SUM
          FROM CART A, PROD B
         WHERE A.CART_PROD=B.PROD_ID
           AND A.CART_NO LIKE V_PERIOD;
       
    DBMS_OUTPUT.PUT_LINE('&P_YEAR'||'기간 매출액 합계 : '||V_SUM);  
  END;

 


STORED PROCEDURE

  • 저장 프로시져(프로시져)로 반환 값이 없는 모듈
  • 미리 컴파일되어 서버에 저장되고 필요시 호출하여 실행
  • 내부 네트워크의 트래픽을 감소시켜 수행 효율 증대 -- 컴퓨터 입력장치, 주기억장치 사이의 내부 네트워크 -- 함수와 다른 점 반환값이 없음 -> DML 명령, SELECT문에 사용 불가 -- EXCUTE, (?) 로 실행

 

 (사용형식)
CREATE [OR REPLACE] PROCEDURE 프로시져명
[(매개변수 [MODE] [데이터타입],
:   )]
IS|AS -- DECLARE와 같은 역할
선언부
BEGIN
실행부
END;
. 'MODE' : 매개변수의 종류 IN(입력전용), OUT(출력전용), INOUT(입출력용) 생략하면 IN으로 간주
-- 프로시져 안으로 입력 IN 프로시져 밖으로 내보낼때 OUT, 컴파일러에게 부담이 되는 INOUT는 되도록 사용X
 
 
 -- 반환값 : 변수에 의해서 반환되어지는 값 (변수를 통해서 받아야함 : 변수에 저장)
 -- => 변수를 사용할 수 있는 익명블록,프로시져,트리거,..(블록 구조)에서 사용
 -- 매개변수 -> 변수를 통해서 밖으로 전달 (변수 이름을 통해서 반환되어지는 경우)
 
 . '데이터타입' : 크기를 지정하지 않음
 -- 데이터 타입은 기술하되 크기는 기술하면 안된다.
 -- 매개변수가 없으면 괄호 다 생략

 

 (실행)
EXECUTE|EXEC 프로시져명[(매개변수list)];
OR
프로시져명[(매개변수list)]; -- 익명블록 또는 프로시져, 함수 내부에서 실행 시 execute|exec 생략

 


예시코드

사용예) 기간(시작년월과 종료년월)을 입력 받아 해당 기간에 상품별 매입집계를 출력하시오

CREATE OR REPLACE PROCEDURE PROC_BUYPROD_SUM(
P_START IN VARCHAR2,
P_END IN VARCHAR2)
IS
V_PID PROD.PROD_ID%TYPE;     -- 상품코드 => REC.BUY_PROD를 사용해서 안 씀..
V_PNAME PROD.PROD_NAME%TYPE; -- 상품명
V_QTY NUMBER:=0;             -- 수량집계
V_AMT NUMBER:=0;             -- 금액집계
V_SDATE DATE := TO_DATE(P_START||'01');
V_EDATE DATE := LAST_DAY(TO_DATE(P_END||'01'));

CURSOR CUR_BUYPROD_SUM IS
    SELECT DISTINCT BUY_PROD
      FROM BUYPROD
     WHERE BUY_DATE BETWEEN V_SDATE  AND V_EDATE;    -- 상품코드만 중복없이 나오도록 집계

BEGIN
	DBMS_OUTPUT.PUT_LINE('------------------------------------------------------------');
	FOR REC IN CUR_BUYPROD_SUM LOOP
	SELECT SUM(A.BUY_QTY), SUM(A.BUY_QTY*B.PROD_COST) INTO V_QTY, V_AMT
	  FROM BUYPROD A, PROD B
	 WHERE A.BUY_PROD= B.PROD_ID
		 AND A.BUY_PROD=REC.BUY_PROD
		 AND A.BUY_DATE BETWEEN V_SDATE AND V_EDATE;
	SELECT PROD_NAME INTO V_PNAME
	  FROM PROD
	 WHERE PROD_ID=REC.BUY_PROD;
-- REC.BUY_PROD -> 커서로 된 BUY_PROD 읽어오는 방법
-- BUYPROD A, PROD B 테이블을 두 개 쓴 이유 : 실무에 나가면 예시처럼 매입테이블에 매입단가가 포함되어있는 경우가 거의 없음!
-- 보통은 상품 테이블 내에 들어있다.
   DBMS_OUTPUT.PUT_LINE(REC.BUY_PROD||'  '||RPAD(V_PNAME,20)||'  '||TO_CHAR(V_QTY,'99,999')||'  '||TO_CHAR(V_AMT,'999,999,999'));
   DBMS_OUTPUT.PUT_LINE('------------------------------------------------------------');
	END LOOP;
END;

EXECUTE PROC_BUYPROD_SUM('202002','202005');
사용예) 부서번호를 입력하여 해당부서의 부서번호, 부서명, 책임사원 이름을 출력하는 프로시져를 생성하시오

CREATE OR REPLACE PROCEDURE PROC_DEPT_MANAGER(
  P_DID IN HR.DEPARTMENTS.DEPARTMENT_ID%TYPE)
IS
    V_DID HR.DEPARTMENTS.DEPARTMENT_ID%TYPE; -- 부서번호
    V_DNAME VARCHAR2(200);                   -- 부서명
    V_MNAME HR.EMPLOYEES.EMP_NAME%TYPE;      -- 사원명
    V_FLAG HR.DEPARTMENTS.MANAGER_ID%TYPE;    -- 관리자번호
BEGIN
    SELECT MANAGER_ID INTO V_FLAG
      FROM HR.DEPARTMENTS
     WHERE P_DID=DEPARTMENT_ID;

    IF V_FLAG IS NULL THEN
        DBMS_OUTPUT.PUT_LINE('사용되지 않은 부서입니다.');
    ELSE
        SELECT A.DEPARTMENT_ID, A.DEPARTMENT_NAME, B.EMP_NAME
          INTO V_DID,V_DNAME,V_MNAME
          FROM HR.DEPARTMENTS A, HR.EMPLOYEES B
         WHERE A.MANAGER_ID=B.EMPLOYEE_ID
           AND P_DID=A.DEPARTMENT_ID;
         DBMS_OUTPUT.PUT_LINE(LPAD(V_DID,4)||RPAD(V_DNAME,20)||V_MNAME);
     END IF;
END;

(실행)
EXECUTE PROC_DEPT_MANAGER(80);
EXECUTE PROC_DEPT_MANAGER(160);

 

사용예) 회원번호를 입력받아 회원명과 주소, 마일리지를 출력하는 프로시져를 작성하시오

CREATE OR REPLACE PROCEDURE PROC_MEMBER_OUTPUT(
    P_MID IN MEMBER.MEM_ID%TYPE,
    P_NAME OUT MEMBER.MEM_NAME%TYPE,
    P_ADDR OUT VARCHAR2,
    P_MILEAGE OUT NUMBER)
IS
BEGIN
    SELECT MEM_NAME,MEM_ADD1||' '||MEM_ADD2, MEM_MILEAGE
      INTO P_NAME, P_ADDR, P_MILEAGE
      FROM MEMBER
     WHERE MEM_ID = P_MID;
END;

(실행)
DECLARE
V_NAME MEMBER.MEM_NAME%TYPE;
V_ADDR VARCHAR2(255);
V_MILEAGE NUMBER:=0;
BEGIN
PROC_MEMBER_OUTPUT('k001',V_NAME,V_ADDR,V_MILEAGE);
-- 입력하고(MEM_NAME), 출력용 변수에 전달받음
-- 프로시져명 (IN|OUT 매개변수)
DBMS_OUTPUT.PUT_LINE('회원번호 : k001');
DBMS_OUTPUT.PUT_LINE('회원명 : '||V_NAME);
DBMS_OUTPUT.PUT_LINE('주소 : '||V_ADDR);
DBMS_OUTPUT.PUT_LINE('마일리지 : '||V_MILEAGE);
END;

 


User Defined Function

  • 사용자 정의 함수
  • 반환 값이 존재
  • 나머지특징은 프로시져와 동일 -- 크기 지정x
(사용형식)
CREATE [OR REPLACE] FUNCTION 함수명[(
매개변수 [MODE] 데이터타입[,]
:
매개변수 [MODE] 데이터타입)]
RETURN 데이터타입
IS|AS
선언부
BEGIN
실행부
END;
. 'RETURN 데이터타입' : 반환 데이터 타입 기술(크기 기술하지 않음)
. 실행부에 반드시 하나 이상의 RETURN 값 expr문이 나와야 함 -- 없으면 오류
-- expr의 데이터 타입이 같아야한다.
-- DML명령, SELECT문에 사용

 

사용예) 장바구니 테이블에 오늘날짜의 장바구니 번호를 자동생성하는 함수를 작성하고 다음 자료를 장바구니에 저장하시오

날짜 : 오늘 (2022.10.12)
회원 : 'k001'
구매상품번호 : 'P201000010'
수량 : 5
INSERT INTO CART_VALUES('k001',FN_CREATE_CARTNO(TO_CHAR(SYSDATE,'YYYYMMDD', 'k001',
'P201000010', 5))); 

구매상품번호 : 'P202000021'
수량 : 2
INSERT INTO CART_VALUES('k001',
                            FN_CREATE_CARTNO(TO_CHAR(SYSDATE,'YYYYMMDD', 'k001',
                            'P202000021', 2)));


구매상품번호 : 'P302000015'
수량 : 7
INSERT INTO CART_VALUES('k001',
                            FN_CREATE_CARTNO(TO_CHAR(SYSDATE,'YYYYMMDD', 'k001',
                            'P302000015', 7)));
CREATE OR REPLACE FUNCTION FN_CREATE_CARTNO(
	P_DATE IN VARCHAR2,
	P_MID IN MEMBER.MEM_ID%TYPE)
	RETURN CHAR
IS
	V_CNT NUMBER:=0; --로그인 순서
	V_CART_NO CART.CART_NO%TYPE; --임시장바구니번호
	V_FLAG NUMBER:=0; -- 입력된 날짜의 자료수
	V_MID MEMBER.MEM_ID%TYPE; -- 입력된 날짜에 제일 마지막에 구매한 회원번호
BEGIN
--입력된 날짜에 판매자료 존재여부 판단(1. 아무도 구매X)
SELECT COUNT(*) INTO V_FLAG
  FROM CART
 WHERE CART_NO LIKE P_DATE||'%';



 IF V_FLAG=0 THEN
    V_CART_NO:=P_DATE||TRIM('00001');
 -- 구매이력이 하나도 없을 경우

 ELSE
-- 2. 제일 큰 값을 갖는 장바구니 번호가 같은지 확인 => 다르면 다른 장바구니 번호 부여
    SELECT MAX(CART_NO) INTO V_CART_NO
      FROM CART
     WHERE CART_NO LIKE P_DATE||'%';
    -- 임시 장바구니 번호에 제일 큰 장바구니 숫자

    SELECT DISTINCT CART_MEMBER INTO V_MID
      FROM CART
     WHERE V_CART_NO=CART_NO;
     -- 가장 큰 장바구니 번호인 회원번호

     IF V_MID != P_MID THEN
        V_CART_NO:=P_DATE||
                   TRIM(TO_CHAR(TO_NUMBER(SUBSTR(V_CART_NO,9))+1,'00000'));
     -- 이전 구매 회원이 아닌 경우
     -- => 장바구니 번호 : 날짜 + (이전장바구니 번호 +1) 부여
     END IF;
 END IF;
 RETURN V_CART_NO;
END;

(실행)
SELECT FN_CREATE_CARTNO('20200406','o001')
FROM DUAL;
사용예) 상품코드를 입력받아 2020년 6월 판매수량 합계를 구하는 함수를 작성하시오
Alias 상품코드, 상품명, 판매단가, 판매수량합계

CREATE OR REPLACE FUNCTION FN_SUM_QTY(
	P_PID IN PROD.PROD_ID%TYPE)
RETURN NUMBER
IS
    P_QTYSUM NUMBER:=0; -- 판매수량합계
BEGIN
    SELECT SUM(CART_QTY) INTO P_QTYSUM
      FROM CART
     WHERE CART_PROD=P_PID
       AND CART_NO LIKE '202006%';
    RETURN P_QTYSUM;
END;


(실행)
SELECT PROD_ID AS 상품코드,
			 PROD_NAME AS 상품명,
			 PROD_PRICE AS 판매단가,
			 FN_SUM_QTY(PROD_ID) AS 판매수량합계
	FROM PROD
 ORDER BY 1;
-- OUTER JOIN이 된다.
사용예) 각 부서별 부서번호, 부서명, 책임사원명, 인원수, 급여합계, 평균급여를 조회하시오
인원수, 급여합계, 평균급여는 함수로 구하시오.


SELECT A.DEPARTMENT_ID AS 부서번호,
       B.DEPARTMENT_NAME AS 부서명,
     --A.EMP_NAME AS 책임사원명,
       COUNT(*) AS 인원수,
       SUM(A.SALARY) AS 급여합계,
       ROUND(AVG(A.SALARY)) AS 평균급여
  FROM HR.EMPLOYEES A, HR.DEPARTMENTS B, HR.EMPLOYEES C
 WHERE A.DEPARTMENT_ID=B.DEPARTMENT_ID
 --AND A.EMPLOYEE_ID=C.MANAGER_ID
 GROUP BY A.DEPARTMENT_ID,B.DEPARTMENT_NAME --, A.EMP_NAME
 ORDER BY 1;
    -- 책임사원명을 넣으면 부서번호 누락, 인원수 오류

CREATE OR REPLACE FUNCTION DEPT_EMP01(
	P_DID IN HR.DEPARTMENTS.DEPARTMENT_ID%TYPE)
	RETURN NUMBER
IS
    P_CNT NUMBER:=0;
    P_SALSUM NUMBER:=0;
    P_SALAVG NUMBER:=0;
BEGIN
    SELECT COUNT(*) INTO P_CNT
      FROM HR.EMPLOYEES A, HR.DEPARTMENTS
     WHERE A.DEPARTMENT_ID=B.DEPARTMENT_ID

    SELECT SUM(SALARY) INTO P_SALSUM
      FROM HR.EMPLOYEES

    SELECT AVG(SALARY) INTO P_SALAVG
      FROM HR.EMPLOYEES
END;




CREATE OR REPLACE FUNCTION FN_DEPT_EMP_CNT(
      P_DID HR.EMPLOYEES.DEPARTMENT_ID%TYPE)
      RETURN NUMBER
   IS
      V_CNT NUMBER:=0;
   BEGIN
    SELECT COUNT(*) INTO V_CNT
	  FROM HR.EMPLOYEES A
	 WHERE DEPARTMENT_ID=P_DID;	
     RETURN V_CNT;
   END;

   CREATE OR REPLACE FUNCTION FN_DEPT_SUM_SALARY(
      P_DID HR.EMPLOYEES.DEPARTMENT_ID%TYPE)
      RETURN NUMBER
   IS
	  V_SUM NUMBER:=0;
   BEGIN
    SELECT SUM(A.SALARY) INTO V_SUM
	  FROM HR.EMPLOYEES A
	 WHERE DEPARTMENT_ID=P_DID;
	RETURN V_SUM;
   END;


   CREATE OR REPLACE FUNCTION FN_DEPT_AVG_SALARY(
      P_DID HR.EMPLOYEES.DEPARTMENT_ID%TYPE)
      RETURN NUMBER
   IS
	  V_AVG NUMBER:=0;
   BEGIN
    SELECT ROUND(AVG(A.SALARY)) INTO V_AVG
      FROM HR.EMPLOYEES A
     WHERE DEPARTMENT_ID=P_DID;
    RETURN V_AVG;
   END;
   
(실행)

    SELECT DISTINCT A.DEPARTMENT_ID AS 부서번호,
           B.DEPARTMENT_NAME AS 부서명,
           A.EMP_NAME AS 책임사원명,
		   FN_DEPT_EMP_CNT(A.DEPARTMENT_ID) AS 사원수,
		   FN_DEPT_SUM_SALARY(A.DEPARTMENT_ID) AS 급여합계,
		   FN_DEPT_AVG_SALARY(A.DEPARTMENT_ID) AS 평균급여
      FROM HR.EMPLOYEES A, HR.DEPARTMENTS B,HR.EMPLOYEES C
     WHERE A.DEPARTMENT_ID=B.DEPARTMENT_ID
	   AND A.EMPLOYEE_ID=C.MANAGER_ID
     ORDER BY 1;

 

 

 

 

-- 200행을 PL/SQL을 사용하여 INSERT해보자
INSERT INTO BOOK(BOOK_ID, TITLE, CATEGORY, PRICE, INSERT_DATE, CONTENT)
VALUE(35,'','',12000,SYSDATE,'내용')

DECLARE -- 선택사항
    BOOK_ID NUMBER;
BEGIN
    -- 가장 마지막 번호 + 1
    SELECT NVL(MAX(BOOK_ID),0)+1 INTO BOOK_ID
    FROM BOOK;
    
    FOR I IN BOOK_ID..BOOK_ID+200 LOOP
    INSERT INTO BOOK(BOOK_ID, TITLE, CATEGORY, PRICE, INSERT_DATE, CONTENT)
    VALUES(I, '핑구와 핑핑이'||I , '동화', 12000,SYSDATE, '책 내용'||I);
    END LOOP;
    COMMIT;
 END;