OutputStream 클래스는 추상 클래스로 모든 바이트 출력 스트림의 최상위 클래스이다.

 

import java.io.OutputStream;

public class Ex05_OutputStream {

	public static void main(String[] args) {
		int data = 97;
		byte [] b = {65, 66, 67, 68, 69, 70};

		try {
			OutputStream os = System.out;
				// OutputStream : 출력 byte 스트림
			
			os.write(data); // 하위 1byte 출력
			System.out.println();
			
			os.write(b); // byte 배열 출력. ABCDEF
			System.out.println();
			
			os.write(b, 2, 3); // CDE. 2번째 인덱스에서 3개 출력
			System.out.println();
			
			System.out.write(b, 0, b.length); // off 인덱스로부터 len 개를 출력 장치로
			System.out.println();
			
			os.close();
			
			System.out.println("end..."); // 출력 안됨
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

os.write(data); 에서 data는 int형(4바이트) 이므로 1바이트씩 읽고 넘겨준다. 하위 1byte를 출력하는데 숫자 97를 문자로 읽어들이고 그 바이트값을 넘기면 ASCII코드의 97를 출력하게 된다. (a)

write(b); 에서 byte 배열인 B를 출력하면 1byte의 문자를 읽어들여 ASCII 코드 값을 반환하므로 ABCDEF.

 

OutputStream의 객체인 os 를 닫으면 close(); 마지막 end...는 출력되지 않는다. close()하면 바이트 입력 스트림을 닫고 사용하고 있던 시스템 자원들을 모두 반환한다.


import java.io.OutputStream;

public class Ex06_OutputStream {

	public static void main(String[] args) {
		try {
			OutputStream os = System.out;
			
			os.write(65); // 하위 1byte출력
						// 출력 버퍼에 출력되며 출력 버퍼가 차지 않으면 
						// 실제 출력 장치로 보내지 않음
			os.write(67);
			os.write(68);
			
			// 위내용 출력 방법
			// 1) 첫번째 방법
			os.flush(); // 출력 버퍼의 내용을 출력 장치로 보냄
			
			// 2) 두번째 방법
			// os.write(13);
			// os.write(10); // 이것만 해도 가능
			
			// 3) 세번째 방법
			// System.out.println(); // 위의 내용이 출력됨
			
			// 4) 네번째 방법
			// os.close();
			
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

write(); 에 넣어준 문자는 바로 출력되지 않고 출력 버퍼에 저장되었다가 출력 버퍼가 차면 출력 장치로 보내준다.

따라서 출력 버퍼의 내용을 출력 장치로 보내는 flush() 메소드를 통해 모아뒀다가 출력할 수 있다.

 

두 번째 방법은 엔터(ASCII로 2byte)의 ASCII 값을 직접 입력 한 것이고, 세 번째 방법도 2번째와 비슷한 방식이다. 네 번째 방법은 바이트 입력 스트림을 닫고 자원을 반환하며 출력된다.

바이트 스트림 Byte Streams ?

- 스트림의 기본 입출력 단위는 바이트(Byte) 이다.

- 1바이트 (8bit) 단위로 입출력을 수행한다.

- 2진 데이터의 입출력이 가능하다.

- 모든 바이트 스트림 클래스는 InputStream 및 OutputSream클래스의 서브 클래스이다.

 

public class Ex01_InputStream {
	public static void main(String[] args) {
		int data; // 4byte 자료형
		char ch; // 2byte 자료형
		
		try {
			System.out.println("문자열 입력[ctrl+z:종료] ? ");
			while ((data = System.in.read()) != -1) {
            	// System.in.read() 로는 1byte만을 읽는다.
            
				// System.out.println(data); // 입력받은 ASCII코드 값 출력
				// System.out.write(data);
					// 4byte(data) 중 하위 1byte만 출력하며 한글도 잘 출력된다.

				ch = (char)data;
				// System.out.print(ch);
					// 한글은 깨져나옴. 2byte문자를 1byte만 읽어서 2byte(ch의 자료형)로 출력하므로
				
				System.out.write(ch);
					// 한글 안깨짐. 2byte중 하위 1byte만 출력
                    // 즉, 1바이트 읽음 -> 읽은 1바이트를 2바이트로 바꿈 -> 1바이트로 읽음
                    // 따라서 안깨짐
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}
}

System.in 은 InputStream의 객체로 byte 스트림이다.

InputStream의 read() 메소드 : 1byte문자를 읽어들여 ASCII코드값을 반환한다.

키보드의 Enter 키는 ASCII 코드의 13(Carriage Return)과 10(Line Feed)를 의미한다. ( 앞으로 와서 줄을 바꿈)

 

자바에서 한글은 한글자에 2byte이다.

 

UTF-16 순수차바 처리 방식으로 한글 2byte, 영어 2byte로 처리한다.

UTF-8 은 오라클 처리 방식으로 대부분의 웹, 리눅스 기본세팅, 맥 등에서 사용된다. 한글 3byte, 영어 1byte로 처리한다.


입력받은 수 까지의 합을 구하는 프로그램

13은 엔터를 의미함.

public class Ex02_InputStream {

	public static void main(String[] args) {
		int n, data;
		n = 0;
		// 12엔터 => 49 50 13 10
		try {
			System.out.print("수 ? ");
			while ((data = System.in.read()) != 13) {
				data = data - 48;
				n = n * 10 + data;
			}

			int s = 0;
			for (int i = 1; i <= n; i++) {
				s += i;
			}
			System.out.println("결과:"+s);

		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

read() 는 키보드로 입력받은 문자를 ASCII 코드 값으로 반환해줌. 따라서 내가 입력한 것이 숫자여도 처리될 때는 숫자를 입력받은 것이 아닌 문자를 입력받았다고 처리된다. 그래서 문자 1의 ASCII값은 49이므로 48을 빼준 것.


public class Ex03_InputStream {

	public static void main(String[] args) {
		int data;
		
		try {
			System.out.println("ABCDEF 입력 후 엔터");
			data = System.in.read(); // 65
			System.out.write(data); // A
			 // write() 1byte를 보냄. 출력버퍼로 데이터를 보내서 출력.
			System.in.skip(2); // 읽어서 버림. BC
			
			data = System.in.read(); // 68
			System.out.write(data); // D
			
			System.in.skip(4); // EF엔터 버림
			
			System.out.flush(); // 출력 버퍼의 내용을 출력 장치로 보내줌.
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

read() 메소드에서 문자를 입력 받아 ASCII 코드 값으로 바꿔준다. (1바이트) 그 읽은 값들을 write() 메소드가 받아서 출력버퍼로 보내주면 후에 버퍼의 내용이 출력 장치로 가게됬을 때 표시된다.

data 에는 ABCDEF 이 스트림이 순서대로 넣어진다.

write() 메소드도 1byte를 읽는 메소드이기 때문에 먼저 읽은 1바이트 65가 출력 버퍼로 보내지고, 2바이트에 해당하는 BC는 skip() 메소드가 읽어서 버리고, 다시 read() 메소드가 1바이트를 읽어 아스키코드 값을 data에 넘겨주고 write()메소드가 1바이트를 읽어 D를 출력한다. EF와 엔터(2byte)는 skip() 메소드에 의해 읽어서 버려진다.

마지막에 flush() 메소드를 통해 출력 버퍼의 내용이 출력 장치로 보내지고, 화면에 보이게 된다. 


1. 증가 2. 감소 3. 종료

선택 =>

public class Ex04_InputStream {

	public static void main(String[] args) {
		boolean run = true;
		int speed = 0;
		char key;
		
		try {
			while(run) {
				do {
					System.out.println("1.증가 2.감소 3.종료");
					System.out.print("선택=>");
					key = (char)System.in.read();
					System.in.skip(2); // 엔터 버리기
				} while(key <'1' || key>'3');
				
				switch(key) {
				case '1':speed++;break;
				case '2':speed--;break;
				case '3':run=false;break;
				}
				System.out.println("speed:"+speed+"\n");
			}
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

}

read() 메소드가 1byte를 읽으면 ASCII값으로 49을 반환하는데 이것을 2byte로 char형인 key에 형변환해서 저장함. char형에서 49는 문자 '1'과 같은 의미이므로 switch(key) 에 '1' 이면 speed++; 해주고 '2'면 speed--; 해주고 '3'이면 종료한다.

 

근데 여기서 123 입력하면 23이 skip 되는거고 엔터는 아직 안읽었기 때문에 speed를 출력 후에 저렇게 문자가 출력된다. 

여기서는 23이 skip되고 3은 남아있었기 때문에 다음에 3을 바로 읽고 종료가 된다.

 

-- 예외처리

DECLARE
    vName VARCHAR2(30);
    vSal NUMBER;
BEGIN
    SELECT name, sal INTO vName, vSal FROM emp WHERE empNo = '1001';
    -- SELECT name, sal INTO vName, vSal FROM emp WHERE empNo = '9001'; -- NO_DATE_FOUND
    -- SELECT name, sal INTO vName, vSal FROM emp; -- TOO_MANY_ROWS
    DBMS_OUTPUT.PUT_LINE(vName || ' : ' || vSal);
    
    EXCEPTION
        WHEN NO_DATA_FOUND THEN 
            DBMS_OUTPUT.PUT_LINE('등록된 자료가 없습니다.');
        WHEN TOO_MANY_ROWS THEN 
            DBMS_OUTPUT.PUT_LINE('두 개 이상의 레코드가 존재합니다.');
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('기타 에러가 발생 했습니다.');
END;
/

 

-- 사용자 정의 예외

DECLARE
    vName VARCHAR2(30);
    vSal NUMBER;
    
    sal_check EXCEPTION; -- 사용자 정의 예외 선언
BEGIN
    SELECT name, sal INTO vName, vSal FROM emp WHERE empNo = '1001';
    IF vSal >= 3000000 THEN
        RAISE sal_check; -- 예외를 발생시킴
    END IF;
    DBMS_OUTPUT.PUT_LINE(vName || ' : ' || vSal);
    
    EXCEPTION
        WHEN NO_DATA_FOUND THEN 
            DBMS_OUTPUT.PUT_LINE('등록된 자료가 없습니다.');
        WHEN TOO_MANY_ROWS THEN 
            DBMS_OUTPUT.PUT_LINE('두 개 이상의 레코드가 존재합니다.');
        WHEN sal_check THEN
            DBMS_OUTPUT.PUT_LINE('급여가 300 이상입니다.');
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE('기타 에러가 발생 했습니다.');
END;
/

 

또, 여태까지 썼던

2021.08.20 - [쌍용강북교육센터/8월] - 0819_Oracle[PL/SQL] : PROCEDURE, FUNCTION 예제

 

IF 조건

RAISE_APPLICATION_ERROR(-20001, '여기에 메세지 입력');

END IF;

방법도 있었다.

 

이때 안에있는 번호는 20000 부터 20999까지 가능하다.

커서 CURSOR ?

 

- 하나의 레코드가 아닌 여러 레코드로 구성된 작업영역에서 SQL문을 실행하고 그 과정에 생긴 정보를 저장하기 위해서 CURSOR를 사용한다.

- 오라클 서버에 의해 실행되는 모든 SQL문은 연관된 각각의 커서를 소유하고 있다.

 

주기억장치에 저장된 공간이 커서이고, 그 커서에서 하나씩 꺼내오는 것이 FETCH이다.

 

-- 명시적 커서

DECLARE
    vname emp.name%TYPE;
    vsal emp.sal%TYPE;
    
    -- 1. 커서 선언
    CURSOR cur_emp IS SELECT name, sal FROM emp;
BEGIN
    -- 2. 커서 OPEN
    OPEN cur_emp;
    
    LOOP
        -- 3. FETCH - 하나 레코드를 가져옴
        FETCH cur_emp INTO vname, vsal;
        EXIT WHEN cur_emp%NOTFOUND; -- 이거 없으면 에러. 
        -- 가져올 자료가 없으면 LOOP를 빠져 나가라.
        
        DBMS_OUTPUT.PUT_LINE(vname || ' : ' || vsal);
    END LOOP;
    -- 커서 CLOSE
    CLOSE cur_emp;
END;
/

 

-- 암시적 커서

DECLARE
    vempNo emp.empNo%TYPE;
    vName emp.name%TYPE;
    vSal emp.sal%TYPE;
    vCount NUMBER;
BEGIN
    vempNo := '1001';
    SELECT name, sal INTO vname, vsal FROM emp WHERE empNo = vempNo;
        -- 레코드가 없거나 2개 이상 존재하면 오류
    vCount := SQL%ROWCOUNT; -- 해당 SQL 문에 영향을 받은 행의 수 
    
    DBMS_OUTPUT.PUT_LINE(vCount || ' : ' || '개 존재');
END;
/

 

-- CURSOR FOR LOOP : 자동 OPNE, 자동 FETCH, 자동 CLOSE

DECLARE 
    CURSOR cur_emp IS SELECT name, sal FROM emp;
BEGIN
    FOR rec IN cur_emp LOOP 
        DBMS_OUTPUT.PUT_LINE(rec.name || ' : ' || rec.sal);
    END LOOP;
END;
/

 

커서 변수 ( CURSOR VARIABLE) 

- 명시적 커서와 암시적 커서는 정적이므로, 커서가 만들어지는 시점에서 쿼리문이 정의된다. 따라서 런타임까지는 커서에 이용할 쿼리를 정의할 수 없기 때문에 REF CURSOR 와 커서변수를 제공하여, 런타임 시에 커서의 쿼리를 정의하고 결과 셋을 처리할 수 있도록 하고 있다.

- 커서 타입의 변수는 프로시저 등의 매개 변수로 사용할 수 있다.

- REF CURSOR 커서 타입 

  강한 커서 타입 : 쿼리의 결과 셋을 구성할 리턴 타입을 %ROWTYPE 속성으로 정의한다.

  약한 커서 타입 : 리턴 타입 정의를 포함하지 않고, 어떤 결과 셋(set)을 얻는 경우에도 이용할 수 있다.

- REF CURSOR type 정의

  형식

   TYPE type_name IS REF CURSOR [ RETRUN return_type ]

  커서 타입 정의 예

   TYPE refcur IS REF CURSOR ; -- 약한 커서 타입

   TYPE refcur_emp IS REF CURSOR RETURN emp%ROWTYPE ; -- 강한 커서 타입

  커서 변수 사용

  1) 커서 변수 선언 : 커서변수 커서타입명;

  2) 커서 변수 사용 : OPEN 커서변수 FOR SELECT 문 ;

 

-- SYS_REFCURSOR 예

CREATE OR REPLACE PROCEDURE pEmpSelectList
(
    pResult OUT SYS_REFCURSOR 
)
IS
BEGIN
    OPEN pResult FOR
        SELECT name, sal, bonus FROM emp;
END;
/

 

-- 확인용 프로시저

CREATE OR REPLACE PROCEDURE pEmpSelectResult
IS
    vName emp.name%TYPE;
    vSal emp.sal%TYPE;
    vBonus emp.bonus%TYPE;
    vResult SYS_REFCURSOR;
BEGIN
    pEmpSelectList( vResult );
    LOOP 
        FETCH vResult INTO vName, vSal, vBonus;
        EXIT WHEN vResult%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(vName || ' : ' || vSal || ' : ' || vBonus);
    END LOOP;
END;
/

 

-- 결과 확인

EXEC pEmpSelectResult;

 

'쌍용강북교육센터 > 8월' 카테고리의 다른 글

0823_Java : InputStream 클래스  (0) 2021.08.23
0820_Oracle[PL/SQL] : 예외처리  (0) 2021.08.23
0820_Oracle[PL/SQL] : TRIGGER 트리거  (0) 2021.08.23
0820_Oracle[PL/SQL] : PACKAGE 패키지  (0) 2021.08.23
0820_Oracle : INDEX  (0) 2021.08.23

프로시저 -> 자주 사용되는 쿼리 저장 -> 유저가 필요하면 실행

트리거 -> 쿼리 저장 -> 자동으로 실행 (서버에 부하를 줄 수 있다.)

 

트리거 ? DML 이나 DDL 명령이 실행되면 자동으로 실행되도록 정의한 동작

트리거는 CREATE TRIGGER 권한이 있어야 만들 수 있다.

관리자(sys)에서 GRANT CREATE TRIGGER TO 계정; 을 통해 권한을 줄 수 있다.

 

문장 트리거 : DML 작업이 여러번 일어나도 트리거는 한번만 실행. 일반적으로 로그 등을 출력.

 

실습을 위한 테이블을 작성해봅시다.

CREATE TABLE insa (
    num VARCHAR2(30) PRIMARY KEY,
    name VARCHAR2(50) NOT NULL,
    dept VARCHAR2(50) NOT NULL,
    sal NUMBER NOT NULL
);

CREATE TABLE insa_time (
    memo VARCHAR2(1000),
    reg_date DATE DEFAULT SYSDATE
);

SELECT* FROM tab; -- 테이블 생성 확인

SELECT * FROM user_sys_privs; -- 권한 확인

 

-- 트리거에서 DML 문은 자동으로 COMMT/ROLLBACK되므로 COMMIT이나 ROLLBACK을 하지 않습니다.

CREATE OR REPLACE TRIGGER tri_insa
AFTER INSERT OR UPDATE OR DELETE ON insa
BEGIN
    IF INSERTING THEN
        INSERT INTO insa_time(memo) VALUES('추가 ...');
    ELSIF UPDATING THEN
        INSERT INTO insa_time(memo) VALUES('수정 ...');
    ELSIF DELETING THEN
        INSERT INTO insa_time(memo) VALUES('삭제 ...');
    END IF;
END;
/

 

-- 트리거 목록 확인

SELECT * FROM user_triggers;
SELECT * FROM user_source;

 

INSERT INTO insa VALUES ('11', '김자바', '개발부', 100);
INSERT INTO insa VALUES ('22', '너자바', '개발부', 100);
INSERT INTO insa VALUES ('33', '다자바', '개발부', 100);
COMMIT;

위 데이터를 추가하면 insa_time에 있는 memo컬럼에 데이터 추가의 시간과 추가... 라는 메모가 같이 적혀져있는 것을 확인할 수 있습니다.

SELECT memo, TO_CHAR(reg_date, 'YYYY-MM-DD HH24:MI:SS') reg_date FROM insa_time;

위 쿼리를 이용하면 insa_time에 작성된 memo의 날짜뿐만 아닌 시, 분, 초를 확인할 수 있습니다.

 

 

-- 지정된 시간에만 DML이 이루어 지도록 문장 트리거 만들기

CREATE OR REPLACE TRIGGER tri_insa
AFTER INSERT OR UPDATE OR DELETE ON insa
BEGIN
    IF TO_CHAR(SYSDATE, 'D') IN (1, 7) OR -- 일요일이거나 토요일이면
        ( TO_CHAR(SYSDATE, 'HH24') >= 20 AND TO_CHAR(SYSDATE, 'HH24') <= 8 ) THEN
            RAISE_APPLICATION_ERROR(-20001, '지금은 근무 시간이 아닙니다.');
    END IF;
END;
/

일요일이거나 토요일이면 예외처리를 통해 '지금은 근무 시간이 아닙니다.'를 출력하는 트리거입니다. 또 안에 적혀져 있는 시간을 통해 오후 8시 부터 오전 8시 까지를 예외처리를 했습니다.

 

-- 트리거 지우기

DROP TRIGGER tri_insa;

DROP TRIGGER 트리거명; 을 통해 삭제할 수 있습니다.

 

-- 행 트리거

DML 문장이 일어난 횟수만큼 트리거가 실행됩니다. 예를 들어 DELETE FROM insa;에서 30개 레코드가 삭제되면 트리거도 30번 실행됩니다.

 

새로운 실습을 위해 테이블을 생성합니다.

 	CREATE TABLE score1 (
          hak VARCHAR2(20) NOT NULL
          ,name VARCHAR2(30) NOT NULL
          ,kor NUMBER(3) NOT NULL
          ,eng NUMBER(3) NOT NULL
          ,mat NUMBER(3) NOT NULL
          ,CONSTRAINT pk_score1_hak PRIMARY KEY(hak)
      );

      CREATE TABLE score2 (
          hak VARCHAR2(20) NOT NULL
          ,kor NUMBER(2,1) NOT NULL
          ,eng NUMBER(2,1) NOT NULL
          ,mat NUMBER(2,1) NOT NULL
          ,CONSTRAINT pk_score2_id PRIMARY KEY(hak)
          ,CONSTRAINT fk_score2_id FOREIGN KEY(hak)
              REFERENCES score1(hak)
      );

      CREATE OR REPLACE FUNCTION fnGrade
      (
          pScore NUMBER
      )
      RETURN NUMBER
      IS
         n NUMBER(2,1);
      BEGIN
         IF pScore>=95 THEN n:=4.5;
         ELSIF pScore>=90 THEN n:=4.0;
         ELSIF pScore>=85 THEN n:=3.5;
         ELSIF pScore>=80 THEN n:=3.0;
         ELSIF pScore>=75 THEN n:=2.5;
         ELSIF pScore>=70 THEN n:=2.0;
         ELSIF pScore>=65 THEN n:=1.5;
         ELSIF pScore>=60 THEN n:=1.0;
         ELSE n:=0.0;
         END IF;
         RETURN n;
      END;
 	     /

 

-- 행트리거 : INSERT

CREATE OR REPLACE TRIGGER triScoreInsert
AFTER INSERT ON score1
FOR EACH ROW -- 행 트리거
DECLARE
-- 변수선언 영역
BEGIN
    -- :NEW -> score1에서 INSERT 한 행. 행트리거만 사용 가능
    INSERT INTO score2(hak, kor, eng, mat) VALUES (:NEW.hak,
        fnGrade(:NEW.kor), fnGrade(:NEW.eng), fnGrade(:NEW.mat) );
END;
/

score1 테이블에 hak, name, kor, eng, mat을 입력하면 자동으로 score2테이블 hak에는 입력받은 값을 입력해주고 kor, eng, mat에는 평점을 계산하여 넣어주는 트리거가 작성되었습니다.

INSERT INTO score1(hak, name, kor, eng, mat) VALUES ('1', 'a', 95, 100, 80);
INSERT INTO score1(hak, name, kor, eng, mat) VALUES ('2', 'b', 70, 90, 100);
COMMIT;

SELECT * FROM score1;
SELECT * FROM score2;

 

-- 행트리거 : UPDATE

CREATE OR REPLACE TRIGGER triScoreUpdate
AFTER UPDATE ON score1
FOR EACH ROW -- 행 트리거
DECLARE
-- 변수선언 영역
BEGIN
    -- :NEW -> score1에 새로 UPDATE할 레코드
    -- :OLD -> score1에서 UPDATE 하기 전 레코드
    UPDATE score2 SET kor = fnGrade(:NEW.kor), eng = fnGrade(:NEW.eng), mat = fnGrade(:NEW.mat) 
    WHERE hak = :OLD.hak ;
END;
/

score1이 UPDATE로 데이터가 바뀌면 score2에 score1에서 값을 연산해서 넣어줍니다. 

OLD.hak 은 바뀌기 전의 hak 을 말합니다. 

 

UPDATE score1 SET kor = 100, eng = 100, mat = 100 WHERE hak = '1';
COMMIT;
SELECT * FROM score1;
SELECT * FROM score2;

 

-- 행 트리거 : DELETE

CREATE OR REPLACE TRIGGER triScoreDELETE
BEFORE DELETE ON score1
FOR EACH ROW -- 행 트리거
DECLARE
-- 변수선언 영역
BEGIN
    -- :OLD -> score1에서 DELETE할 레코드
    DELETE FROM score2 WHERE hak = :OLD.hak ;
END;
/

여기서는 score1의 데이터가 삭제되기전에 score2에 입력한  hak에 해당되는 자료를 먼저 지워줍니다. score2 테이블에서 hak은 참조키 이므로, 부모테이블에서 해당 hak을 지우고 싶으면 자식을 먼저 지워야 되기때문에 자동으로 자식을 먼저 지워지게 한 것입니다.

DELETE FROM score1 WHERE hak = '1';
COMMIT;
SELECT * FROM score1;
SELECT * FROM score2;

-- 패키지 선언

CREATE OR REPLACE PACKAGE pEmp IS
    FUNCTION fnTax( p IN NUMBER ) RETURN NUMBER;
    PROCEDURE empList(pName VARCHAR2);
    PROCEDURE empList;   -- 중복 정의 (패키지 안에서는 가능)
END pEmp;
/

앞으로 만들 FUNCTION과 PROCEDURE을 선언할 수 있습니다.

 

-- 패키지 몸체 구현

CREATE OR REPLACE PACKAGE BODY pEmp IS
    -- 함수 구현
    FUNCTION fnTax( p IN NUMBER ) 
    RETURN NUMBER
    IS
        t NUMBER := 0;
    BEGIN
        IF p >= 3000000 THEN
            t := TRUNC(p * 0.03, -1);
        ELSIF p >= 2000000 THEN
            t := TRUNC(p * 0.02, -1);
        ELSE
            t := 0;
        END IF;
        
        RETURN t;
    END;
    
    PROCEDURE empList( pName VARCHAR2 )
    IS
        vName VARCHAR2(30);
        vSal NUMBER;
        CURSOR cur_emp IS
            SELECT name, sal FROM emp WHERE INSTR( name, pName) = 1;
        BEGIN
            OPEN cur_emp;
            LOOP
                FETCH cur_emp INTO vName, vSal;
                EXIT WHEN cur_emp%NOTFOUND;
                DBMS_OUTPUT.PUT_LINE(vName|| ' : ' || vSal);
            END LOOP;
            CLOSE cur_emp;
        END;
        
        PROCEDURE empList
        IS
        BEGIN
            FOR rec IN ( SELECT name, sal+bonus pay, fnTax(sal+bonus) tax FROM emp ) LOOP
                DBMS_OUTPUT.PUT_LINE( rec.name || ' : ' || rec.pay || ' : ' || rec.tax);
            END LOOP;
        END;
END pEmp;
/

 

-- 패키지 확인

SELECT * FROM user_objects;
SELECT * FROM user_objects WHERE object_type = 'PACKAGE';
SELECT * FROM user_procedures;

 

-- 실행

EXEC pEmp.empList;
EXEC pEmp.empList('김');

-- 패키지에 있는 함수를 SELECT 문에서도 사용할 수 있다.

SELECT name, sal, bonus, sal+bonus, pEmp.fnTax( sal+bonus) tax FROM emp;

 

-- 패키지 삭제

DROP PACKAGE 패키지명;
DROP PACKAGE BODY 패키지명;

선언부를 삭제하게 되면 몸체도 자동으로 삭제가 된다.

인덱스 (INDEX) ?

- 일반 테이블이나 클러스에서 쓰여지는 선택적인 객체로서, 오라클 데이터베이스 테이블내의 원하는 레코드를 빠르게 찾아갈 수 있도록 만들어진 데이터 구조이다.

- SQL 명령문의 처리 속도를 향상시키기 위해서 컬럼에 대해서 생성하는 오라클 객체이다.

- 인덱스는 디스크 I/O를 줄이는 많은 방법 중 하나이다.

 

-- 인덱스 확인

SELECT * FROM user_indexes;
SELECT * FROM user_indexes WHERE table_name = 'EMP';
SELECT * FROM user_ind_columns WHERE table_name = 'EMP';

PRIMARY KEY, UNIQUE 컬럼은 자동으로 인덱스가 만들어지며 인덱스 이름은 제약조건의이름이다.

 

-- B-Tree 인덱스 만들기

SELECT empNo, name, sal FROM emp WHERE name = '심심해';

실행하려는 SQL의 범위를 잡고 F10을 누르면 밑에 계획 설명을 보여준다. COST를 보면 SQL문을 실행해서 걸리는 여러 요인들을 계산해서 대략적인 수치를 보여준다. (원래는 3인데, 지금은 INDEX를 만들어 놓은 상태라서 2입니다.)

 

-- B-Tree 인덱스 작성

CREATE INDEX idx_emp_name ON emp ( name ); -- NON UNIQUE

이름은 중복의 값이 있을 수 있기 때문에 UNIQUE 값이 아닙니다.

CREATE INDEX 인덱스명 ON 테이블명 (컬럼) ;

 

여러 예들 ;

SELECT empNo, name, sal FROM emp WHERE SUBSTR(name, 1, 1) = '심';

SELECT empNo, name, sal FROM emp WHERE NOT name = '심심해';

위의 쿼리의 경우 인덱스를 사용하지 않습니다. 비교전에 SUBSTR를 통해 변형되기 때문입니다. 또 NOT을 사용한 문장에서도 인덱스가 사용되지 않습니다.

 

-- 인덱스 삭제

DROP INDEX idx_emp_name;

DROP INDEX 인덱스명; 을 통해 삭제할 수 있습니다.

 

-- 결합 인덱스

CREATE INDEX idx_emp_comp ON emp(name, dept);

AND 조건으로 검색하는 경우 결합 인덱스를 만들면 성능에 중요한 역할을 합니다. OR연산은 인덱스를 만들지 않습니다.

CREATE INDEX 인덱스명 ON 테이블명(컬럼명, 컬럼명);

 

※ 주의

CREATE INDEX idx_emp_comp ON emp(dept, name);

위 쿼리처럼 INDEX를 만들면 dept 에서 name 을 검색하므로, (순서가 다르므로) name을 검색하고 dept을 검색하는 것보다 속도가 느립니다. name을 만족하는 행보다 dept을 만족하는 행이 더 많기 때문입니다. (name이 중복값이 있을 수 있지만 dept보다 행의 개수가 적다.) 따라서 결합 인덱스를 만들 때, 괄호 안의 위치도 신경을 써야합니다.

 

-- 함수 기반 인덱스 만들기

CREATE INDEX idx_emp_fun ON emp(MOD(SUBSTR(rrn, 8, 1), 2 ) ) ;

 

-- 인덱스 관리

-- 모니터링

-- 인덱스 생성
CREATE INDEX idx_emp_name ON emp(name);

-- 모니터링 시작
ALTER INDEX idx_emp_name MONITORING USAGE;

-- 인덱스 사용 유무 확인
SELECT * FROM v$object_usage; -- used :  no

-- 모니터링 중단
ALTER INDEX idx_emp_name NOMONITORING USAGE;

-- REBUILD

 

실습을 위해 테이블을 작성하고 테이터를 10000개를 넣어주었습니다.

CREATE TABLE demo (
    num NUMBER
);

BEGIN
    FOR n IN 1 .. 10000 LOOP
        INSERT INTO demo VALUES (n);
    END LOOP;
    COMMIT;
END;
/

SELECT COUNT(*) FROM demo;

 

-- 위 테이블에 인덱스 추가하기.

CREATE INDEX idx_demo_num ON demo(num);

 

-- 인덱스 분석

ANALYZE INDEX idx_demo_num VALIDATE STRUCTURE;

SELECT * FROM index_stats;

SELECT (del_lf_rows_len / lf_rows_len ) * 100
FROM index_stats
WHERE name = 'IDX_DEMO_NUM'; -- 0에 가까우면 좋은 상태(인덱스가 깨지지 않은 상태)

DELETE FROM demo WHERE num <= 4000;
COMMIT;

ANALYZE INDEX idx_demo_num VALIDATE STRUCTURE;

SELECT (del_lf_rows_len / lf_rows_len ) * 100
FROM index_stats
WHERE name = 'IDX_DEMO_NUM'; -- 40% 정도 밸런싱이 깨짐

세 번째 쿼리를 처음 실행하면 0이 나오는데, 이는 인덱스가 깨지지 않은 상태를 의미합니다. 후 에, 10000개의 데이터에서 4000개를 삭제하면 IDX_DEMO_NUM 이 39.xxxxx 로 나옵니다. 인덱스가 40%정도 깨져있는 것을 확인할 수 있습니다.

 

-- REBUILD

ALTER INDEX idx_demo_num REBUILD; -- 밸런싱에 문제가 있으면 관리자가 다시 조절한다.

-- 인덱스 다시 분석
ANALYZE INDEX idx_demo_num VALIDATE STRUCTURE;

SELECT (del_lf_rows_len / lf_rows_len ) * 100
FROM index_stats
WHERE name = 'IDX_DEMO_NUM' -- 인덱스를 REBUILD 했기 때문에 다시 밸런싱이 좋아짐.

밸런싱에 문제가 있어서 ALTE INDEX 인덱스명 REBUILD; 를 통해 REBUILD하고 나면 다시 0으로 되어있는 것을 확인할 수 있습니다.

 

인덱스는 데이터가 삭제되도 남아있기 때문에 밸런싱이 깨지면 나중에 REBUILD를 하는 것이 필요해보입니다.

문제

-- score1 테이블 작성
hak     문자(20)  기본키
name   문자(30)  NOT  NULL
kor      숫자(3)     NOT  NULL
eng      숫자(3)    NOT  NULL
mat      숫자(3)    NOT  NULL

 

풀이 >>

더보기
CREATE TABLE score1 (
    hak VARCHAR2(30),
    name VARCHAR2(30) NOT NULL,
    kor NUMBER(3) NOT NULL,
    eng NUMBER(3) NOT NULL,
    mat NUMBER(3) NOT NULL,
    CONSTRAINT pk_score1_hak 
        PRIMARY KEY(hak)
);

-- score2 테이블 작성
hak     문자(20)  기본키, score1 테이블의 참조키
kor      숫자(2,1)     NOT  NULL
eng      숫자(2,1)    NOT  NULL
mat      숫자(2,1)    NOT  NULL
     

풀이 >>

더보기
CREATE TABLE score2 (
    hak VARCHAR2(20),
    kor NUMBER(2, 1) NOT NULL,
    eng NUMBER(2, 1) NOT NULL,
    mat NUMBER(2, 1) NOT NULL,
    CONSTRAINT pk_score2_hak PRIMARY KEY (hak),
    CONSTRAINT fk_score2_hak FOREIGN KEY (hak) 
        REFERENCES score1 (hak)
);


-- 평점을 구하는 함수 작성
-- 함수명 : fnGrade(s)
95~100:4.5    90~94:4.0
85~89:3.5     80~84:3.0
75~79:2.5     70~74:2.0
65~69:1.5     60~64:1.0
60미만 0
       

풀이 >> 

더보기
CREATE OR REPLACE FUNCTION fnGrade
(
	n NUMBER
)
RETURN NUMBER
IS
	s NUMBER(2, 1) := 0;
BEGIN
	IF n>= 95 THEN s := 4.5;
	ELSIF n>=90 THEN s := 4.0;
	ELSIF n>=85 THEN s := 3.5;
	ELSIF n>=80 THEN s := 3.0;
	ELSIF n>=75 THEN s := 2.5;
	ELSIF n>=70 THEN s := 2.0;
	ELSIF n>=65 THEN s := 1.5;
	ELSIF n>=60 THEN s := 1.0;
	ELSE s := 0;
    END IF;
	
	RETURN s;
END;
/


-- score1 테이블과 score2 테이블에 데이터를 추가하는 프로시저 만들기
프로시저명 : pScoreInsert
실행예 : EXEC pScoreInsert('1111', '가가가', 80, 60, 75);
   
score1 테이블 => '1111', '가가가', 80, 60, 75  정보 추가
score2 테이블 => '1111',            3.0, 1.0, 2.5 정보 추가(국, 영, 수 점수가 평점으로 계산되어 추가)
   
단, 국어, 영어, 수학 점수는 0~100 사이가 아니면 예외 발생하고 종료
 

풀이 >>

더보기
CREATE OR REPLACE PROCEDURE pScoreInsert
(
	pHak VARCHAR2,
	pName VARCHAR2,
	pKor NUMBER,
	pEng NUMBER,
	pMat NUMBER
)
IS
BEGIN
	IF pKor < 0 OR pKor > 100 THEN 
		RAISE_APPLICATION_ERROR(-20001, '점수는 0~100 사이 값만 입력 가능합니다.');
	ELSIF pEng < 0 OR pEng > 100 THEN
		RAISE_APPLICATION_ERROR(-20001, '점수는 0~100 사이 값만 입력 가능합니다.');
	ELSIF pMat <0 OR pMat > 100 THEN
		RAISE_APPLICATION_ERROR(-20001, '점수는 0~100 사이 값만 입력 가능합니다.');
	END IF;
	
	INSERT INTO score1(hak, name, kor, eng, mat) 
		VALUES(pHak, pName, pKor, pEng, pMat);
	INSERT INTO score2(hak, kor, eng, mat)
		VALUES(pHak, fnGrade(pKor), fnGrade(pEng), fnGrade(pMat) );
	COMMIT;
END;
/


-- score1 테이블과 score2 테이블에 데이터를 수정하는 프로시저 만들기
프로시저명 : pScoreUpdate
실행예 : EXEC pScoreUpdate('1111', '가가가', 90, 60, 75);
   
score1 테이블 => 학번이 '1111' 인 자료를  '가가가', 90, 60, 75  으로 정보 수정
score2 테이블 => 학번이 '1111' 인 자료를           4.0, 1.0, 2.5 으로 정보 수정(국, 영, 수 점수가 평점으로 계산되어 수정)
   
단, 국어, 영어, 수학 점수는 0~100 사이가 아니면 예외 발생하고 종료
   

풀이 >>

CREATE OR REPLACE PROCEDURE pScoreUpdate(
	pHak score1.hak%TYPE,
	pName score1.name%TYPE,
	pKor score1.kor%TYPE,
	pEng score1.eng%TYPE,
	pMat score1.mat%TYPE
)
IS
BEGIN
	IF pKor < 0 OR pKor > 100 THEN 
		RAISE_APPLICATION_ERROR(-20001, '점수는 0~100 사이 값만 입력 가능합니다.');
	ELSIF pEng < 0 OR pEng > 100 THEN
		RAISE_APPLICATION_ERROR(-20001, '점수는 0~100 사이 값만 입력 가능합니다.');
	ELSIF pMat <0 OR pMat > 100 THEN
		RAISE_APPLICATION_ERROR(-20001, '점수는 0~100 사이 값만 입력 가능합니다.');
	END IF;
	
	UPDATE score1 SET name = pName, kor = pKor, eng = pEng, mat = pMat 
		WHERE hak = pHak;
	UPDATE score2 SET kor = fnGrade(pKor), eng = fnGrade(pEng), mat = fnGrade(pMat)
		WHERE hak = pHak;
	COMMIT;
END;
/

 

-- score1 테이블과 score2 테이블에 데이터를 삭제하는 프로시저 만들기
프로시저명 : pScoreDelete
실행예 : EXEC pScoreDelete('1111');
score1 과 score2 테이블 정보 삭제

 

풀이 >>

CREATE PROCEDURE pScoreDelete(
	pHak score1.hak%TYPE
)
IS
BEGIN
	DELETE FROM score2 WHERE hak = pHak;
	DELETE FROM score1 WHERE hak = pHak;
	COMMIT;
END;
/

 

※ 프로시저 실행시에는 앞에 꼭 EXEC 를 붙여야 한다. 

EXEC pScoreInsert('1111', '홍길동', 80, 90, 90);
EXEC pScoreUpdate('1111', '홍길동', 100, 90, 90);
EXEC pScoreDelete('1111');

 

+ Recent posts