본문으로 건너뛰기

18.2 MySQL → PostgreSQL

MySQL/MariaDB에서 PostgreSQL로의 마이그레이션은 Oracle보다 단순하지만 데이터 모델 차이·정확성·트랜잭션의 PostgreSQL 표준에 맞춰 코드 검토가 필요합니다. 차이점과 표준 절차를 정리합니다.

주요 차이점​

영역MySQLPostgreSQL
트랜잭션InnoDB만 ACID, MyISAM은 X모든 테이블 ACID
격리 수준 기본Repeatable ReadRead Committed
식별자backtick `표준 더블따옴표 "
대소문자일반적으로 대소문자 무시 (collation)구분 (citext 또는 LOWER)
AUTO_INCREMENT컬럼 옵션bigserial 또는 GENERATED ALWAYS AS IDENTITY
UNSIGNED int가능없음 — bigint로
TINYINT(1) boolean관행boolean 타입
DATETIMEtimezone 없음timestamptz
0000-00-00 같은 invalid date가능불가 — sql_mode 검토
ENUM컬럼 옵션CREATE TYPE ... AS ENUM
spatial기본PostGIS extension
JSON / JSONBJSON 타입JSONB (더 빠름·인덱싱 강력)
ReplicationbinlogWAL

도구 — pgloader​

가장 표준입니다. Common Lisp 작성, 매우 빠릅니다.

sudo dnf install -y pgloader
# 또는 Docker
docker run --rm dimitri/pgloader pgloader ...

기본 사용​

pgloader mysql://root:secret@mysql.example.com/app_db \
postgresql://pgadmin:secret@pg.example.com/app_db

한 줄로 스키마·데이터 모두 마이그레이션. 작은~중간 DB는 즉시.

상세 설정 — .load 파일​

LOAD DATABASE
FROM mysql://root:secret@mysql.example.com/app_db
INTO postgresql://pgadmin:secret@pg.example.com/app_db

WITH include drop, create tables, create indexes, reset sequences,
workers = 8, concurrency = 1,
multiple readers per thread, rows per range = 50000

CAST type tinyint when (= precision 1) to boolean using tinyint-to-boolean,
type datetime to timestamptz drop default drop not null,
type date drop not null drop default using zero-dates-to-null,
type int with unsigned to bigint

INCLUDING ONLY TABLE NAMES MATCHING ~/^orders/, ~/^users/

ALTER SCHEMA 'app_db' RENAME TO 'public'

BEFORE LOAD DO
$$ SET search_path TO public; $$,
$$ CREATE EXTENSION IF NOT EXISTS citext; $$;

CAST 절로 타입 변환 규칙 명시합니다. BEFORE LOAD DO로 확장 활성화합니다.

ora2pg도 MySQL 지원​

ORACLE_DSN dbi:mysql:database=app_db;host=mysql.example.com

ora2pg가 MySQL도 source로 지원합니다. 평가 report도 동일하게 활용합니다.

CDC — 무중단​

대용량 마이그레이션:

Debezium MySQL connector → Kafka → JDBC sink. 또는 AWS DMS·Google DMS도 동일.

자주 마주치는 타입 변환​

TINYINT(1) → boolean​

-- MySQL
is_active TINYINT(1) DEFAULT 0

-- PostgreSQL
is_active boolean DEFAULT false

pgloader가 자동 변환합니다.

0000-00-00 잘못된 날짜​

MySQL의 허용된 잘못된 날짜가 PG에서는 INSERT 실패. pgloader가 NULL로 변환합니다.

USING zero-dates-to-null

UNSIGNED int​

-- MySQL
user_id INT UNSIGNED

-- PostgreSQL
user_id bigint -- unsigned int 없음, 더 큰 타입으로

ENUM​

-- MySQL
status ENUM('active','inactive')

-- PostgreSQL
CREATE TYPE status_enum AS ENUM ('active', 'inactive');
status status_enum

또는 lookup table(14.1 참고)로 변환합니다.

JSON → JSONB​

-- MySQL
data JSON

-- PostgreSQL
data JSONB -- 권장. JSON도 가능하지만 JSONB가 거의 항상 답

식별자 quoting​

MySQL의 `column`은 PG의 "column". application 코드 점검합니다.

-- MySQL
SELECT `user_id` FROM `users`;

-- PostgreSQL
SELECT user_id FROM users; -- 일반적으로 quoting 불필요
SELECT "user_id" FROM "users"; -- 필요시

case-sensitivity 함정​

MySQL은 기본적으로 WHERE email = 'A@B.com'이 a@b.com과 매칭. PG는 다름.

-- MySQL 동작 → PG에서
WHERE LOWER(email) = LOWER('A@B.com')

-- 또는 citext 컬럼
ALTER TABLE users ALTER COLUMN email TYPE citext;

AUTO_INCREMENT → IDENTITY​

-- MySQL
id INT AUTO_INCREMENT PRIMARY KEY

-- PostgreSQL (구식)
id SERIAL PRIMARY KEY

-- PostgreSQL (PG 10+, SQL 표준, 권장)
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY

pgloader가 자동 변환합니다. IDENTITY가 더 표준·안전합니다.

REPLACE INTO → ON CONFLICT​

-- MySQL
REPLACE INTO users (id, name) VALUES (1, 'alice');
INSERT IGNORE INTO users ...;
INSERT ... ON DUPLICATE KEY UPDATE ...;

-- PostgreSQL
INSERT INTO users (id, name) VALUES (1, 'alice')
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;

INSERT ... ON CONFLICT DO NOTHING;

LIMIT 문법​

같습니다. MySQL의 LIMIT offset, count 형식은 PG에서 안 됩니다.

-- MySQL
LIMIT 10, 20

-- PostgreSQL
LIMIT 20 OFFSET 10

검증​

# row count
mysql -e "SELECT 'orders', COUNT(*) FROM orders" -B
psql -c "SELECT 'orders', COUNT(*) FROM orders"

# checksum
mysql -e "SELECT MD5(GROUP_CONCAT(...)) FROM ..." -B

큰 테이블은 MD5 합계·sum·min/max으로 비교합니다.

cutover 절차​

Oracle과 동일 (18.1):

  1. 초기 데이터 마이그레이션 (pgloader 또는 CDC)
  2. CDC 활성화
  3. application read 점진 PG로
  4. write cut-over
  5. MySQL은 read-only 관찰 기간
  6. decommission

ORM 측면​

ORMPG·MySQL 차이
Djangoengine 변경. 일부 쿼리 (__icontains 등) 검증
Rails (ActiveRecord)adapter 변경. like/ilike 차이
Sequelizedialect 변경. ENUM·JSON 처리 검증
TypeORM같음
Hibernatedialect 변경
SQLAlchemyURL 변경. 일부 reflection 차이

코드 변경은 ORM이 잘 추상화하지만 raw SQL 부분만 수동으로 처리합니다.

정리​

pgloader가 MySQL에서 PostgreSQL로 옮기는 표준 도구이고 자동 변환이 강력해서, 작은~중간 DB는 한 줄로 끝납니다. 대용량은 Debezium·DMS로 CDC를 씁니다.

  • 자주 변환: TINYINT(1)→bool, UNSIGNED→bigint, ENUM→TYPE/lookup, JSON→JSONB
  • case-sensitivity, REPLACE INTO 등 application 코드를 점검합니다
  • ORM은 대부분 추상화하고 raw SQL만 수동 확인합니다