데이터베이스 연구실.log

Oracle에서 MySQL로 이행

MySQL용 구조 생성과 CSV 적재, 타입, 식별자, NULL, 채번의 차이 및 저장 프로그램 미이행 범위

LOAD DATA LOCAL INFILEAUTO_INCREMENTERROR 3948my.cnfMySQL 8.4LOAD DATA LOCALWarnings

#핵심 요약

실습에서는 Oracle 사전으로 MySQL용 DDL, LOAD DATA, 제약조건을 생성하고 시퀀스 사용처는 트리거 소스를 읽어 식별함

적재 건수뿐 아니라 LOAD DATA의 경고와 실제 값, 한글, 날짜, 금액을 대조함

독립 시퀀스 대신 AUTO_INCREMENT를 적용하는 실습 예이며 AFTER 트리거와 저장 프로그램은 별도 재작성 대상임

#개념과 동작 원리

항목 실습의 MySQL 8.4 처리
Oracle DATE DATETIME으로 시, 분, 초 보존
TIMESTAMP 계열 DATETIME(6) 매핑
정밀도 없는 NUMBER 기본키 AUTO_INCREMENT 적용을 위한 BIGINT 선택
정밀도 없는 다른 NUMBER DECIMAL(38,10) 선택, 실제 값의 소수 범위 확인 요구
VARCHAR2, NVARCHAR2 VARCHAR의 문자 길이 지정
CLOB, BLOB LONGTEXT, LONGBLOB
식별자 소문자와 역따옴표 사용, 실제 대소문자는 lower_case_table_names 확인
기본키 실습 제약 이름 대신 PRIMARY로 표시
외래키 부모 참조 컬럼과 순서를 명시
문자열 길이 문자 수는 CHAR_LENGTH, 바이트 수는 LENGTH로 Oracle 함수와 구분

타입 매핑은 해당 실습 생성기의 선택임
정밀도 없는 NUMBER를 BIGINT, DECIMAL(38,10)으로 바꾸는 것이 모든 실제 값을 보존하는지는 수업 자료에 검증 결과가 없음
타입 범위 확인용으로 제시한 암묵적 문자열 길이 식도 완전한 정밀도 검증으로 채택하지 않음

CSV 적재문은 컬럼별 사용자 변수와 NULLIF로 빈 필드를 NULL로 바꿈
LOAD DATA LOCAL INFILE의 구분자, 인용 문자, 이스케이프, 줄 구분 설정을 생성하고 서버 local_infile과 클라이언트 허용 설정을 함께 확인하는 흐름임

#필수 명령어 또는 SQL

#접속 정보 파일

파일 (~/.my.cnf, 권한 600)객체 또는 설정을 변경함
[client]
host=127.0.0.1
port=3306
user=<USERNAME>
password=<PASSWORD>
default-character-set=utf8mb4

[mysql]
database=<TARGET_DB_NAME>
local-infile=1
oracle 계정 셸객체 또는 설정을 변경함
chmod 600 ~/.my.cnf
mysql -e "SELECT CURRENT_USER(), DATABASE(), @@character_set_client"
mysql -e "SHOW TABLES"

결과 해석:

PostgreSQL은 PGPASSWORD로, MySQL은 ~/.my.cnf로 반복되는 -u -p 입력을 줄임
비밀번호가 든 파일이라 chmod 600으로 막음

default-character-set=utf8mb4가 빠지는 등 문자셋이 어긋나면 한글이 깨질 수 있음

#1. 테이블 생성문 (기본키 여부를 반영한 타입 매핑)

SQL*Plus (<USERNAME>, gen_ddl.sql)조회만 함
@/home/oracle/mig/my/set_gen.sql
SPOOL /home/oracle/mig/my/my_01_tables.sql
SELECT 'SET FOREIGN_KEY_CHECKS = 0;' FROM dual;
SELECT 'DROP TABLE IF EXISTS `' || LOWER(t.table_name) || '`;' || CHR(10) ||
       'CREATE TABLE `' || LOWER(t.table_name) || '` (' || CHR(10) ||
       LISTAGG('  `' || LOWER(c.column_name) || '` ' ||
         CASE
           WHEN c.data_type = 'NUMBER' AND c.data_precision IS NULL AND c.pk_col = 'Y' THEN 'BIGINT'
           WHEN c.data_type = 'NUMBER' AND c.data_precision IS NULL THEN 'DECIMAL(38,10)'
           WHEN c.data_type = 'NUMBER' AND c.data_scale = 0 AND c.data_precision <= 9  THEN 'INT'
           WHEN c.data_type = 'NUMBER' AND c.data_scale = 0 AND c.data_precision <= 18 THEN 'BIGINT'
           WHEN c.data_type = 'NUMBER' AND c.data_scale = 0 THEN 'DECIMAL(' || c.data_precision || ')'
           WHEN c.data_type = 'NUMBER'                     THEN 'DECIMAL(' || c.data_precision || ',' || c.data_scale || ')'
           WHEN c.data_type IN ('VARCHAR2','NVARCHAR2')    THEN 'VARCHAR(' || c.char_length || ')'
           WHEN c.data_type IN ('CHAR','NCHAR')            THEN 'CHAR(' || c.char_length || ')'
           WHEN c.data_type = 'DATE'                       THEN 'DATETIME'
           WHEN c.data_type LIKE 'TIMESTAMP%'              THEN 'DATETIME(6)'
           WHEN c.data_type IN ('FLOAT','BINARY_DOUBLE')   THEN 'DOUBLE'
           WHEN c.data_type = 'CLOB'                       THEN 'LONGTEXT'
           WHEN c.data_type = 'BLOB'                       THEN 'LONGBLOB'
           ELSE 'LONGTEXT /* 확인 필요: ' || c.data_type || ' */'
         END ||
         CASE WHEN c.nullable = 'N' THEN ' NOT NULL' END,
       ',' || CHR(10)) WITHIN GROUP (ORDER BY c.column_id)
       || CHR(10) || ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;' || CHR(10)
  FROM user_tables t
  JOIN (SELECT tc.table_name, tc.column_name, tc.column_id, tc.data_type,
               tc.data_precision, tc.data_scale, tc.char_length, tc.nullable,
               CASE WHEN EXISTS (SELECT 1
                                   FROM user_constraints k
                                   JOIN user_cons_columns kc ON kc.constraint_name = k.constraint_name
                                  WHERE k.constraint_type = 'P'
                                    AND kc.table_name  = tc.table_name
                                    AND kc.column_name = tc.column_name)
                    THEN 'Y' ELSE 'N' END AS pk_col
          FROM user_tab_columns tc) c
    ON c.table_name = t.table_name
 WHERE t.table_name <> 'MIG_CHK' AND t.dropped = 'NO'
 GROUP BY t.table_name
 ORDER BY t.table_name;
SELECT 'SET FOREIGN_KEY_CHECKS = 1;' FROM dual;
SPOOL OFF
EXIT
oracle 계정 셸객체 또는 설정을 변경함
sqlplus -s <USERNAME>/<PASSWORD> @gen_ddl.sql
cat my_01_tables.sql
mysql < my_01_tables.sql
mysql -e "DESC orders"

결과 해석:

TIMESTAMP를 MySQL TIMESTAMP가 아니라 DATETIME(6)으로 바꾸는 것은 MySQL TIMESTAMP의 2038년 1월 19일 상한을 피하기 위해서임

정밀도 없는 NUMBER를 DECIMAL(38,10)로 바꾸면 금액 1000이 1000.0000000000으로 보이므로 실제 소수 자릿수를 확인하고 타입을 정함
기본키만 BIGINT로 둔 것은 뒤에서 AUTO_INCREMENT를 걸기 위해서이며, DECIMAL 컬럼에는 AUTO_INCREMENT를 걸 수 없음

식별자를 역따옴표로 감싸 rank, order 같은 예약어와 부딪치지 않게 함
대소문자를 구분하는 환경에서 소문자로 만든 orders를 ORDERS로 조회하면 Table '<TARGET_DB_NAME>.ORDERS' doesn't exist가 날 수 있어 애플리케이션 SQL의 표기도 맞춰야 함

#2. LOAD DATA 적재

SQL*Plus (<USERNAME>, gen_load.sql)조회만 함
@/home/oracle/mig/my/set_gen.sql
SPOOL /home/oracle/mig/my/my_02_load.sql
SELECT 'LOAD DATA LOCAL INFILE ''/home/oracle/mig/my/data/' || LOWER(t.table_name) || '.csv''' || CHR(10) ||
       '  INTO TABLE `' || LOWER(t.table_name) || '`' || CHR(10) ||
       '  CHARACTER SET utf8mb4' || CHR(10) ||
       q'[  FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '']' || CHR(10) ||
       q'[  LINES TERMINATED BY '\n']' || CHR(10) ||
       '  (' || LISTAGG('@' || LOWER(c.column_name), ', ') WITHIN GROUP (ORDER BY c.column_id) || ')' || CHR(10) ||
       '  SET ' || LISTAGG('`' || LOWER(c.column_name) || '` = NULLIF(@' || LOWER(c.column_name) || ', '''')', ',' || CHR(10) || '      ')
                   WITHIN GROUP (ORDER BY c.column_id) || ';' || CHR(10)
  FROM user_tables t
  JOIN user_tab_columns c ON c.table_name = t.table_name
 WHERE t.table_name <> 'MIG_CHK' AND t.dropped = 'NO'
 GROUP BY t.table_name
 ORDER BY t.table_name;
SPOOL OFF
EXIT
mysql (root)객체 또는 설정을 변경함
SHOW GLOBAL VARIABLES LIKE 'local_infile';
SET PERSIST local_infile = ON;
mysql (<USERNAME>)객체 또는 설정을 변경함
SOURCE /home/oracle/mig/my/my_02_load.sql
SHOW WARNINGS;
SELECT * FROM orders ORDER BY order_id;

결과 해석:

q'[ ... ]'는 Oracle 대체 따옴표 문법이라 안쪽 작은따옴표를 반복해 이스케이프하지 않아도 됨

실습 기록: 처음 SOURCE는 ERROR 3948 (42000): Loading local data is disabled; this must be enabled on both the client and server sides로 실패했음
클라이언트는 ~/.my.cnf의 local-infile=1로 켜 두었고, root로 SET PERSIST local_infile = ON(재기동해도 유지)을 한 뒤 다시 실행하자 테이블마다 Records: 8 Deleted: 0 Skipped: 0 Warnings: 0 같은 결과가 나왔음

Warnings가 0이 아니면 바로 SHOW WARNINGS로 원인을 보고 실제 적재값을 확인함

#3. 제약조건, AUTO_INCREMENT

SQL*Plus (<USERNAME>, gen_cons.sql의 외래키 부분)조회만 함
SELECT 'ALTER TABLE `' || LOWER(c.table_name) || '` ADD CONSTRAINT `' || LOWER(c.constraint_name) ||
       '` FOREIGN KEY (' ||
       LISTAGG('`' || LOWER(cc.column_name) || '`', ', ') WITHIN GROUP (ORDER BY cc.position) ||
       ') REFERENCES `' || LOWER(p.table_name) || '` (' ||
       LISTAGG('`' || LOWER(pc.column_name) || '`', ', ') WITHIN GROUP (ORDER BY cc.position) || ')' ||
       DECODE(c.delete_rule, 'CASCADE', ' ON DELETE CASCADE', 'SET NULL', ' ON DELETE SET NULL', '') || ';'
  FROM user_constraints c
  JOIN user_cons_columns cc ON cc.constraint_name = c.constraint_name
  JOIN user_constraints  p  ON p.constraint_name  = c.r_constraint_name
  JOIN user_cons_columns pc ON pc.constraint_name = p.constraint_name AND pc.position = cc.position
 WHERE c.constraint_type = 'R'
 GROUP BY c.table_name, c.constraint_name, p.table_name, c.delete_rule;
SQL*Plus (<USERNAME>, 시퀀스 사용처)조회만 함
SELECT sequence_name, last_number, increment_by, cache_size FROM user_sequences;

SELECT d.name AS trigger_name, t.table_name, d.referenced_name AS sequence_name
  FROM user_dependencies d
  JOIN user_triggers t ON t.trigger_name = d.name
 WHERE d.referenced_type = 'SEQUENCE';

SELECT line, text FROM user_source WHERE name = 'TRG_ORDERS_BI' ORDER BY line;
mysql (<USERNAME>)객체 또는 설정을 변경함
SELECT MAX(order_id) FROM orders;
ALTER TABLE orders MODIFY order_id BIGINT NOT NULL AUTO_INCREMENT;
SHOW CREATE TABLE orders\G
SHOW INDEX FROM orders;
ALTER TABLE orders MODIFY reg_date DATETIME DEFAULT CURRENT_TIMESTAMP;

결과 해석:

기본키, 유니크, 일반 인덱스 생성 부분은 PostgreSQL 생성기와 같은 구조이고, 기본키는 이름을 붙이지 않고 ADD PRIMARY KEY로 만듦(MySQL 기본키 이름은 늘 PRIMARY라 Oracle의 SYS_C... 이름에 기대던 운영 스크립트는 고쳐야 함)

MySQL 외래키는 부모 참조 컬럼을 반드시 적어야 해 부모 제약 컬럼을 같은 position으로 조인함

CLOB에서 바꾼 LONGTEXT 컬럼에 일반 인덱스를 걸려면 접두 길이를 지정해야 함

MySQL 8.4에는 독립 시퀀스 객체가 없어, 데이터 사전 의존성으로 orders의 트리거가 ord_seq를 쓰는 것까지 찾은 뒤 트리거 소스의 :NEW.order_id := ord_seq.NEXTVAL을 읽어 어느 컬럼인지 정함

AUTO_INCREMENT를 건 뒤 SHOW CREATE TABLE 끝의 AUTO_INCREMENT= 값이 최대값 + 1인지 봄
더 크게 시작하려면 ALTER TABLE orders AUTO_INCREMENT = 2000처럼 주며 기존 최대값 이하로는 못 내림

#4. 대조

SQL*Plus (<USERNAME>)조회만 함
SELECT COUNT(*), SUM(amount), MIN(reg_date), MAX(reg_date) FROM orders;
SELECT order_id, cust_name, LENGTH(cust_name) AS chars, LENGTHB(cust_name) AS bytes
  FROM orders ORDER BY order_id;
mysql (<USERNAME>)객체 또는 설정을 변경함
SELECT COUNT(*), SUM(amount), MIN(reg_date), MAX(reg_date) FROM orders;
SELECT order_id, cust_name, CHAR_LENGTH(cust_name) AS chars, LENGTH(cust_name) AS bytes
  FROM orders ORDER BY order_id;

START TRANSACTION;
INSERT INTO orders (cust_name, amount) VALUES ('<NAME>', 55000);
SELECT LAST_INSERT_ID();
SELECT COUNT(*) FROM order_hist;
ROLLBACK;
oracle 계정 셸조회만 함
mysql -t < both_count.sql

결과 해석:

MySQL에는 INSERT ... RETURNING이 없어 LAST_INSERT_ID()로 방금 발급된 번호를 보고, ROLLBACK해도 AUTO_INCREMENT 값은 되돌아가지 않음

테스트 INSERT에서도 order_hist 건수는 그대로라 AFTER 트리거, 프로시저, 함수, 함수를 쓰는 뷰는 MySQL 저장 프로그램 문법으로 다시 써야 함
함수를 만들 때 나는 ERROR 1418은 함수 선언과 log_bin_trust_function_creators 설정에 따른 조건부 오류라 수업 자료는 설명함

항목 PostgreSQL MySQL
Oracle DATE TIMESTAMP(0) DATETIME
정밀도 없는 NUMBER NUMERIC DECIMAL(38,10), 기본키는 BIGINT
스키마 DB 안의 계정 이름 스키마(search_path) DB = 스키마
적재 \copy가 바로 됨 LOAD DATA LOCAL, 서버 local_infile 필요
잘못된 값 오류와 전체 취소 경고만 내고 적재될 수 있어 Warnings 확인
기본키 이름 Oracle 이름 유지 항상 PRIMARY
시퀀스 CREATE SEQUENCE + setval 없음, 트리거 소스를 읽고 AUTO_INCREMENT
방금 넣은 번호 RETURNING LAST_INSERT_ID()
문자열 길이 LENGTH = 글자 수 LENGTH = 바이트 수, 글자 수는 CHAR_LENGTH

#사용 조건과 주의사항

수업 자료에는 ERROR 3948과 양쪽 LOCAL 허용 설정 확인, 각 적재 직후 SHOW WARNINGS와 실제 값 확인 절차가 있음

LOAD DATA LOCAL은 값 변환, 잘림, 기본값 대입 후 경고만 내고 적재를 계속할 수 있음
오류에 따라 적재가 중단될 수도 있음

AUTO_INCREMENT 대상 컬럼은 의존성 목록과 트리거의 대입문을 함께 확인함
최대 기존 번호와 다음 발급값을 비교함

DEFAULT, AUTO_INCREMENT 대체 후에도 이력 트리거, 프로시저, 함수, 뷰는 미이행 목록에 남음

이 글의 데이터 이행 설명을 MySQL 저장 프로그램 전체 변환 완료로 보지 않음

SEO_HKabsumveritas@gmail.comBuilt with Claude Code & Codex

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

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