데이터베이스 연구실.log

저장 프로그램 이행

Oracle, MySQL에서 PostgreSQL로 저장 프로그램을 변환하고 트랜잭션, 행 수, 시각, 업무 호출을 대조하며 Oracle→MySQL 변환 절차는 확인 불가

GET DIAGNOSTICSRETURNS trigger서브트랜잭션PL/SQLPL/pgSQL트리거COMMIT예외 처리

#핵심 요약

테이블, 데이터, 제약의 이동만으로 업무 함수, 이력 트리거, 프로시저가 이행되지는 않음

객체 원문, 매개변수, 의존관계와 내부 COMMIT, 예외 처리, 출력 방식을 읽고 타깃 문법으로 재작성함

PL/SQL 객체 자체의 생성, 호출 설명은 기존 PL/SQL 글에서 다룸

#개념과 동작 원리

Oracle 소스는 USER_SOURCE, USER_ARGUMENTS, USER_DEPENDENCIES, 뷰는 USER_VIEWS로 확인하는 흐름임
MySQL 소스는 information_schema의 정의, 본문과 mysqldump 원문을 읽음

함수보다 뷰를 먼저 만들지 않고, 시퀀스와 다른 스키마 참조를 준비하며, 데이터 적재 뒤 트리거를 구성해 이력 중복을 피하는 순서가 수업 자료에 있음

Oracle → PostgreSQL 실습의 대응
선언, 종료 LANGUAGE plpgsql과 본문 구분 형식으로 재작성
함수 RETURN VARCHAR2를 RETURNS VARCHAR로 변경, 함수 다음에 의존 뷰 생성
트리거 RETURNS trigger 함수와 트리거를 분리, :NEW 대신 NEW 사용
BEFORE 행 트리거 NEW 반환 경로 확인, NULL 반환 시 해당 행 처리 생략 가능
채번 NEXTVAL을 nextval 호출로 변환
시각 SYSDATE의 대체 예에 LOCALTIMESTAMP(0), clock_timestamp, 날짜 단위는 CURRENT_DATE 사용
처리 건수 SQL%ROWCOUNT를 GET DIAGNOSTICS의 ROW_COUNT로 변환
출력, 호출 DBMS_OUTPUT을 RAISE NOTICE, EXEC를 CALL로 변경
트랜잭션 예외 처리 안쪽 블록과 바깥 COMMIT을 구분, 호출 경로, 오토커밋 조건 확인
MySQL → PostgreSQL 실습의 대응
함수 선언 DELIMITER 제거, 함수 본문, 특성 재작성
채번, 트리거 AUTO_INCREMENT를 IDENTITY로, 트리거 본문을 함수와 트리거로 분리
NOW 실습에서는 문장 시작 시각을 유지하는 대상으로 statement_timestamp를 제시
결과 출력 결과 집합 SELECT를 NOTICE로 바꾸는 예, 호출자가 받는 출력 방식 확인
예외, 트랜잭션 START TRANSACTION을 그대로 옮기지 않고 안쪽 EXCEPTION 블록과 바깥 COMMIT으로 재구성
건수 ROW_COUNT와 타깃 ROW_COUNT의 값이 달라지는 0% 인상 호출 비교
다른 DB 참조 같은 DB의 별도 스키마로 변환

#주요 용어와 구성 요소

구문 의미
PL/pgSQL BEGIN, END 코드 블록의 범위이며 SQL 트랜잭션 시작, 끝과 같은 의미가 아님
ROW_COUNT 직전 SQL이 처리한 행 수를 bigint로 제공하며 값이 달라진 행 수만을 뜻하지 않음
행 트리거 반환 BEFORE 행 트리거는 진행할 NEW, OLD 또는 건너뛸 NULL을 반환하며 AFTER의 반환값은 무시됨

#필수 명령어 또는 SQL (Oracle → PostgreSQL)

항목 실행 조건
작업 대상 Oracle 객체, 의존성 추출, PostgreSQL 스키마, 함수, 뷰, 트리거, 프로시저와 실제 업무 호출
필요 권한 Oracle 조회 객체 권한, 타깃 스키마 생성, USAGE, CREATE, 객체 소유, 필요 DML, 함수 실행 권한을 단계별로 구분함
바꿀 입력값 <USERNAME>, <TARGET_DB_NAME>, <REPORT_USER>, <PASSWORD>와 실제 소스, 타깃 객체
결과 해석 기존 기대와 호출 기록을 유지하며 누락된 변환 코드와 최종 데이터, 예외, 커밋 결과는 확인 불가

#대상과 의존관계, 원문 추출

SQL*Plus (<USERNAME>)조회만 함
SELECT object_type, object_name, status
  FROM user_objects
 WHERE object_type IN ('FUNCTION', 'PROCEDURE', 'TRIGGER', 'VIEW')
 ORDER BY object_type, object_name;

SELECT name, type, referenced_owner, referenced_name, referenced_type
  FROM user_dependencies
 WHERE referenced_owner NOT IN ('SYS', 'PUBLIC')
 ORDER BY type, name;
SQL*Plus (<USERNAME>, gen_src.sql)조회만 함
@<ORACLE_WORK_DIR>/set_gen.sql
SPOOL <ORACLE_WORK_DIR>/ora_plsql_src.txt
SELECT CASE WHEN line = 1
            THEN CHR(10) || '-- ===== ' || type || ' ' || name || ' =====' || CHR(10)
       END || RTRIM(text, CHR(10))
  FROM user_source
 ORDER BY DECODE(type, 'FUNCTION', 1, 'TRIGGER', 2, 'PROCEDURE', 3), name, line;
SELECT CHR(10) || '-- ===== VIEW ' || view_name || ' =====' FROM user_views;
SELECT text FROM user_views;
SPOOL OFF
EXIT

결과 해석: 의존관계에서 V_EMP_GRADE → GET_GRADE(함수 다음 뷰), TRG_ORDERS_BI → ORD_SEQ(시퀀스 다음 트리거), SEND_REPORT → <REPORT_USER>.RPT_DAILY(다른 스키마를 미리 준비)를 읽음
순서는 테이블 → 데이터 → 제약 → 시퀀스 → 다른 스키마 → 함수 → 뷰 → 트리거 → 프로시저이며, 트리거는 적재 뒤에 만들어야 이력이 중복으로 쌓이지 않음
뷰 본문은 USER_SOURCE가 아니라 USER_VIEWS.TEXT(LONG)에서 따로 뽑음

#다른 스키마 준비 (관리 계정)

psql (<TARGET_DB_NAME>, <ADMIN_USER>)객체 또는 설정을 변경함
CREATE ROLE <REPORT_USER> LOGIN PASSWORD '<PASSWORD>';
CREATE SCHEMA <REPORT_USER> AUTHORIZATION <REPORT_USER>;
CREATE TABLE <REPORT_USER>.rpt_daily (rpt_date TIMESTAMP(0), order_cnt NUMERIC, total_amt NUMERIC);
ALTER TABLE <REPORT_USER>.rpt_daily OWNER TO <REPORT_USER>;
GRANT USAGE ON SCHEMA <REPORT_USER> TO <USERNAME>;
GRANT SELECT, INSERT ON <REPORT_USER>.rpt_daily TO <USERNAME>;

결과 해석:

Oracle의 GRANT INSERT, SELECT ON <REPORT_USER>.rpt_daily에 더해 PostgreSQL은 스키마 USAGE 권한이 따로 필요함

#함수와 뷰

psql (<TARGET_DB_NAME>, <USERNAME>)객체 또는 설정을 변경함
CREATE OR REPLACE FUNCTION get_grade (p_sal NUMERIC)
RETURNS VARCHAR
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
  IF p_sal >= 3000 THEN
    RETURN 'A';
  ELSIF p_sal >= 2000 THEN
    RETURN 'B';
  ELSE
    RETURN 'C';
  END IF;
END;
$$;

CREATE OR REPLACE VIEW v_emp_grade AS
SELECT empno, ename, sal, get_grade(sal) AS grade FROM emp_m;

결과 해석: 개별 실행 출력은 기재 없음

#트리거 (함수 + 트리거로 나눔)

psql (<TARGET_DB_NAME>, <USERNAME>)객체 또는 설정을 변경함
CREATE OR REPLACE FUNCTION trg_orders_bi_fn()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  IF NEW.order_id IS NULL THEN
    NEW.order_id := nextval('ord_seq');
  END IF;
  IF NEW.reg_date IS NULL THEN
    NEW.reg_date := LOCALTIMESTAMP(0);
  END IF;
  RETURN NEW;
END;
$$;

CREATE TRIGGER trg_orders_bi
BEFORE INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION trg_orders_bi_fn();

CREATE OR REPLACE FUNCTION trg_orders_ai_fn()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO order_hist VALUES (NEW.order_id, 'INSERT', NEW.amount, LOCALTIMESTAMP(0));
  RETURN NULL;
END;
$$;

CREATE TRIGGER trg_orders_ai
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION trg_orders_ai_fn();

결과 해석:

BEFORE 행 트리거는 RETURN NEW로 행을 돌려줘야 들어가고, RETURN NULL이면 그 행을 오류 없이 건너뛰어 INSERT가 0건으로 끝날 수 있음
AFTER 트리거의 반환값은 무시됨
RETURN 경로가 없으면 control reached end of trigger procedure without RETURN이 남

확인: BEGIN; INSERT INTO orders (cust_name, amount) VALUES ('테스트', 1) RETURNING *; 뒤 order_hist 최신 1건을 보고 ROLLBACK

#프로시저 (COMMIT 위치와 ROW_COUNT)

psql (<TARGET_DB_NAME>, <USERNAME>)객체 또는 설정을 변경함
CREATE OR REPLACE PROCEDURE add_order (p_cust VARCHAR, p_amt NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
  BEGIN
    INSERT INTO orders (cust_name, amount) VALUES (p_cust, p_amt);
  EXCEPTION
    WHEN OTHERS THEN
      RAISE NOTICE '주문 실패 : %', SQLERRM;
      RETURN;
  END;
  COMMIT;
  RAISE NOTICE '주문 성공 : %', p_cust;
END;
$$;

CREATE OR REPLACE PROCEDURE raise_sal (p_deptno NUMERIC, p_pct NUMERIC)
LANGUAGE plpgsql
AS $$
DECLARE
  v_cnt INTEGER;
BEGIN
  UPDATE emp_m SET sal = sal * (1 + p_pct / 100) WHERE deptno = p_deptno;
  GET DIAGNOSTICS v_cnt = ROW_COUNT;
  RAISE NOTICE '%명 급여 인상', v_cnt;
  COMMIT;
END;
$$;

CREATE OR REPLACE PROCEDURE send_report ()
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO <REPORT_USER>.rpt_daily
  SELECT CURRENT_DATE, COUNT(*), SUM(amount) FROM orders;
  COMMIT;
END;
$$;

결과 해석:

Oracle 원문처럼 본문에 COMMIT을 두고 EXCEPTION ... ROLLBACK을 그대로 옮긴 첫 변환은 CALL add_order('토스', 55000)에서 NOTICE: 주문 실패 : cannot commit while a subtransaction is active가 나고 주문 건수는 그대로, 시퀀스 번호만 하나 쓰였음
EXCEPTION이 있는 블록은 서브트랜잭션 경계가 되어 그 안에서 COMMIT할 수 없으므로, 실패할 수 있는 INSERT만 안쪽 블록에 두고 COMMIT은 EXCEPTION이 없는 바깥 블록으로 옮김

같은 이름이라도 매개변수 타입이 다르면 별도 프로시저가 생기므로 이행 중 타입을 바꿨다면 \df add_order로 중복 정의를 확인함

#객체와 업무 호출 확인

psql (<TARGET_DB_NAME>, <USERNAME>)조회만 함
SELECT CASE prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, proname
  FROM pg_proc WHERE pronamespace = '<USERNAME>'::regnamespace ORDER BY 1, 2;
SELECT tgrelid::regclass AS table_name, tgname, tgenabled FROM pg_trigger WHERE NOT tgisinternal ORDER BY 1, 2;
SELECT viewname FROM pg_views WHERE schemaname = '<USERNAME>';

CALL add_order('토스', 55000);
CALL add_order(repeat('가', 31), 1000);
CALL raise_sal(10, 0);
CALL send_report();
SELECT * FROM <REPORT_USER>.rpt_daily;

결과 해석:

종류 Oracle PostgreSQL
함수 1 3 (get_grade + 트리거 함수 2)
프로시저 3 3
트리거 2 2 (tgenabled = O)
뷰 1 1

개수만 맞추지 않고 객체별 대응을 보며, 양쪽에서 v_emp_grade 등급별 집계, orders 건수, 합계, order_hist 건수를 같은 SQL로 비교함

예상값: 정상 주문은 주문 성공과 함께 주문, 이력이 1건씩 늘고, 31자 고객명은 주문 실패 : value too long for type character varying(30)로 두 테이블 모두 그대로, raise_sal(10, 0)은 값은 그대로 두고 처리 건수만 NOTICE로 보임
주석의 주문번호, 건수는 시퀀스 소비 이력에 따라 달라지므로 실제 조회로 확인함

COMMIT이 든 프로시저를 명시적 BEGIN으로 연 트랜잭션 안에서 부르면 invalid transaction termination이 날 수 있어, 애플리케이션의 autocommit 설정과 호출 경로를 확인함

#필수 명령어 또는 SQL (MySQL → PostgreSQL)

#변환 규칙

MySQL PostgreSQL 변환 시 주의점
DELIMITER와 END // AS $$ ... $$; 클라이언트 구분자 제거
DROP 후 CREATE CREATE OR REPLACE 함수, 뷰, 프로시저 재정의
ELSEIF ELSIF, ELSEIF도 허용됨 잘린 문장 오류와 철자 오류 구분
DETERMINISTIC NO SQL IMMUTABLE get_grade의 입력만 사용하는 조건
트리거 본문, SET NEW.col RETURNS trigger 함수, NEW.col := 값 BEFORE는 RETURN NEW, AFTER 반환값은 무시됨
AUTO_INCREMENT BY DEFAULT IDENTITY BEFORE 트리거 전에 기본 번호 부여
NOW() statement_timestamp() 문장 시작 시각 유지
START TRANSACTION 프로시저에서 제거 PL/pgSQL 내부 명령 지원 여부
EXIT HANDLER, ROLLBACK 안쪽 BEGIN, EXCEPTION 블록 해당 블록 변경 자동 취소
MESSAGE_TEXT SQLERRM 또는 GET STACKED DIAGNOSTICS 최초 오류와 후속 출력 오류 구분
SELECT ... AS result RAISE NOTICE 결과 집합과 메시지 채널 차이
ROW_COUNT() GET DIAGNOSTICS ... = ROW_COUNT 변경된 행과 처리한 행의 차이
CURDATE(), 다른 DB 참조 CURRENT_DATE, 다른 스키마 참조 리포트 구조, 권한 준비

#함수와 뷰 적용

DB 클라이언트 셸 (<WORK_DIR>)데이터 또는 파일을 삭제함, 되돌릴 수 없음
psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -f '<MYSQL_PROGRAM_DIR>/my_05_func_view.sql'

결과 해석: DELIMITER에서 문법 오류가 시작되고 ELSEIF, ELSE, END IF, //와 get_grade(numeric) 미존재 오류가 이어진 기록임
ELSEIF 오류는 이 단계에서 본문이 세미콜론으로 잘려 발생했으며 PL/pgSQL이 ELSEIF 철자를 받지 않는다는 의미는 아님

DB 클라이언트 셸 (<WORK_DIR>)객체 또는 설정을 변경함
cat > pg_05_func_view.sql <<'EOF'
CREATE OR REPLACE FUNCTION get_grade (p_sal NUMERIC)
RETURNS VARCHAR
LANGUAGE plpgsql
IMMUTABLE
AS $$
BEGIN
  IF p_sal >= 3000 THEN
    RETURN 'A';
  ELSIF p_sal >= 2000 THEN
    RETURN 'B';
  ELSE
    RETURN 'C';
  END IF;
END;
$$;

CREATE OR REPLACE VIEW v_emp_grade AS
SELECT empno, ename, sal, get_grade(sal) AS grade
  FROM emp_m;
EOF

psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -v ON_ERROR_STOP=1 -f pg_05_func_view.sql

psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -c "SELECT * FROM v_emp_grade ORDER BY empno"

결과 해석: CREATE FUNCTION, CREATE VIEW 뒤 6행이 조회된 기록임
등급 결과는 아래 값 비교 절과 같으며 함수 다음에 의존 뷰를 생성함

#IDENTITY와 등록일, 이력 트리거

DB 클라이언트 셸 (<WORK_DIR>)데이터 또는 파일을 삭제함, 되돌릴 수 없음
cat > pg_06_triggers.sql <<'EOF'
-- ===== trg_orders_bi : 등록일만 (번호는 IDENTITY가 채움) =====
CREATE OR REPLACE FUNCTION trg_orders_bi_fn()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  IF NEW.reg_date IS NULL THEN
    NEW.reg_date := statement_timestamp()::timestamp(0);   -- MySQL NOW() = 문장 시작 시각
  END IF;
  RETURN NEW;                       -- BEFORE 트리거는 NEW를 돌려줘야 행이 들어감
END;
$$;

DROP TRIGGER IF EXISTS trg_orders_bi ON orders;
CREATE TRIGGER trg_orders_bi
BEFORE INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION trg_orders_bi_fn();

-- ===== trg_orders_ai : 주문 이력 기록 =====
CREATE OR REPLACE FUNCTION trg_orders_ai_fn()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO order_hist
  VALUES (NEW.order_id, 'INSERT', NEW.amount, statement_timestamp()::timestamp(0));
  RETURN NULL;                      -- AFTER 트리거는 돌려주는 값을 무시함
END;
$$;

DROP TRIGGER IF EXISTS trg_orders_ai ON orders;
CREATE TRIGGER trg_orders_ai
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION trg_orders_ai_fn();
EOF

psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -v ON_ERROR_STOP=1 -f pg_06_triggers.sql

결과 해석: 트리거 함수 2개와 트리거 2개가 생성된 기록임
Oracle → PostgreSQL 예의 ord_seq, LOCALTIMESTAMP와 달리 이 방향은 IDENTITY로 번호를 채우고 statement_timestamp()로 시각을 기록함
MySQL NOW()는 문장 시작 시각이고 PostgreSQL now()는 트랜잭션 시작 시각이므로 이름만 같게 바꾸지 않음

psql (<TARGET_DB_NAME>, <USERNAME>)객체 또는 설정을 변경함
-- 트리거만 먼저 테스트 (트랜잭션으로 묶고 되돌림)
BEGIN;
INSERT INTO orders (cust_name, amount) VALUES ('테스트', 1) RETURNING *;
SELECT * FROM order_hist ORDER BY order_id DESC LIMIT 1;
ROLLBACK;
\q

결과 해석: 테스트 주문번호 1010, 금액 1, 주문 등록일과 이력 시각 2026-10-19 08:11:26이 조회된 기록임
ROLLBACK은 주문, 이력 변경을 취소하지만 IDENTITY 번호 1010은 되돌리지 않아 이후 정상 주문은 1011로 기록됨
MySQL의 BEFORE 트리거에서 AUTO_INCREMENT 번호가 0이었던 실습과 달리 PostgreSQL은 BEFORE 트리거 전에 IDENTITY 기본값을 채움

#add_order 실패 변환과 오류 원인

psql (<TARGET_DB_NAME>, <USERNAME>)객체 또는 설정을 변경함
-- ① MySQL 프로시저를 문법만 바꿔 옮긴 것
CREATE OR REPLACE PROCEDURE add_order (p_cust VARCHAR(255), p_amt NUMERIC(38,10))
LANGUAGE plpgsql
AS $$
DECLARE
  v_msg TEXT;
BEGIN
  START TRANSACTION;
  INSERT INTO orders (cust_name, amount) VALUES (p_cust, p_amt);
  COMMIT;
  SELECT CONCAT('주문 성공 : ', p_cust) AS result;
EXCEPTION
  WHEN OTHERS THEN
    GET STACKED DIAGNOSTICS v_msg = MESSAGE_TEXT;
    ROLLBACK;
    SELECT CONCAT('주문 실패 : ', v_msg) AS result;
END;
$$;

CALL add_order('배민', 33000);

-- ② 화면 출력만 RAISE NOTICE로 바꾼 것
CREATE OR REPLACE PROCEDURE add_order (p_cust VARCHAR(255), p_amt NUMERIC(38,10))
LANGUAGE plpgsql
AS $$
DECLARE
  v_msg TEXT;
BEGIN
  START TRANSACTION;
  INSERT INTO orders (cust_name, amount) VALUES (p_cust, p_amt);
  COMMIT;
  RAISE NOTICE '주문 성공 : %', p_cust;
EXCEPTION
  WHEN OTHERS THEN
    GET STACKED DIAGNOSTICS v_msg = MESSAGE_TEXT;
    ROLLBACK;
    RAISE NOTICE '주문 실패 : %', v_msg;
END;
$$;

CALL add_order('배민', 33000);
SELECT COUNT(*) FROM orders;
\q
변환 단계 기록된 출력 데이터
첫 정의의 SELECT 출력 query has no destination for result data 예외 처리의 SELECT가 최초 오류를 가림
두 번째 정의의 NOTICE 출력 주문 실패 : unsupported transaction command in PL/pgSQL, 이어서 CALL orders 9건 유지

결과 해석: 먼저 실패한 명령은 START TRANSACTION이며 첫 정의에서는 예외 처리의 SELECT가 다시 실패함
출력만 NOTICE로 바꾼 두 번째 정의는 업무 실패를 알리면서 CALL 자체는 오류 없이 끝남

#프로시저 3개 적용

DB 클라이언트 셸 (<WORK_DIR>)객체 또는 설정을 변경함
cat > pg_07_procedures.sql <<'EOF'
-- ===== add_order : START TRANSACTION 지움, COMMIT은 예외처리 블록 밖으로, SELECT 출력은 RAISE NOTICE =====
CREATE OR REPLACE PROCEDURE add_order (p_cust VARCHAR, p_amt NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
  BEGIN                                       -- 안쪽 블록 : 실패하면 여기만 되돌림 (MySQL EXIT HANDLER + ROLLBACK)
    INSERT INTO orders (cust_name, amount) VALUES (p_cust, p_amt);
  EXCEPTION
    WHEN OTHERS THEN
      RAISE NOTICE '주문 실패 : %', SQLERRM;
      RETURN;
  END;

  COMMIT;                                     -- 바깥 블록에는 EXCEPTION이 없으므로 가능
  RAISE NOTICE '주문 성공 : %', p_cust;
END;
$$;

-- ===== raise_sal : ROW_COUNT() → GET DIAGNOSTICS =====
CREATE OR REPLACE PROCEDURE raise_sal (p_deptno INT, p_pct NUMERIC)
LANGUAGE plpgsql
AS $$
DECLARE
  v_cnt INTEGER;
BEGIN
  UPDATE emp_m
     SET sal = sal * (1 + p_pct / 100)
   WHERE deptno = p_deptno;

  GET DIAGNOSTICS v_cnt = ROW_COUNT;
  RAISE NOTICE '%명 급여 인상', v_cnt;
  COMMIT;
END;
$$;

-- ===== send_report : 다른 DB(<REPORT_USER>) → 다른 스키마, CURDATE() → CURRENT_DATE =====
CREATE OR REPLACE PROCEDURE send_report ()
LANGUAGE plpgsql
AS $$
BEGIN
  INSERT INTO <REPORT_USER>.rpt_daily
  SELECT CURRENT_DATE, COUNT(*), SUM(amount)
    FROM orders;
  COMMIT;
END;
$$;
EOF

psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -v ON_ERROR_STOP=1 -f pg_07_procedures.sql

psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -c "\df add_order"

결과 해석: CREATE PROCEDURE 3건과 add_order 한 개가 조회된 기록임
VARCHAR(255), NUMERIC(38,10)을 VARCHAR, NUMERIC으로 바꿔도 매개변수 길이, 자릿수는 프로시저 식별 기준에 들어가지 않아 같은 정의가 교체됨
Oracle → PostgreSQL 예의 raise_sal(p_deptno NUMERIC)과 달리 이 실습의 인자는 INT임
두 방향의 파일을 같은 DB에 섞어 적용하면 인자 타입이 다른 별도 정의가 생길 수 있으므로 각각의 실습 DB와 호출 대상을 구분함

안쪽 EXCEPTION 블록 실패는 주문 INSERT와 트리거의 이력 INSERT를 함께 취소함
COMMIT은 EXCEPTION이 없는 바깥 블록에 두며 CALL 경로, autocommit 조건은 아래 트랜잭션 설명과 같음

#객체 목록과 함수, 뷰 값 비교

DB 클라이언트 셸 (<WORK_DIR>)조회만 함
mysql -t -e "SELECT routine_type, routine_name FROM information_schema.routines WHERE routine_schema = DATABASE() ORDER BY 1, 2;
SELECT trigger_name, action_timing FROM information_schema.triggers WHERE trigger_schema = DATABASE();
SELECT table_name AS view_name FROM information_schema.views WHERE table_schema = DATABASE();"

psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>'
psql (<TARGET_DB_NAME>, <USERNAME>)조회만 함
-- PostgreSQL (psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>')
SELECT CASE prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS object_type, proname
  FROM pg_proc
 WHERE pronamespace = '<USERNAME>'::regnamespace
 ORDER BY 1, 2;

SELECT tgrelid::regclass AS table_name, tgname, tgenabled
  FROM pg_trigger
 WHERE NOT tgisinternal
 ORDER BY 1, 2;

SELECT viewname FROM pg_views WHERE schemaname = '<USERNAME>';
\q
종류 MySQL PostgreSQL
함수 get_grade 1개 get_grade, 트리거 함수 2개, 합계 3개
프로시저 add_order, raise_sal, send_report 3개 같은 업무 프로시저 3개
트리거 2개 2개, tgenabled = O
뷰 v_emp_grade 1개 v_emp_grade 1개

결과 해석: 함수 수 차이는 트리거 함수 분리 때문임
pg_trigger 조회에는 스키마 필터가 없으므로 다른 스키마의 사용자 트리거가 있는 DB에서는 대상 구분이 필요함

DB 클라이언트 셸 (<WORK_DIR>)객체 또는 설정을 변경함
cat > compare.sql <<'EOF'
SELECT * FROM v_emp_grade ORDER BY empno;
SELECT grade, COUNT(*), SUM(sal) FROM v_emp_grade GROUP BY grade ORDER BY grade;
SELECT COUNT(*), SUM(amount) FROM orders;
SELECT COUNT(*) FROM order_hist;
EOF

echo "===== MySQL ====="
mysql -t < compare.sql
echo "===== PostgreSQL ====="
psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -f compare.sql
empno sal grade
7499 1600 C
7566 2975 B
7782 2450 B
7788 3000 A
7839 5000 A
7900 950 C
집계 양쪽 실습 기록
A / B / C 인원 각 2명
A / B / C 급여 합계 8000 / 5425 / 2550
주문 건수 / 금액 합계 9 / 700000
이력 건수 9

결과 해석: 위 표는 업무 호출 전 양쪽 조회 기록임
후속 주문을 타깃에만 넣으면 양쪽 데이터의 비교 시점이 달라짐

#정상 주문, 실패 주문, 급여, 리포트 호출

psql (<TARGET_DB_NAME>, <USERNAME>)객체 또는 설정을 변경함
-- ① 정상 주문
CALL add_order('배민', 33000);

SELECT * FROM orders     ORDER BY order_id DESC LIMIT 1;
SELECT * FROM order_hist ORDER BY order_id DESC LIMIT 1;
SELECT COUNT(*) FROM orders;
SELECT COUNT(*) FROM order_hist;

-- ② 실패하는 주문 (고객명 30자 초과)
CALL add_order(repeat('가', 31), 1000);
SELECT COUNT(*) FROM orders;
SELECT COUNT(*) FROM order_hist;

-- ③ 급여 인상 — 0%로 호출해서 값은 바꾸지 않고 동작만 확인
CALL raise_sal(10, 0);

-- ④ 다른 스키마에 리포트 적재
CALL send_report();
SELECT * FROM <REPORT_USER>.rpt_daily;
\q
호출 실습 기록
정상 주문 33000 주문 성공 NOTICE, 주문번호 1011, 주문, 이력 각 10건
고객명 31자 value too long for type character varying(30) NOTICE 뒤 CALL, 주문, 이력 각 10건 유지
raise_sal(10, 0) 2명 급여 인상 NOTICE, 급여 값은 그대로임
send_report 리포트 1행, 2026-10-19 00:00:00, 주문 건수 10, 합계 733000

결과 해석: 리포트 합계는 호출 전 700000에 정상 주문 33000을 더한 실습 결과임
실패 주문에서 CALL이 끝나도 주문과 이력이 늘어난 것은 아님

DB 클라이언트 셸 (<WORK_DIR>)객체 또는 설정을 변경함
mysql -t -e "CALL raise_sal(10, 0)"
MySQL 클라이언트 (<SOURCE_DB_NAME>, 이행 계정)객체 또는 설정을 변경함
UPDATE emp_m SET sal = sal * 1 WHERE deptno = 10;
-- Query OK, 0 rows affected
-- Rows matched: 2  Changed: 0  Warnings: 0

결과 해석: 같은 0% 인상 호출에서 MySQL은 0명, PostgreSQL은 2명으로 기록됨
MySQL UPDATE의 표시도 Rows matched: 2, Changed: 0, Warnings: 0임
MySQL ROW_COUNT()의 변경 행 수와 PostgreSQL 처리 행 수를 구분하며 이 결과를 모든 드라이버의 CALL 반환값으로 확대하지 않음
자료에는 CLIENT_FOUND_ROWS와 JDBC의 반환 차이 설명이 있으나 해당 프로시저 내부 값, 접속 옵션별 실행 출력은 기재 없음

#이행 파일 확인

DB 클라이언트 셸 (<WORK_DIR>)조회만 함
ls -l *.sql *.sh data/

결과 해석: 구조, 제약, 번호 생성기, TSV 추출, 이행 묶음, 적재용 SQL 4종, 저장 프로그램 파일 3종과 data 폴더가 기록된 구성임
목록, 값 비교 파일은 both_count.sql과 compare.sql임

#구문과 옵션 설명

프로시저의 트랜잭션 제어는 최상위 CALL, DO 또는 다른 명령이 끼지 않은 연속 CALL, DO 호출에서 허용됨
중간에 SELECT 함수 호출이 끼면 아래 프로시저는 트랜잭션 제어를 할 수 없음
EXCEPTION이 있는 블록 안에서는 트랜잭션을 끝낼 수 없으며 허용된 COMMIT, ROLLBACK 뒤에는 새 트랜잭션이 자동으로 시작됨

#사용 조건과 주의사항

실습의 IMMUTABLE, DETERMINISTIC 예는 해당 함수의 입력, 외부 상태 의존성을 검토하는 조건임
모든 함수에 같은 선언을 붙이는 규칙으로 쓰지 않음

예외를 잡아 NOTICE만 출력하면 CALL의 외형적 완료와 업무 실패가 함께 나타날 수 있음
메시지와 함께 실제 데이터와 이력을 확인해야 함

Oracle → PostgreSQL 실습의 실패 NOTICE, 번호, 건수 주석은 실제 호출 조건과 대조해야 함
호출 성공 여부는 확인 불가

SEO_HKabsumveritas@gmail.comBuilt with Claude Code & Codex

마지막 갱신 개인 학습 기록이며 운영 환경 적용 전 공식 문서 확인 필요

두 글자 이상 입력, 위아래 화살표로 이동하고 Enter로 열기