Oracle

[211006] PROCEDURE 연습문제

감자탈출기 2021. 10. 8. 02:12

PROCEDURE


PL/SQL문을 저장하는 객체

필요할 때마다 복잡한 구문을 다시 입력할 필요 없이 간단하게 호출해서 실행 결과를 얻을 수 있음

 

/*
    1. 재고관리를 위한 테이블 생성
        테이블 명 : PRODUCTS
        컬럼명  데이터타입      제약조건        비고
           PID  VARCHAR2(6)     Primary Key     문자와 숫자의 조합으로 만들어진 코드를 사용한다.
     PNAME  VARCHAR2(30)    NOT NULL        제품 이름
    AMOUNT  NUMBER          NOT NULL        제품의 단가
           CNT  NUMBER          NOT NULL        제품의 수량

        테이블 명 : PROD_INOUT
        컬럼명  데이터타입      제약조건        비고
            ID  NUMBER          Primary Key     제품 입고 출고내역을 식별하기 위한 번호, 자동증가
                                                기능을 활용한다.
        IN_OUT  CHAR(1)         CHECK           I:입고, O:출고 임을 구분하는 컬럼
          PID  VARCHAR2(6)     Foreign Key     PRODUCTS 테이블을 참조하는 외래키로 입출고된 제품을
                                                구분한다.
    AMOUNT  NUMBER          NOT NULL        입출고 시점의 제품 단가
           CNT  NUMBER          NOT NULL        입출고된 제품의 수량, DEFAULT(1)
         PRICE  NUMBER          NOT NULL        입출고 시점의 제품 전체 금액(단가 x 수량)
                                                입고 제품에는 10% 할인된 금액으로 기록되어야 한다.
                                                출고 제품에는 10% 부가세가 포함된 금액으로 기록되어야
                                                한다. 소수점은 절삭한다.
    INOUT_DATE  DATE                            입출고 날짜, DEFAULT(SYSDATE)
*/
SELECT * FROM PRODUCTS;
SELECT * FROM PROD_INOUT;

DROP TABLE PROD_INOUT;
DROP TABLE PRODUCTS CASCADE CONSTRAINTS;
;
CREATE TABLE PRODUCTS (
          PID     VARCHAR2(6)     PRIMARY KEY
        , PNAME   VARCHAR2(30)    NOT NULL
        , AMOUNT  NUMBER          NOT NULL
        , CNT     NUMBER          NOT NULL
);
COMMENT ON COLUMN PRODUCTS.PID IS '문자와 숫자의 조합으로 만들어진 코드를 사용한다.';
COMMENT ON COLUMN PRODUCTS.PNAME IS '제품 이름';
COMMENT ON COLUMN PRODUCTS.AMOUNT IS '제품의 단가';
COMMENT ON COLUMN PRODUCTS.AMOUNT IS '제품의 수량';

CREATE TABLE PROD_INOUT (
          ID            NUMBER          PRIMARY KEY
        , IN_OUT        CHAR(1)         CHECK(IN_OUT IN('O','I'))
        , PID           VARCHAR2(6)     REFERENCES PRODUCTS(PID)
        , AMOUNT        NUMBER          NOT NULL
        , CNT           NUMBER          NOT NULL  --디폴트 (아래에서 따로 넣어 주었지만 컬럼레벨도 가능하다.)
        , PRICE         NUMBER          NOT NULL
        , INOUT_DATE    DATE   --디폴트
);
COMMENT ON COLUMN PROD_INOUT.ID IS '제품 입고 출고내역을 식별하기 위한 번호, 자동증가 기능을 활용한다.';
COMMENT ON COLUMN PROD_INOUT.IN_OUT IS 'I:입고, O:출고 임을 구분하는 컬럼';
COMMENT ON COLUMN PROD_INOUT.PID IS 'PRODUCTS 테이블을 참조하는 외래키로 입출고된 제품을
                                                구분한다.';
COMMENT ON COLUMN PROD_INOUT.AMOUNT IS '입출고 시점의 제품 단가';
COMMENT ON COLUMN PROD_INOUT.CNT IS '입출고된 제품의 수량'
COMMENT ON COLUMN PROD_INOUT.PRICE IS '입출고 시점의 제품 전체 금액(단가 x 수량)
                                       입고 제품에는 10% 할인된 금액으로 기록되어야 한다.
                                       출고 제품에는 10% 부가세가 포함된 금액으로 기록되어야한다.
                                       소수점은 절삭한다.';
COMMENT ON COLUMN PROD_INOUT.INOUT_DATE IS '입출고 날짜';

ALTER TABLE PROD_INOUT MODIFY CNT DEFAULT(1);
ALTER TABLE PROD_INOUT MODIFY INOUT_DATE DEFAULT(SYSDATE);

/*
    2. 1번에서 만든 테이블의 기본 데이터를 추가한다.
        - PRODUCTS 테이블에는 다음의 데이터를 추가한다.
            PID:A00001  PNAME:투명박스       AMOUNT:15,000   CNT:10
            PID:A00002  PNAME:텀블러         AMOUNT:10,000   CNT:15
            PID:A00003  PNAME:마스크(10개입) AMOUNT:5,000    CNT:20
            PID:A00004  PNAME:투명테이프     AMOUNT:1,000    CNT:30
            PID:A00005  PNAME:종이컵(355ml)  AMOUNT:2,000    CNT:40

        - PROD_INOUT과 PRODUCTS 테이블에는 다음 설명에 맞게 데이터가 추가 및 변경될 수 있게 한다.
            투명박스 2개를 2021년 01월 02일에 출고하였다.
            텀블러 1개를 2021년 01월 03일에 출고하였다.
            마스크 10개를 2021년 01월 05일에 출고하였다.
            종이컵 5개를 2021년 01월 12일에 출고하였다.
            마스크 20개를 2021년 01월 15일에 입고하였다.
            투명테이프 2개를 2021년 01월 17일에 출고하였다.
            종이컵 10개를 2021년 01월 21일에 출고하였다.
            투명박스 7개를 2021년 01월 25일에 입고하였다.
*/
-- PRODUCTS 테이블에는 다음의 데이터를 추가한다.
INSERT INTO PRODUCTS VALUES('A00001''투명박스'1500010);
INSERT INTO PRODUCTS VALUES('A00002''텀블러'1000015);
INSERT INTO PRODUCTS VALUES('A00003''마스크(10개입)'500020);
INSERT INTO PRODUCTS VALUES('A00004''투명테이프'100030);
INSERT INTO PRODUCTS VALUES('A00005''종이컵(355ml)'200040);

SELECT*FROM PRODUCTS--참조되는자식테이블 (PID, PNAME, AMOUNT, CNT)
SELECT*FROM PROD_INOUT--(ID, IN_OUT, PID, AMOUNT, CNT, PRICE, INOUT_DATE)
DESC PROD_INOUT;
DROP SEQUENCE PROD_INOUT_SEQ;
CREATE SEQUENCE PROD_INOUT_SEQ;

INSERT INTO PROD_INOUT VALUES (PROD_INOUT_SEQ.NEXTVAL'O''A00001'1500020TO_DATE('20210102''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES (PROD_INOUT_SEQ.NEXTVAL'O''A00002'1000010TO_DATE('20210103''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES (PROD_INOUT_SEQ.NEXTVAL'O''A00003'5000100TO_DATE('20210105''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES (PROD_INOUT_SEQ.NEXTVAL'O''A00005'200050TO_DATE('20210112''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES (PROD_INOUT_SEQ.NEXTVAL'I''A00003'5000200TO_DATE('20210115''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES (PROD_INOUT_SEQ.NEXTVAL'O''A00004'100020TO_DATE('20210117''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES (PROD_INOUT_SEQ.NEXTVAL'O''A00005'2000100TO_DATE('20210121''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES (PROD_INOUT_SEQ.NEXTVAL'I''A00001'1500070TO_DATE('20210125''yyyymmdd'));

-- PRICE 계산식에 부가세 작업까지 한번에 (사용자 정의 함수 sample5 line 51)
UPDATE PROD_INOUT
   SET PRICE = AMOUNT * CNT * 1.1
WHERE IN_OUT = 'O';

UPDATE PROD_INOUT
   SET PRICE = AMOUNT * CNT * 0.9
WHERE IN_OUT = 'I';

/*
    3. 1월 1달간 출고 내역을 조회하시오.
        제품코드    제품명      단가        수량        금액
        A00001      투명박스    15,000      2           33,000
        A00002      텀블러      10,000      1           11,000
        A00003      마스크      5,000       10          55,000
        ....
*/
SELECT*FROM PROD_INOUT--(ID, IN_OUT, PID, AMOUNT, CNT, PRICE, INOUT_DATE)
SELECT*FROM PRODUCTS;

SELECT A.PID AS 제품코드
     , B.PNAME AS 제품명
     , TO_CHAR(MAX(A.AMOUNT), '999,999,999'AS 단가
     , TO_CHAR(SUM(A.CNT), '999,999,999'AS 수량
     , TO_CHAR(SUM(A.PRICE), '999,999,999'AS 금액
FROM PROD_INOUT A JOIN PRODUCTS B
  ON A.PID = B.PID
WHERE A.IN_OUT = 'O'
  AND INOUT_DATE BETWEEN TO_DATE('20210101''yyyymmdd')
                     AND TO_DATE('20210131''yyyymmdd')
GROUP BY A.PIDB.PNAME;

/*
    4. 1월 1달간의 입/출고 내역을 다음과 같이 나오도록 조회하시오.
        제품코드    제품명      입고량      출고량
        A00001      투명박스    7           2
        A00002      텀블러      0           1
        A00003      마스크      20          10
        ...
        총합                    xx          xx
*/
SELECT*FROM PROD_INOUT--(ID, IN_OUT, PID, AMOUNT, CNT, PRICE, INOUT_DATE)
SELECT*FROM PRODUCTS;

-- UNION 안 쓰는 방법
SELECT  DECODE(A.PIDNULL'총합'A.PIDAS 제품코드  --와..
      , B.PNAME AS 제품명
      , SUM(DECODE(A.IN_OUT'I'A.CNT0)) AS 입고량
      , SUM(DECODE(A.IN_OUT'O'A.CNT0)) AS 출고량
  FROM PROD_INOUT A JOIN PRODUCTS B
    ON A.PID = B.PID
 WHERE INOUT_DATE BETWEEN TO_DATE('20210101''yyyymmdd')
                       AND TO_DATE('20210131''yyyymmdd')
 GROUP BY ROLLUP((A.PIDB.PNAME)); -- 묶음을 하나로 보기 때문에 묶음별 값이 하나랑, 전체 계 한개 총 2개 기준으로  GROUP BY 됨
                                    -- 전체 계는 집계값 제외한 나머지 열 NULL로 나오기 때문에 DECODE 써서 '총합' 넣을 수 있는 것
-- DECODE(A.IN_OUT, 'I', A.CNT, 0)
-- A.IN_OUT이 'I'면 A.CNT 값을 반환, 아니면 0 반환.
-- A.IN_OUT 값을 A.CNT로 변경하는 게 아닌 단순히 해당 값으로 SELECT 컬럼을 반환하는 것)

-- UNION
SELECT A.PID AS 제품코드
     , B.PNAME AS 제품명
     , SUM(DECODE(A.IN_OUT'I'A.CNT0)) AS 입고량
     , SUM(DECODE(A.IN_OUT'O'A.CNT0)) AS 출고량
FROM PROD_INOUT A JOIN PRODUCTS B 
  ON A.PID = B.PID
WHERE INOUT_DATE BETWEEN TO_DATE('20210101''yyyymmdd')
                     AND TO_DATE('20210131''yyyymmdd')
GROUP BY A.PIDB.PNAME
UNION -- 행결합
SELECT '총합' AS 제품코드
      , NULL AS 제품명
      , SUM(DECODE(A.IN_OUT'I'A.CNT0)) AS 입고량
      , SUM(DECODE(A.IN_OUT'O'A.CNT0)) AS 출고량
  FROM PROD_INOUT A JOIN PRODUCTS B
    ON A.PID = B.PID
  WHERE INOUT_DATE BETWEEN TO_DATE('20210101''yyyymmdd'AND TO_DATE('20210131''yyyymmdd');
-- GROUP BY 하지 않고 다 더해야 총합. 행 한줄 따로 생성한 것

SELECT * FROM PROD_INOUT;

/*
    5. 3번, 4번 조회 쿼리 활용한 VIEW를 생성하여 년/월 조회 조건만으로도
       동일한 결과가 조회될 수 있도록 하시오.
*/
SELECT*FROM PROD_INOUT--(ID, IN_OUT, PID, AMOUNT, CNT, PRICE, INOUT_DATE)
SELECT*FROM PRODUCTS;
-- 3번(1월 1달간 출고 내역) 뷰
CREATE OR REPLACE VIEW V_출고내역
AS
   SELECT EXTRACT(YEAR FROM A.INOUT_DATEAS 년도
        , EXTRACT(MONTH FROM A.INOUT_DATEAS 월
        , A.PID AS 제품코드
        , B.PNAME AS 제품명
        , TO_CHAR(MAX(A.AMOUNT), '999,999,999'AS 단가
        , TO_CHAR(SUM(A.CNT), '999,999,999'AS 수량
        , TO_CHAR(SUM(A.PRICE), '999,999,999'AS 금액
     FROM PROD_INOUT A JOIN PRODUCTS B 
       ON A.PID = B.PID
     WHERE A.IN_OUT = 'O'          -- WHERE의 월 조건 필요 없음 삭제. 뷰 조회시 조건으로 넣을 것
     GROUP BY EXTRACT(YEAR FROM A.INOUT_DATE), EXTRACT(MONTH FROM A.INOUT_DATE), A.PIDB.PNAME
WITH READ ONLY;

SELECT 제품코드, 제품명, 단가, 수량, 금액
  FROM V_출고내역
 WHERE 년도 = 2021 AND 월 = 2;

-- 테스트를 위한 2월달 추가 데이터 입력
INSERT INTO PROD_INOUT VALUES(PROD_INOUT_SEQ.NEXTVAL'O''A00002'100003
     , 10000 * 3 * 1.1TO_DATE('20210205''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES(PROD_INOUT_SEQ.NEXTVAL'O''A00002'100002
     , 10000 * 2 * 1.1TO_DATE('20210209''yyyymmdd'));
INSERT INTO PROD_INOUT VALUES(PROD_INOUT_SEQ.NEXTVAL'O''A00003'50005
     , 5000 * 5 * 1.1TO_DATE('20210212''yyyymmdd'));

-- 4번(1월 1달간의 입/출고 내역) 뷰
SELECT*FROM PROD_INOUT--(ID, IN_OUT, PID, AMOUNT, CNT, PRICE, INOUT_DATE)
SELECT*FROM PRODUCTS;

CREATE OR REPLACE VIEW V_입출고내역(년도, 월, 제품코드, 제품명, 입고량, 출고량) -- 별칭 일괄 부여
AS-- 서브쿼리의 SELECT절에 함수 사용했을 경우 반드시 별칭 지정하여야 한다.
   SELECT EXTRACT(YEAR FROM A.INOUT_DATE--AS 년도
        , EXTRACT(MONTH FROM A.INOUT_DATE--AS 월
        , DECODE(A.PIDNULL'총합'A.PID--AS 제품코드
        , B.PNAME --AS 제품명
        , SUM(DECODE(A.IN_OUT'I'A.CNT0)) --AS 입고량
        , SUM(DECODE(A.IN_OUT'O'A.CNT0)) --AS 출고량
    FROM PROD_INOUT A JOIN PRODUCTS B
      ON A.PID = B.PID
    GROUP BY ROLLUP( (EXTRACT(YEAR FROM A.INOUT_DATE), EXTRACT(MONTH FROM A.INOUT_DATE), A.PIDB.PNAME) ); 
WITH READ ONLY-- 롤업 방식때문에 년월별 분류되는 총합 사라짐 (잘못된 구문)

SELECT 제품코드, 제품명, 입고량, 출고량
  FROM V_입출고내역
 WHERE 년도 = 2021 AND 월 = 1;

-- 최종 수정
CREATE OR REPLACE VIEW V_입출고내역
AS
   SELECT EXTRACT(YEAR FROM A.INOUT_DATEAS 년도
        , EXTRACT(MONTH FROM A.INOUT_DATEAS 월
        , DECODE(A.PIDNULL'총합'A.PIDAS 제품코드
        , B.PNAME AS 제품명
        , SUM(DECODE(A.IN_OUT'I'A.CNT0)) AS 입고량
        , SUM(DECODE(A.IN_OUT'O'A.CNT0)) AS 출고량
    FROM PROD_INOUT A JOIN PRODUCTS B
      ON A.PID = B.PID
    GROUP BY ROLLUP(
                        (EXTRACT(YEAR FROM A.INOUT_DATE), EXTRACT(MONTH FROM A.INOUT_DATE))
                      , (A.PIDB.PNAME)
                    ) 
WITH READ ONLY;

SELECT 제품코드, 제품명, 입고량, 출고량
  FROM V_입출고내역
 WHERE 년도 = 2021 AND 월 = 1;

 /*
  ROLLUP : 인자로 전달받은 그룹중 가장먼저 전달된 그룹을 기준으로 추가적 집계 결과 반환
  년월 기준으로 추가 집계결과 반환. (년+월)+(디코드+네임), (년+월), (전체 집계)반환
*/

-- DECODE(A.PID ~ 구문 그대로 그룹바이에 사용하면 안 되는 이유
/* 실행 순서
1. FROM 절      2. WHERE 절
3. GROUP BY 절  4. HAVING 절
5. SELECT 절    6. ORDER BY 절
*/
-- : GROUP BY 실행하면 컬럼의 이름이 DECODE(A.PID ~로 바뀐상태라서 SELECT 실행시 DECODE 할 A.PID 컬럼을 찾지 못하기 때문.
-- 이해를 위한 구문
CREATE OR REPLACE VIEW V_입출고내역
AS
   SELECT EXTRACT(YEAR FROM A.INOUT_DATEAS 년도
        , EXTRACT(MONTH FROM A.INOUT_DATEAS 월
        , DECODE(DECODE(A.PIDNULL'총합'A.PID), NULL'총합'DECODE(A.PIDNULL'총합'A.PID)) AS 제품코드
                 -- ㄴ 정상 작동하길 원한다면 이러한 구조가 되어야 함
        , B.PNAME AS 제품명
        , SUM(DECODE(A.IN_OUT'I'A.CNT0)) AS 입고량
        , SUM(DECODE(A.IN_OUT'O'A.CNT0)) AS 출고량
    FROM PROD_INOUT A JOIN PRODUCTS B
      ON A.PID = B.PID
    GROUP BY ROLLUP(
                        (EXTRACT(YEAR FROM A.INOUT_DATE), EXTRACT(MONTH FROM A.INOUT_DATE))
                      , (DECODE(A.PIDNULL'총합'A.PID), B.PNAME)
                    )      -- ㄴ 그대로 붙여넣기 했을 때
WITH READ ONLY;

SELECT 제품코드, 제품명, 입고량, 출고량
  FROM V_입출고내역
 WHERE 년도 = 2021 AND 월 = 1;

-- 뷰 안쪽 점검하기
SELECT EXTRACT(YEAR FROM A.INOUT_DATEAS 년도
     , EXTRACT(MONTH FROM A.INOUT_DATEAS 월
     , DECODE(A.PIDNULL'총합'A.PIDAS 제품코드
     , B.PNAME AS 제품명
     , SUM(DECODE(A.IN_OUT'I'A.CNT0)) AS 입고량
     , SUM(DECODE(A.IN_OUT'O'A.CNT0)) AS 출고량
 FROM PROD_INOUT A JOIN PRODUCTS B
     ON A.PID = B.PID
 GROUP BY ROLLUP((EXTRACT(YEAR FROM A.INOUT_DATE), EXTRACT(MONTH FROM A.INOUT_DATE), A.PIDB.PNAME)); 
-- ㄴ롤업때문에 년 월별 총합은 사라짐

-- 수정
SELECT EXTRACT(YEAR FROM A.INOUT_DATEAS 년도
     , EXTRACT(MONTH FROM A.INOUT_DATEAS 월
     , DECODE(A.PIDNULL'총합'A.PIDAS 제품코드
     , B.PNAME AS 제품명
     , SUM(DECODE(A.IN_OUT'I'A.CNT0)) AS 입고량
     , SUM(DECODE(A.IN_OUT'O'A.CNT0)) AS 출고량
 FROM PROD_INOUT A JOIN PRODUCTS B
     ON A.PID = B.PID
 GROUP BY ROLLUP(
                     (EXTRACT(YEAR FROM A.INOUT_DATE), EXTRACT(MONTH FROM A.INOUT_DATE))
                     , (A.PIDB.PNAME)
                 ); 

/*
    6. 제품이 출고 또는 입고가 될 때 PRODUCTS에 기록된 수량도 같이 반영될 수 있도록 프로시저를
       만들어 동작시키시오. 단, 출고작업 진행시 출고에 필요한 상품의 수량이 부족한 경우에는
       출고작업이 이루어지지 않도록 해야 한다.
*/

SET SERVEROUTPUT ON;
DROP SEQUENCE PROD_INOUT_SEQ;

-- 입고 예시
CREATE OR REPLACE PROCEDURE PROC_제품입고(i_name IN VARCHARi_cnt IN NUMBERi_date IN DATE)
IS
    p_id PRODUCTS.PID%TYPE;
    amount PRODUCTS.AMOUNT%TYPE;
BEGIN
    SELECT PIDAMOUNT INTO p_idamount FROM PRODUCTS WHERE PNAME = i_name;
    INSERT INTO PROD_INOUT VALUES(PROD_INOUT_SEQ.NEXTVAL'I'p_id
                                , amounti_cnt, (amount * i_cnt * 0.9), i_date);
    UPDATE PRODUCTS 
          SET CNT = CNT + i_cnt 
     WHERE PID = p_id;
    COMMIT;
    --ROLLBACK; -- 구문 검증용. 작동때마다 테이블 데이터 매번 바뀌면 헷갈리니까.
                -- 참고로 시퀀스는 롤백 안 됨 알아서 DROP 초기화할 것)
END;
/
EXEC PROC_제품입고('텀블러'5TO_DATE('20210219''yyyymmdd'));

-- 출고 예시
CREATE OR REPLACE PROCEDURE PROC_제품출고(i_name IN VARCHARi_cnt IN NUMBERi_date IN DATE)
IS
    p_id PRODUCTS.PID%TYPE;
    amount PRODUCTS.AMOUNT%TYPE;
    cnt PRODUCTS.AMOUNT%TYPE;
    not_enough_count EXCEPTION;
BEGIN
    SELECT PIDamount INTO p_idamount FROM PRODUCTS WHERE PNAME = i_name;
    INSERT INTO PROD_INOUT VALUES(PROD_INOUT_SEQ.NEXTVAL'O'p_id
                                , amounti_cntamount * i_cnt * 1.1i_date);
    UPDATE PRODUCTS 
       SET CNT = CNT - i_cnt 
     WHERE PID = p_id;

    SELECT CNT INTO cnt FROM PRODUCTS WHERE PNAME = i_name;

    IF cnt < 0 THEN -- 복습할 때 보니까 cnt < i_cnt 같음
        RAISE not_enough_count -- RAISE 예외 발생 시키기
       -- ROLLBACK; --예외처리로 아래에서 롤백하고 삭제 할 부분
    ELSE
        COMMIT;
    END IF;
EXCEPTION
     WHEN not_enough_count THEN
        DBMS.OUTPUT.PUT_LINE('제품 출고를 위한 수량이 부족합니다');
        ROLLBACK;
END;
/
EXEC PROC_제품출고('텀블러'11TO_DATE('20210220''yyyymmdd'));

SELECT * FROM PROD_INOUT;
SELECT * FROM PRODUCTS;