MySQL에서 PostgreSQL로 이행
MySQL 사전 기반 DDL, TSV, 제약, IDENTITY 이행, 양쪽 건수, 합계 비교와 상품 이행 연습의 확인 기준
#핵심 요약
MySQL 8.4.10의 업무 DB를 PostgreSQL 15.19의 새 DB로 이행하는 실습임
업무 스키마와 리포트 스키마를 나누고 테이블, 데이터, 채번, 저장 프로그램을 순서대로 옮김
실습 기록은 테이블 7개 50행 적재, 주문 금액 합계 700000, 급여 합계 15975의 양쪽 일치임
건수, 합계 일치는 비교한 항목의 결과이며 모든 열의 값과 동시 변경 통제까지 입증하는 기준은 아님
#개념과 동작 원리
| 단계 | 처리 |
|---|---|
| 준비 | 두 DB 접속, 버전 확인, 전용 작업 폴더와 타깃 DB, 스키마 준비 |
| 대상 확정 | information_schema에서 테이블, 루틴, 트리거, 뷰 목록 조회 |
| 의존성 | 뷰 정의, 트리거 본문, 루틴 본문의 다른 DB 참조 확인 |
| 추출 | mysqldump로 구조, 프로그램 추출, 사전 조회로 타깃 DDL 생성 |
| 데이터 | MySQL 배치 TSV를 psql의 클라이언트 복사로 적재 |
| 적용 | 테이블 → 데이터 → PK, UNIQUE → FK → 일반 인덱스 → IDENTITY 보정 |
| 프로그램 | 리포트 스키마, 권한 준비 후 함수 → 뷰 → 트리거 → 프로시저 |
| 확인 | 양쪽 COUNT, SUM, 함수, 뷰 값, 정상, 실패 업무 호출 비교 |
리포트 DB는 타깃의 같은 DB 안에 별도 스키마로 대응함
기존 리포트 데이터 1건은 다른 시스템 소유이므로 이행 범위에서 제외하고 구조, 권한만 준비함
저장 프로그램의 변환 코드와 호출 확인은 저장 프로그램 이행에 있음
#필수 명령어 또는 SQL
#1 접속과 대상 목록 확인
export PGPASSWORD='<PASSWORD>'
export PGCLIENTENCODING=UTF8
mkdir -p '<WORK_DIR>' && cd '<WORK_DIR>'
mysql -e "SELECT CURRENT_USER(), DATABASE(), VERSION()"
psql -h '<HOST>' -U '<USERNAME>' -d '<MAINTENANCE_DB>' -c "SELECT current_user, version()"결과 해석: 접속 출력의 MySQL 버전은 8.4.10, PostgreSQL 버전은 15.19임
PGCLIENTENCODING은 UTF8이며 PGPASSWORD는 현재 셸에서 사용할 비밀번호 환경변수임
SELECT table_name, table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'
ORDER BY 1;
SELECT routine_type, routine_name
FROM information_schema.routines
WHERE routine_schema = DATABASE()
ORDER BY 1, 2;
SELECT trigger_name, action_timing, event_manipulation, event_object_table
FROM information_schema.triggers
WHERE trigger_schema = DATABASE();
SELECT table_name AS view_name
FROM information_schema.views
WHERE table_schema = DATABASE();| 테이블 | 소스 TABLE_ROWS 표시 | 이후 COUNT, 적재 기록 |
|---|---|---|
| dept | 4 | 4 |
| emp | 14 | 14 |
| emp_m | 6 | 6 |
| emp_t | 6 | 6 |
| emp_t_audit | 2 | 2 |
| order_hist | 9 | 9 |
| orders | 9 | 9 |
| 합계 | 50 | 50 |
결과 해석: 함수 get_grade 1개, 프로시저 add_order, raise_sal, send_report 3개, 트리거 trg_orders_bi, trg_orders_ai 2개, 뷰 v_emp_grade 1개임
TABLE_ROWS는 InnoDB 통계의 추정치이므로 최종 비교에는 COUNT(*)를 사용함
#2 의존관계와 생성 구문 추출
SELECT table_name, view_definition
FROM information_schema.views
WHERE table_schema = DATABASE()\G
SELECT trigger_name, action_statement
FROM information_schema.triggers
WHERE trigger_schema = DATABASE()\G
SELECT routine_name
FROM information_schema.routines
WHERE routine_schema = DATABASE()
AND routine_definition LIKE '%<REPORT_USER>.%';
EXIT결과 해석: v_emp_grade는 get_grade, 이력 트리거는 order_hist, send_report는 다른 DB의 rpt_daily를 참조함
본문 문자열 검색은 동적 SQL, 주석도 걸릴 수 있으므로 검색된 루틴의 정의를 함께 확인함
mysqldump --no-data --routines --triggers --skip-comments --no-tablespaces '<SOURCE_DB_NAME>' > my_src.sql
grep -nE "^CREATE|CREATE DEFINER|^DELIMITER" my_src.sql결과 해석: --no-data는 행을 제외하고 --routines, --triggers는 저장 프로그램을 포함함
트리거 생성 구문은 버전 주석 안에 있어 CREATE만 검색하면 빠질 수 있으며 DELIMITER 줄로도 위치를 확인함
실습에서는 PROCESS 권한이 없는 계정의 테이블스페이스 정보 추출을 피하려고 --no-tablespaces를 사용함
#3 타깃 DB, 스키마, 리포트 권한 준비
PGPASSWORD='<PASSWORD>' psql -h '<HOST>' -U '<ADMIN_USER>'CREATE DATABASE <TARGET_DB_NAME> OWNER <USERNAME>;
\c <TARGET_DB_NAME>
CREATE SCHEMA <USERNAME> AUTHORIZATION <USERNAME>;
CREATE SCHEMA <REPORT_USER> AUTHORIZATION <REPORT_USER>;
CREATE TABLE <REPORT_USER>.rpt_daily
( rpt_date TIMESTAMP(0),
order_cnt NUMERIC(38,10),
total_amt NUMERIC(38,10) );
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>;
\dn
\q결과 해석: DB 1개, 업무, 리포트 스키마 2개, 리포트 테이블과 USAGE, SELECT, INSERT 권한이 준비된 기록임
롤은 클러스터 전체에서 공유하므로 기존 롤을 사용함
기본 검색 경로의 "$user", public을 이용하는 조건이며 업무 계정과 같은 이름의 스키마를 준비함
실습에서는 업무 스키마가 없으면 public 스키마 생성 권한 오류가 발생하는 조건을 경고함
#4 타입과 테이블 DDL 생성
| MySQL 타입, 속성 | PostgreSQL 생성 규칙 |
|---|---|
| bigint / int / smallint / tinyint | BIGINT / INTEGER / SMALLINT / SMALLINT |
| decimal(p,s) | NUMERIC(p,s) |
| varchar(n) / char(n) | VARCHAR(n) / CHAR(n), 글자 수 기준 |
| datetime(n) / date | TIMESTAMP(n) / DATE |
| double / text / longtext / longblob | DOUBLE PRECISION / TEXT / TEXT / BYTEA |
| auto_increment | GENERATED BY DEFAULT AS IDENTITY |
| 나머지 타입 | TEXT와 확인 필요 주석 생성 |
| ENGINE, CHARSET | 테이블 생성문에서 제외하고 타깃 DB 인코딩 사용 |
cat > gen_ddl_pg.sql <<'EOF'
SET SESSION group_concat_max_len = 1000000;
SELECT CONCAT('DROP TABLE IF EXISTS ', t.table_name, ' CASCADE;\n',
'CREATE TABLE ', t.table_name, ' (\n',
GROUP_CONCAT(CONCAT(' ', RPAD(c.column_name, 12, ' '), ' ',
CASE c.data_type
WHEN 'bigint' THEN 'BIGINT'
WHEN 'int' THEN 'INTEGER'
WHEN 'smallint' THEN 'SMALLINT'
WHEN 'tinyint' THEN 'SMALLINT'
WHEN 'decimal' THEN CONCAT('NUMERIC(', c.numeric_precision, ',', c.numeric_scale, ')')
WHEN 'varchar' THEN CONCAT('VARCHAR(', c.character_maximum_length, ')')
WHEN 'char' THEN CONCAT('CHAR(', c.character_maximum_length, ')')
WHEN 'datetime' THEN CONCAT('TIMESTAMP(', c.datetime_precision, ')')
WHEN 'date' THEN 'DATE'
WHEN 'double' THEN 'DOUBLE PRECISION'
WHEN 'text' THEN 'TEXT'
WHEN 'longtext' THEN 'TEXT'
WHEN 'longblob' THEN 'BYTEA'
ELSE CONCAT('TEXT /* 확인 필요: ', c.column_type, ' */')
END,
IF(c.extra LIKE '%auto_increment%', ' GENERATED BY DEFAULT AS IDENTITY', ''),
IF(c.is_nullable = 'NO', ' NOT NULL', ''))
ORDER BY c.ordinal_position SEPARATOR ',\n'),
'\n);\n')
FROM information_schema.tables t
JOIN information_schema.columns c
ON c.table_schema = t.table_schema AND c.table_name = t.table_name
WHERE t.table_schema = DATABASE()
AND t.table_type = 'BASE TABLE'
GROUP BY t.table_name
ORDER BY t.table_name;
EOF
mysql -N -B --raw < gen_ddl_pg.sql > pg_01_tables.sql
cat pg_01_tables.sql결과 해석: 테이블 7개의 CREATE TABLE이 생성되고 orders.order_id에 BY DEFAULT IDENTITY가 붙은 기록임
기존 번호를 명시해 적재해야 하므로 BY DEFAULT를 사용함
GROUP_CONCAT 길이를 늘려 열 목록 잘림을 피하며 -N은 머리글 제외, -B는 배치 형식, --raw는 생성문의 개행을 그대로 출력하는 용도임
#5 TSV 추출과 적재 구문 생성
cat > export_tsv.sh <<'EOF'
#!/bin/bash
# 테이블마다 TSV로 내려받고, 적재 명령(\copy)을 pg_02_load.sql 로 만든다
mkdir -p data
for t in $(mysql -N -B -e "SELECT table_name FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE' ORDER BY 1"); do
mysql -N -B -e "SELECT * FROM \`$t\`" > data/$t.tsv
echo "\copy $t FROM 'data/$t.tsv' WITH (FORMAT text, NULL 'NULL')"
done > pg_02_load.sql
wc -l data/*.tsv
EOF
chmod +x export_tsv.sh
./export_tsv.sh
cat pg_02_load.sql
head -3 data/orders.tsv data/emp.tsv결과 해석: 파일별 행 수는 위 표와 같고 합계는 50행임
MySQL 배치 출력의 NULL은 글자 NULL로 나타나므로 적재 구문은 NULL 'NULL'을 지정함
클라이언트 복사는 psql이 파일을 읽으므로 DB 서버 OS 계정의 작업 폴더 읽기 권한과 구분됨
자료의 배치 TSV와 COPY text가 같다는 설명은 이 실습의 파일 규칙에 한정함
FORMAT text의 기본 NULL 표식은 \N이며 여기서 지정한 글자 NULL과 다름
실제 NULL과 문자열 'NULL'이 같은 표식으로 출력되면 구별할 수 없음
기본 NULL 표식과 문자 그대로의 역슬래시 N은 올바르게 이스케이프하면 구분 가능함
NULL 표식 판정은 역슬래시 제거보다 먼저 수행되므로 파일의 인코딩, 열 순서, 탭, 개행, 캐리지 리턴, 역슬래시 규칙을 함께 확인함
#6 제약조건, 인덱스, IDENTITY 생성
cat > gen_cons_pg.sql <<'EOF'
SELECT ddl FROM (
-- 기본키·유니크
SELECT 1 AS ord, tc.table_name AS tn,
CONCAT('ALTER TABLE ', tc.table_name, ' ADD ',
IF(tc.constraint_type = 'PRIMARY KEY', 'PRIMARY KEY (',
CONCAT('CONSTRAINT ', tc.constraint_name, ' UNIQUE (')),
GROUP_CONCAT(k.column_name ORDER BY k.ordinal_position), ');') AS ddl
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage k
ON k.constraint_schema = tc.constraint_schema
AND k.constraint_name = tc.constraint_name AND k.table_name = tc.table_name
WHERE tc.table_schema = DATABASE() AND tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE')
GROUP BY tc.table_name, tc.constraint_name, tc.constraint_type
UNION ALL
-- 외래키
SELECT 2, k.table_name,
CONCAT('ALTER TABLE ', k.table_name, ' ADD CONSTRAINT ', k.constraint_name,
' FOREIGN KEY (', GROUP_CONCAT(k.column_name ORDER BY k.ordinal_position),
') REFERENCES ', k.referenced_table_name,
' (', GROUP_CONCAT(k.referenced_column_name ORDER BY k.ordinal_position), ')',
IF(r.delete_rule = 'CASCADE', ' ON DELETE CASCADE', ''), ';')
FROM information_schema.key_column_usage k
JOIN information_schema.referential_constraints r
ON r.constraint_schema = k.constraint_schema AND r.constraint_name = k.constraint_name
WHERE k.table_schema = DATABASE() AND k.referenced_table_name IS NOT NULL
GROUP BY k.table_name, k.constraint_name, k.referenced_table_name, r.delete_rule
UNION ALL
-- 제약조건이 아닌 일반 인덱스
SELECT 3, s.table_name,
CONCAT('CREATE ', IF(s.non_unique = 0, 'UNIQUE ', ''), 'INDEX ', s.index_name,
' ON ', s.table_name, ' (', GROUP_CONCAT(s.column_name ORDER BY s.seq_in_index), ');')
FROM information_schema.statistics s
WHERE s.table_schema = DATABASE() AND s.index_name <> 'PRIMARY'
AND s.index_name NOT IN (SELECT constraint_name FROM information_schema.table_constraints
WHERE table_schema = DATABASE())
GROUP BY s.table_name, s.index_name, s.non_unique
) x
ORDER BY ord, tn;
EOF
cat > gen_seq_pg.sql <<'EOF'
SET SESSION information_schema_stats_expiry = 0; -- 캐시된 AUTO_INCREMENT 말고 지금 값을 읽음
SELECT CONCAT('SELECT setval(pg_get_serial_sequence(''', c.table_name, ''', ''', c.column_name, '''), ',
t.auto_increment, ', false);')
FROM information_schema.columns c
JOIN information_schema.tables t
ON t.table_schema = c.table_schema AND t.table_name = c.table_name
WHERE c.table_schema = DATABASE()
AND c.extra LIKE '%auto_increment%';
EOF
mysql -N -B --raw < gen_cons_pg.sql > pg_03_constraints.sql
mysql -N -B --raw < gen_seq_pg.sql > pg_04_identity.sql
cat pg_03_constraints.sql pg_04_identity.sql결과 해석: 실습 출력은 emp_m.empno, emp_t.empno, orders.order_id의 PK 3개와 아래 번호 보정 구문임
이 소스에는 FK, 일반 인덱스가 없어 해당 생성 분기의 실제 적용 결과는 기재 없음
SELECT setval(pg_get_serial_sequence('orders', 'order_id'), 1010, false);결과 해석: 1010은 소스의 다음 AUTO_INCREMENT 값이며 false는 다음 nextval이 지정값을 반환하도록 하는 설정임
기존 번호 적재와 후속 번호 보정은 별도 작업임
information_schema_stats_expiry를 0으로 설정해 캐시된 통계 대신 현재 AUTO_INCREMENT 값을 읽음
기본 통계 캐시 만료 시간은 실습 설명에서 86400초로 제시됨
자료는 생성기를 다른 DB에 그대로 재사용할 수 있다고 설명하지만 여기서 확인된 적용은 해당 실습 구조에 한정됨
FK 생성은 DELETE CASCADE만 반영하며 일반 인덱스 이름, 구성은 별도 점검 대상임
여러 테이블에 같은 인덱스 이름을 쓸 수 있는 MySQL과 달리 PostgreSQL은 스키마 전체에서 인덱스 이름 충돌을 확인해야 함
#7 데이터 이행 묶음 적용과 건수 비교
cat > mig_data.sh <<'EOF'
#!/bin/bash
set -e
PSQL="psql -h <HOST> -U <USERNAME> -d <TARGET_DB_NAME> -v ON_ERROR_STOP=1"
echo "[1] MySQL에서 생성문·데이터 만들기"
mysql -N -B --raw < gen_ddl_pg.sql > pg_01_tables.sql
mysql -N -B --raw < gen_cons_pg.sql > pg_03_constraints.sql
mysql -N -B --raw < gen_seq_pg.sql > pg_04_identity.sql
./export_tsv.sh
echo "[2] PostgreSQL에 적용"
$PSQL -q -f pg_01_tables.sql
$PSQL -f pg_02_load.sql
$PSQL -q -f pg_03_constraints.sql
$PSQL -f pg_04_identity.sql
echo "[완료]"
EOF
chmod +x mig_data.sh
./mig_data.sh결과 해석: COPY 4, 14, 6, 6, 2, 9, 9와 setval 1010이 출력된 기록임
첫 실행의 table does not exist, skipping NOTICE는 삭제할 테이블이 없다는 안내임
cat > both_count.sql <<'EOF'
SELECT 'dept' AS table_name, COUNT(*) AS cnt FROM dept
UNION ALL SELECT 'emp', COUNT(*) FROM emp
UNION ALL SELECT 'emp_m', COUNT(*) FROM emp_m
UNION ALL SELECT 'emp_t', COUNT(*) FROM emp_t
UNION ALL SELECT 'emp_t_audit', COUNT(*) FROM emp_t_audit
UNION ALL SELECT 'order_hist', COUNT(*) FROM order_hist
UNION ALL SELECT 'orders', COUNT(*) FROM orders
ORDER BY 1;
SELECT SUM(amount) AS sum_amount FROM orders;
SELECT SUM(sal) AS sum_sal FROM emp_m;
EOF
echo "===== MySQL ====="
mysql -t < both_count.sql
echo "===== PostgreSQL ====="
psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -f both_count.sql| 비교 항목 | MySQL | PostgreSQL |
|---|---|---|
| 테이블별 COUNT | 4 / 14 / 6 / 6 / 2 / 9 / 9 | 4 / 14 / 6 / 6 / 2 / 9 / 9 |
| orders 금액 합계 | 700000.0000000000 | 700000.0000000000 |
| emp_m 급여 합계 | 15975.0000000000 | 15975.0000000000 |
결과 해석: 같은 SQL 파일을 양쪽에서 조회한 실습 기록임
이 단계는 데이터 비교임
주문 이력과 리포트 호출은 저장 프로그램을 적용한 뒤 확인함
#8 상품 테이블 단독 이행 연습
DROP TABLE IF EXISTS products;
CREATE TABLE products
( prod_id INT NOT NULL AUTO_INCREMENT,
prod_name VARCHAR(50) NOT NULL,
category VARCHAR(20),
price DECIMAL(10,0),
stock INT,
reg_date DATETIME,
PRIMARY KEY (prod_id)
);
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('갤럭시 버즈3', '전자', 189000, 40, '2026-09-01 09:10:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('아이패드 에어', '전자', 929000, 12, '2026-09-01 10:20:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('로지텍 마우스', '전자', 39000, 85, '2026-09-02 11:00:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('기계식 키보드', '전자', 129000, 30, '2026-09-02 14:30:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('27인치 모니터', '전자', 349000, 15, '2026-09-03 09:00:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('USB-C 충전기', '전자', 29000, 120, '2026-09-03 16:45:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('SQL 200제', '도서', 28000, 60, '2026-09-04 10:00:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('친절한 SQL 튜닝', '도서', 36000, 45, '2026-09-04 13:20:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('오라클 19c 관리', '도서', 42000, 25, '2026-09-05 09:30:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('PostgreSQL 입문', '도서', 32000, NULL, '2026-09-05 15:10:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('리눅스 첫걸음', '도서', 25000, 50, '2026-09-06 11:40:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('파이썬 데이터분석', '도서', 34000, 38, '2026-09-06 17:00:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('제주 감귤 5kg', '식품', 27000, 70, '2026-09-07 08:50:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('유기농 우유 1L', '식품', 3500, 200, '2026-09-07 09:15:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('햇반 24개입', '식품', 23900, 90, '2026-09-08 10:05:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('커피 원두 1kg', '식품', 31000, 35, '2026-09-08 13:30:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('견과류 선물세트', '식품', 45000, 20, '2026-09-09 10:10:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('생수 2L 12병', '식품', 9900, 150, '2026-09-09 14:00:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('후드 티셔츠', '의류', 49000, 55, '2026-09-10 09:40:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('청바지', '의류', 59000, 40, '2026-09-10 12:00:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('경량 패딩', '의류', 89000, 22, '2026-09-11 10:30:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('운동화', '의류', 99000, 30, '2026-09-11 15:20:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('니트 가디건', '의류', 69000, 18, '2026-09-12 11:00:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('양말 10족', '의류', 12900, 100, '2026-09-12 16:30:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('원목 책상', '가구', 259000, 8, '2026-09-13 10:00:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('사무용 의자', '가구', 189000, 14, '2026-09-13 13:45:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('3단 책장', '가구', 79000, 20, '2026-09-14 09:20:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('스탠드 조명', '가구', 45000, 33, '2026-09-14 14:10:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('수납 선반', '가구', 39000, 27, '2026-09-15 10:50:00');
INSERT INTO products (prod_name, category, price, stock, reg_date) VALUES ('접이식 테이블', '가구', 55000, 16, '2026-09-15 17:30:00');
CREATE UNIQUE INDEX uk_products_name ON products (prod_name);
CREATE INDEX idx_products_cat_price ON products (category, price);
CREATE INDEX idx_products_regdate ON products (reg_date);
SELECT COUNT(*) FROM products;
SHOW INDEX FROM products;
EXIT결과 해석: 준비 입력은 30건이며 PostgreSQL 입문 상품의 stock 한 건은 NULL임
PK 1개, 유니크 1개, 일반 인덱스 2개를 확인하는 구성임
-- mysql 에서
SHOW CREATE TABLE products\G
SELECT index_name, non_unique, seq_in_index, column_name
FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = 'products'
ORDER BY index_name, seq_in_index;
SET SESSION information_schema_stats_expiry = 0;
SELECT auto_increment FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'products';
EXIT결과 해석: 정답 기준은 인덱스 4개, 컬럼 구성 5행, AUTO_INCREMENT 다음 값 31임
idx_products_cat_price는 category → price 순서의 결합 인덱스임
mkdir -p data
mysql -N -B -e "SELECT prod_id, prod_name, category, price, stock, reg_date FROM products ORDER BY prod_id" > data/products.tsv
wc -l data/products.tsv
grep -n NULL data/products.tsv결과 해석: 정답 설명의 파일 행 수는 30, NULL이 있는 데이터 행은 한 줄임
열 이름을 명시해 추출하고 타깃 열 순서와 맞춤
cat > q4_products.sql <<'EOF'
DROP TABLE IF EXISTS products CASCADE;
CREATE TABLE products (
prod_id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
prod_name VARCHAR(50) NOT NULL,
category VARCHAR(20),
price NUMERIC(10,0),
stock INTEGER,
reg_date TIMESTAMP(0)
);
\copy products FROM 'data/products.tsv' WITH (FORMAT text, NULL 'NULL')
ALTER TABLE products ADD PRIMARY KEY (prod_id);
ALTER TABLE products ADD CONSTRAINT uk_products_name UNIQUE (prod_name);
CREATE INDEX idx_products_cat_price ON products (category, price);
CREATE INDEX idx_products_regdate ON products (reg_date);
SELECT setval(pg_get_serial_sequence('products', 'prod_id'), 31, false);
EOF
psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -v ON_ERROR_STOP=1 -f q4_products.sql결과 해석: 정답 확인 기준은 COPY 30과 setval 31임
유니크를 제약조건으로 만들면 해당 유니크 인덱스가 함께 생성됨
독립 UNIQUE INDEX로 만들어도 중복을 막지만 제약조건 사전에서 보이는 구성은 다름
cat > q4_check.sql <<'EOF'
SELECT COUNT(*) AS cnt, SUM(price) AS sum_price, SUM(stock) AS sum_stock,
COUNT(*) - COUNT(stock) AS null_stock
FROM products;
SELECT category, COUNT(*) AS cnt, SUM(price) AS sum_price
FROM products GROUP BY category ORDER BY category;
EOF
echo "===== MySQL ====="; mysql -t < q4_check.sql
echo "===== PostgreSQL ====="; psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -f q4_check.sql
psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>' -c "SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'products' ORDER BY 1"| 확인 항목 | 정답 기준 |
|---|---|
| 건수 / 가격 합계 / 재고 합계 / 재고 NULL | 30 / 3045200 / 1468 / 1 |
| 가구 / 도서 / 식품 / 의류 / 전자 | 각 6건 |
| 카테고리별 가격 합계 | 666000 / 197000 / 140300 / 377900 / 1664000 |
| 타깃 인덱스 | idx_products_cat_price, idx_products_regdate, products_pkey, uk_products_name |
결과 해석: SUM(stock)은 NULL을 빼고 더하므로 NULL이 0으로 바뀌어도 합계는 같을 수 있음
NULL 건수를 별도 비교하고 pg_indexes에서 인덱스 구성과 열 순서를 확인함
-- psql -h '<HOST>' -U '<USERNAME>' -d '<TARGET_DB_NAME>'
BEGIN;
INSERT INTO products (prod_name, category, price, stock, reg_date)
VALUES ('테스트 상품', '기타', 1000, 1, now()) RETURNING prod_id;
INSERT INTO products (prod_name, category, price, stock, reg_date)
VALUES ('SQL 200제', '도서', 28000, 1, now());
ROLLBACK;결과 해석: 정답 설명은 첫 입력의 prod_id 31과 두 번째 입력의 uk_products_name 중복 오류를 제시함
오류 뒤 트랜잭션은 ROLLBACK으로 끝내는 구성임
#사용 조건과 주의사항
IDENTITY 통계 캐시를 끄는 작업과 이행 중 동시 주문을 통제하는 작업은 별도임
실제 동시 변경 통제, 전체 열 값의 보존, 모든 타입의 TSV 호환 결과는 확인 불가
번호 보정 1010과 이후 트리거 테스트에서 1010을 소비한 기록이 있으므로 첫 업무 주문 1011과 구분함
정상 주문 뒤 주문, 이력 각 10건과 리포트 합계 733000은 후속 호출 결과이며 이행 직후의 주문 9건, 합계 700000과 비교 시점이 다름
실습의 건수, 합계 비교를 모든 행, 열의 완전 일치나 운영 전환 완료로 확대하지 않음