HOME/DB MIGRATION

Oracle → PostgreSQL

Oracle에서 PostgreSQL로,
옮기면서 부딪히는 문제 57가지

총 3,697만 행 규모의 레거시 DB를 직접 옮기며 겪은 것과 설계 단계에서 먼저 막아야 하는 것을 영역별로 정리했습니다. 문제마다 증상 · 원인 · 대응을 적었고, 실제로 측정해 확인한 31건에는 실측 표시를 붙였습니다.

Oracle 19cPostgreSQL 17ora2pgoracle_fdwPL/pgSQLRDKit
Oracle 19c EE → PostgreSQL 17 + RDKit 4.7 · 4 vCPU / 12 GiB2026-09 자체 실측
603s2,452만 행 테이블 Oracle → PostgreSQL 이관ora2pg COPY · 4 jobs · 평균 40,675행/초
100%12개 테이블 3,697만 행 정합성 일치행 수 · PK·수치 합계 · 표본 3.3만 행 전 컬럼 비교, 불일치 0
2배구조식(CLOB) 이관 속도 개선292만 건 · ora2pg 약 1,100행/초 → 병렬 경로 2,230행/초
1.5s흔한 부분구조 검색 P95결과 100건 · 목표 3초 이하 충족 · P50 0.19초
0.76s유사도 Top100 검색 P95Morgan 지문 KNN · 목표 1초 이하 충족 · 완전일치 P95 0.21초
4.8배구조 인덱스 생성 시간 단축GiST 35분 47초 → 7분 25초 · 검색 전용 파티션 테이블
637s화합물 292만 건 RDKit 분자 변환PostgreSQL 내부 · 4 병렬 · 2,896,339건 생성
100%오염 데이터 격리 · 오탐 0일부러 심은 오류 10,740건(깨진 SMILES · 비정형 순도값 · 잘못된 코드값) 전부 격리
7종자동 변환 오류 식별ora2pg 평가 B-5 · 수작업 보정 6건 실행까지 검증
36배LONG 컬럼 테이블 이관 지연 발견952행/초 · 일반 테이블 대비, 행 단위 fetch가 원인
17건RDKit이 거부한 원본 구조과원자가 PF6⁻ · 풀러렌 유도체 등 툴킷 간 차이 · 전체의 0.0006%
6.1s희귀 골격 부분구조 검색 P50 · 목표 미달거짓 후보 재확인 비용이 원인 · 메모리 · 지문 크기 · 비동기 검색으로 대책 수립

데이터: 공개 ChEMBL 데이터를 레거시 구조(스키마 3개 · 테이블 16개 · 총 36,975,841행)로 재배치. 다른 작업과 자원을 나눠 쓰는 4 vCPU 서버에서 측정해, 절대값보다 구성 간 비교에 의미가 있습니다.

01 · Index · Partition

인덱스 · 파티션

같은 인덱스라도 두 DB가 찾는 방식이 다릅니다. 이관 뒤 갑자기 풀스캔이 되는 원인은 대부분 여기 있습니다.

전역(GLOBAL) 인덱스 없음

증상
파티션 키 없이 조회하면 모든 파티션의 인덱스를 차례로 훑습니다. 파티션이 많을수록 실행계획 수립 시간까지 늘어납니다.
원인
PostgreSQL 파티션 인덱스는 전부 파티션별(로컬) 인덱스입니다. 전체를 관통하는 단일 인덱스가 없습니다.
대응
조회 조건에 파티션 키를 넣고, 파티션 수를 줄이거나 파티셔닝 자체를 할지 성능을 측정해 결정합니다. 꼭 필요하면 상용판(EDB)을 검토합니다.

파티션 키 없는 UNIQUE · PK 불가

증상
unique constraint on partitioned table must include all partitioning columns 오류. id 하나로 PK나 FK를 걸 수 없습니다.
원인
전역 유니크 인덱스가 없어 파티션을 가로지르는 중복을 막을 수 없습니다.
대응
PK를 (id, 파티션 키)로 넓히거나, 파티션하지 않은 등록 테이블을 따로 두고 유니크·FK를 그쪽에 맡깁니다.

NULL이 인덱스에 들어가는 방식

증상
Oracle에서 IS NULL 조회를 태우려고 만든 (col, 0) 같은 꼼수 인덱스가 그대로 넘어옵니다.
원인
Oracle B-tree는 인덱스 컬럼이 모두 NULL인 행을 저장하지 않고, PostgreSQL은 저장합니다.
대응
꼼수 인덱스는 없애고, NULL을 빼고 싶으면 WHERE col IS NOT NULL 부분 인덱스로 바꿉니다.

복합 UNIQUE와 NULL

증상
Oracle에서 막히던 (1, NULL) 중복 행이 PostgreSQL에서는 들어갑니다. 마스터 데이터를 통합할 때 중복이 조용히 생깁니다.
원인
PostgreSQL은 NULL끼리 서로 다른 값으로 봅니다.
대응
PostgreSQL 15 이상은 NULLS NOT DISTINCT를 쓰되, 모두 NULL인 행까지 막는 차이가 있어 필요하면 부분 유니크 인덱스로 맞춥니다.

한글 정렬과 LIKE 접두 검색실측

증상
name LIKE '아스피%'가 인덱스를 타지 않고 풀스캔이 됩니다. 같은 쿼리인데 이름 목록 순서가 전환 전과 달라집니다.
원인
DB 정렬 규칙이 C가 아니면(한국어 ICU 규칙 포함) 일반 B-tree가 접두 검색에 쓰이지 않습니다. Oracle 바이트 순 정렬과 같은 결과를 내는 건 COLLATE "C"뿐입니다.
대응
text_pattern_ops 인덱스로 한글명 100만 행 기준 64.8ms → 0.06ms를 확인했습니다. 목록 순서를 지켜야 하는 컬럼은 COLLATE "C"를 매핑 정의서에 명시하고, 중간 일치는 pg_trgm GIN 인덱스로 대응합니다.

함수 기반 인덱스

증상
TO_CHAR(reg_date, 'YYYY') 같은 인덱스가 생성 단계에서 실패합니다.
원인
PostgreSQL 표현식 인덱스는 IMMUTABLE 함수만 허용합니다. to_char는 STABLE입니다.
대응
생성 컬럼(GENERATED ALWAYS AS … STORED)에 인덱스를 걸거나 날짜 범위 조건으로 바꿉니다.

비트맵 · IOT · 리버스 키 인덱스

증상
Oracle 전용 인덱스 유형이 변환되지 않거나 일반 B-tree로 바뀌어 성능이 달라집니다.
원인
PostgreSQL에는 저장되는 비트맵 인덱스, 인덱스 구조 테이블, 리버스 키 인덱스가 없습니다.
대응
B-tree 조합과 부분 인덱스·GIN, INCLUDE 커버링 인덱스, 해시 파티션으로 대체합니다.

파티션 테이블의 NOT VALID 외래키실측

증상
큰 테이블에 외래키를 먼저 걸고 나중에 검증하려 했는데, PostgreSQL 17은 파티션 테이블에 NOT VALID 외래키를 허용하지 않습니다.
원인
Oracle·일반 테이블에서 쓰던 “먼저 걸고 나중에 검증” 관례가 파티션 테이블에서는 막혀 있습니다.
대응
적재를 끝낸 뒤 즉시 검증 방식으로 부여합니다. 2,452만 행 기준 21초로 측정해 전환 시간에 넣었습니다.

옵티마이저 힌트

증상
Oracle 힌트로 붙잡아 두던 SQL이 이관 뒤 다른 실행계획으로 갑니다.
원인
PostgreSQL은 /*+ … */ 힌트를 무시합니다.
대응
인덱스와 확장 통계(CREATE STATISTICS)로 다시 잡고, 꼭 필요한 곳만 pg_hint_plan을 씁니다.

02 · Data Types

데이터 타입 · 값

에러 없이 옮겨지지만 값이 달라지는 문제들입니다. 가장 늦게 발견되고 가장 비쌉니다.

빈 문자열 = NULL실측

증상
이관 뒤 애플리케이션이 빈 문자열을 넣기 시작하면 IS NULL 조회가 그 행을 놓치고, NVL 결과가 달라집니다.
원인
Oracle은 빈 문자열을 NULL로 저장하고, PostgreSQL은 둘을 구분합니다.
대응
ora2pg 이관 경로에서 기존 데이터의 의미는 보존되는 것을 확인했습니다. 이관 뒤 새로 들어오는 값은 CHECK (col <> '')로 막고, 코드의 빈 문자열 비교를 전수 검색합니다.

문자열 연결과 NULL

증상
'a' || NULL이 Oracle에서는 'a', PostgreSQL에서는 NULL입니다. 이름·주소 조합 결과가 통째로 비어 나옵니다.
원인
PostgreSQL의 || 연산자는 NULL을 전파합니다.
대응
concat()을 쓰거나 COALESCE로 감쌉니다.

NUMBER(p,s) → double precision실측

증상
ora2pg 자동 변환이 소수 컬럼을 부동소수로 매핑해 정밀도가 깨집니다.
원인
정밀도 지정이 있는 NUMBER를 근사 타입으로 매핑합니다.
대응
ora2pg 설정 PG_NUMERIC_TYPE 0으로 numeric(p,s)를 유지하고, 정수는 integer/bigint로 가려 매핑합니다. 정밀도 없는 NUMBER를 전부 numeric으로 두면 느립니다.

DATE와 SYSDATE

증상
Oracle DATE의 시각이 사라지거나, 날짜 차이가 숫자가 아닌 interval로 나옵니다. 배치 안의 현재 시각이 멈춰 있습니다.
원인
PostgreSQL date는 날짜만 저장합니다. now()는 트랜잭션 시작 시각입니다.
대응
timestamp(0)로 매핑하고, 날짜 연산을 interval 기준으로 고칩니다. ora2pg는 SYSDATE를 statement_timestamp()로 바꿉니다. 실제 현재 시각이 필요하면 clock_timestamp()를 씁니다.

암묵적 형변환실측

증상
트리거의 LPAD(숫자)가 생성은 되지만 실행하면 실패합니다. varchar_id = 12345 같은 비교는 오류가 나거나 인덱스를 못 탑니다.
원인
Oracle은 숫자·문자·날짜를 알아서 바꿔 주지만 PostgreSQL은 하지 않습니다.
대응
명시적 캐스트와 형식 지정(to_date(…, 'YYYYMMDD'))으로 고칩니다.

유니코드 정규화 함정실측

증상
순도값 정리에 NFKC 정규화를 걸었더니 '¼'이 1 ⁄ 4 세 글자로 쪼개져 8,432행이 순도 1%로 저장됐습니다.
원인
정규화 뒤 문자열 일부만 맞아도 숫자로 뽑았습니다.
대응
전체 형식이 맞을 때만 수치로 바꾸고, 격리 대상은 NULL로 두되 원문을 보존하도록 규칙을 고쳤습니다. 전각 >95%는 95로 제대로 복구되는 것을 확인했습니다.

LONG 컬럼실측

증상
LONG 컬럼이 있는 테이블은 이관 속도가 952행/초로, 일반 테이블보다 약 36배 느립니다.
원인
LONG은 행 단위로만 읽을 수 있습니다.
대응
전환 일정에서 LONG 테이블을 따로 떼어 계획하고, 가능하면 원본에서 CLOB으로 먼저 바꿉니다.

CLOB · BLOB실측

증상
ora2pg 기본 경로로 구조식(CLOB) 292만 건을 옮기면 초당 약 1,100행입니다.
원인
LOB을 건별로 가져오는 비용이 큽니다.
대응
문자열로 가져와 COPY로 넣는 병렬 경로로 바꿔 2,230행/초(약 2배)를 측정했습니다. PostgreSQL text·bytea는 값 하나당 1GB가 한계입니다.

ROWID실측

증상
ora2pg가 ROWID를 oid로 바꿔 놓아 실행이 실패합니다.
원인
PostgreSQL 테이블에는 oid 컬럼이 없고, 대응하는 ctid는 수정·VACUUM 때 바뀝니다.
대응
중복 제거처럼 한 문장 안에서 쓰는 곳은 ctid로 다시 쓰고, 로직에서 행을 기억하는 곳은 PK로 바꿨습니다.

문자셋과 식별자 대소문자

증상
KO16MSWIN949 → UTF8에서 변환되지 않는 문자가 나오고, 따옴표로 만든 대문자 이름은 이관 뒤 매번 따옴표가 필요합니다.
원인
Oracle은 이름을 대문자로, PostgreSQL은 소문자로 접습니다.
대응
이관 전에 변환 불가 문자를 스캔하고, 식별자는 소문자로 통일합니다.

03 · SQL

SQL 문법

자동 변환이 가장 많이 건드리는 영역이지만, 결과가 같은지는 따로 대조해야 합니다.

CONNECT BY → WITH RECURSIVE실측

증상
자동 변환 결과가 재귀부에 없는 LEVEL 컬럼을 참조하고 조인 별칭이 모호해 생성부터 실패합니다. 형제 정렬도 부모별로 되지 않습니다.
원인
계층 질의를 재귀 CTE로 옮기면서 깊이·경로·정렬(ORDER SIBLINGS BY) 의미가 빠집니다.
대응
깊이 컬럼과 경로 배열을 직접 전개해 고쳤습니다. Oracle과 결과 행·정렬 순서가 완전히 같은 것을 대조했습니다.

(+) 외부 조인

증상
ANSI 조인으로 바꾼 뒤 결과 건수가 달라집니다.
원인
조건이 ON 절에 있느냐 WHERE 절에 있느냐에 따라 의미가 달라집니다.
대응
LEFT JOIN으로 바꾸고 쿼리별로 결과를 대조합니다.

ROWNUM

증상
WHERE ROWNUM <= 10 ORDER BY x를 LIMIT으로 바꾸면 결과가 달라집니다.
원인
Oracle은 10건을 먼저 자른 뒤 정렬합니다.
대응
LIMIT·ROW_NUMBER로 바꾸되, 원래 의도가 무엇이었는지 확인합니다(원래 버그인 경우가 많습니다).

시노님실측

증상
변환된 시노님이 별도 파일로 나와, 적용 순서가 맞지 않으면 생성 단계에서 실패합니다.
원인
PostgreSQL에는 시노님이 없습니다.
대응
시노님 참조를 스키마 한정명(schema.table)으로 바꿔 순서 의존을 없앴습니다.

DECODE · NVL · DUAL · MINUS · MERGE

증상
Oracle 전용 함수와 구문이 오류를 냅니다.
원인
PostgreSQL 문법에 없는 구문입니다.
대응
CASE · COALESCE · EXCEPT로 바꾸고, MERGE는 PostgreSQL 15 이상 MERGE나 INSERT … ON CONFLICT로 옮깁니다. orafce 확장으로 일부를 그대로 쓸 수도 있습니다.

DB 링크

증상
다른 DB를 참조하는 SQL이 동작하지 않습니다.
원인
Oracle DB 링크에 해당하는 기본 기능이 없습니다.
대응
oracle_fdw · postgres_fdw로 옮깁니다. 전환 기간에 두 DB를 함께 조회해 검증할 때도 씁니다.

04 · PL/SQL → PL/pgSQL

PL/SQL → PL/pgSQL

생성까지 성공하고 실행에서 실패하는 문제가 몰려 있는 영역입니다. 그래서 모든 객체를 실제로 실행해 보는 것을 검증 기준으로 삼습니다.

생성 성공 ≠ 실행 성공실측

증상
트리거·프로시저·패키지가 DDL 적용 단계에서는 전부 성공했지만, 실행하자 실패했습니다.
원인
PL/pgSQL은 함수 본문을 실행할 때 비로소 검사합니다.
대응
DDL 적용 성공이 아니라 모든 PL/pgSQL 객체의 실행 테스트를 검증 기준으로 둡니다. 수작업 보정 6건을 적용하고 실행까지 확인했습니다.

괄호 없는 함수 호출실측

증상
패키지 안에서 인자 없는 함수를 괄호 없이 부른 코드가 실행 중 실패합니다.
원인
PostgreSQL은 괄호 없는 이름을 컬럼으로 해석합니다.
대응
호출부를 전부 찾아 괄호를 붙였습니다.

자율 트랜잭션실측

증상
감사 로그용 PRAGMA AUTONOMOUS_TRANSACTION을 자동 변환이 dblink 자기 접속으로 흉내 냅니다.
원인
PostgreSQL에는 자율 트랜잭션이 없습니다.
대응
호출자가 롤백해도 로그가 남는 동작은 Oracle과 같게 확인했습니다. 다만 호출마다 새 접속을 열고 접속 비밀번호가 코드에 남아, 애플리케이션 로깅으로 다시 설계하는 것을 권합니다.

EXCEPTION 블록 안의 COMMIT실측

증상
예외 처리 블록 안에서 COMMIT하는 프로시저가 실패합니다.
원인
PostgreSQL은 예외 블록(서브트랜잭션) 안에서 트랜잭션을 제어할 수 없습니다.
대응
커밋 지점을 블록 밖으로 빼도록 프로시저 구조를 다시 설계합니다.

패키지

증상
패키지 변수·초기화 블록·오버로딩이 옮겨지지 않습니다.
원인
PostgreSQL에는 패키지가 없습니다.
대응
패키지 하나를 스키마 하나로 묶고, 패키지 변수는 세션 변수(set_config)나 임시 테이블로 옮깁니다.

오류 뒤 트랜잭션 중단

증상
루프 안에서 오류 난 행만 건너뛰던 적재 로직이 첫 오류 뒤 전부 실패합니다.
원인
Oracle은 실패한 문장만 롤백하고, PostgreSQL은 트랜잭션 전체를 중단 상태로 만듭니다.
대응
SAVEPOINT나 ON CONFLICT, 사전 검증 후 격리 테이블로 다시 짭니다. JDBC는 autosave=conservative를 씁니다.

REF CURSOR와 JDBC

증상
커서를 돌려주는 프로시저를 Java에서 부르면 “cursor does not exist” 오류가 납니다.
원인
PostgreSQL refcursor는 트랜잭션이 열려 있어야 읽을 수 있습니다.
대응
호출 구간에서 autocommit을 끄거나 집합 반환 함수로 바꿉니다.

BULK COLLECT · DBMS_JOB · UTL_FILE

증상
Oracle 전용 기능과 내장 패키지가 변환되지 않습니다.
원인
PostgreSQL에 대응 기능이 없습니다.
대응
집합 SQL 한 문장으로 다시 쓰고, 스케줄은 pg_cron, 파일 처리는 애플리케이션으로 옮깁니다.

05 · Migration Run

이관 도구 · 절차

도구가 알려 주지 않는 절차상의 함정입니다. 리허설에서 한 번씩 다 밟았습니다.

전환 난이도 평가실측

증상
어디까지 자동이고 어디부터 사람이 고쳐야 하는지 모른 채 일정을 잡습니다.
원인
저장 프로시저·트리거 규모가 공수를 좌우합니다.
대응
ora2pg 평가 리포트로 먼저 등급을 뽑습니다. 실측 결과 B-5(스키마 1개 기준, 코드 재작성 5인일 이하)였습니다.

시퀀스 값실측

증상
전환 직후 첫 신규 등록부터 PK 중복 오류가 납니다.
원인
ora2pg는 시퀀스의 현재값만 옮기고 테이블의 실제 최대 ID는 확인하지 않습니다.
대응
이관 마지막 단계에서 모든 시퀀스를 테이블 최대값 기준으로 setval합니다.

스키마 동결실측

증상
리허설 사이에 원본 컬럼 길이가 바뀌었는데 타깃 DDL을 다시 뽑지 않아 COPY가 value too long으로 실패했습니다.
원인
원본과 타깃 스키마가 어긋났습니다.
대응
이관 리허설 일정에 스키마 동결 시점을 넣고, 동결 뒤 변경은 DDL 재추출을 거칩니다.

중단한 이관의 잔존 프로세스실측

증상
이관 도구를 멈췄는데 작업 프로세스가 살아남아 같은 데이터를 한 번 더 적재했습니다(2,926,837 → 5,613,674행).
원인
도구의 워커 자식 프로세스는 프로세스 이름이 바뀌어 있어 종료되지 않았고, 대체 경로와 동시에 적재했습니다.
대응
테이블별 행 수 비교로 즉시 탐지해 중복 2,686,837행을 134초에 지우고 재검증했습니다. 중단할 때는 프로세스 그룹 단위로 종료하고 타깃 DB 세션까지 확인합니다.

대량 적재 직후실측

증상
적재 직후 첫 조회에 읽기만 하는데도 페이지 쓰기가 섞였습니다(45,529페이지). 통계도 비어 있어 실행계획이 불안정합니다.
원인
첫 조회가 행 상태 비트를 기록하고, 새로 들어온 행이 모두 같은 시점에 동결 대상이 됩니다.
대응
VACUUM (FREEZE, ANALYZE)를 절차에 넣고(실측 237초 · 144초) 전환 시간 예산에 포함합니다. 성능은 그 뒤에 측정합니다.

중단 시간 줄이기

증상
대용량이면 전환 당일 이관 시간이 업무 중단 시간이 됩니다.
원인
일괄 이관은 데이터 양에 비례해 오래 걸립니다.
대응
미리 옮겨 두고 변경분만 동기화(CDC)하다가 짧게 전환합니다. 2,452만 행 테이블 이관은 603초로 측정했습니다.

06 · Verification

정합성 검증

행 수만 맞춰서는 부족합니다. 비교 규칙부터 정해야 검증 결과를 믿을 수 있습니다.

검증 방법실측

증상
무엇을 비교했는지 모르면 “100% 일치”가 의미가 없습니다.
원인
행 수만 비교하면 값이 틀려도 통과합니다.
대응
테이블별 행 수 · PK 합계 · 수치 컬럼 합계를 비교하고, PK 기준 약 0.1% 표본은 전 컬럼을 비교합니다. 12개 테이블 3,697만 행, 표본 3.3만 행에서 불일치 0이었습니다.

비교 전 정규화실측

증상
NUMBER 정밀도, DATE 초 단위, 빈 문자열/NULL, CLOB 전문이 두 DB에서 다르게 표현돼 거짓 불일치가 납니다.
원인
같은 값이라도 두 DB가 내보내는 형식이 다릅니다.
대응
비교 규칙(정밀도·시간 단위·NULL 처리·LOB 전문 비교)을 먼저 정하고 양쪽에 똑같이 적용합니다.

검증 도구의 오류실측

증상
행 수·PK·표본은 전부 같은데 수치 합계만 달랐습니다. DB는 정상이었습니다.
원인
검증 스크립트가 Oracle NUMBER를 부동소수로 받아 유효숫자 16자리에서 잘랐습니다.
대응
정확한 소수형(Decimal)으로 받도록 고치자 일치했습니다. 검증 도구도 검증 대상입니다.

07 · Operations

운영 이슈

PostgreSQL로 옮긴 뒤에야 드러나는 문제입니다. 전환할 때 설정과 감시를 같이 넘겨야 합니다.

VACUUM과 테이블 팽창

증상
수정·삭제가 많은 테이블이 계속 불어나 느려집니다.
원인
PostgreSQL은 옛 버전 행을 테이블 안에 남기고, VACUUM이 치웁니다.
대응
테이블별 autovacuum 설정과 fillfactor를 조정하고 팽창을 모니터링합니다.

트랜잭션 ID 고갈

증상
방치하면 DB가 쓰기를 멈춥니다.
원인
트랜잭션 번호가 한계에 가까워지면 강제 정리가 필요합니다.
대응
대량 적재 직후 VACUUM FREEZE로 미리 정리하고, 한계에 얼마나 가까운지 계속 감시합니다.

연결 하나가 프로세스 하나

증상
Oracle처럼 연결을 수백 개 열면 메모리가 버티지 못합니다.
원인
PostgreSQL은 연결마다 서버 프로세스를 띄웁니다.
대응
PgBouncer로 연결 풀을 둡니다.

실행계획이 갑자기 바뀜

증상
같은 SQL을 여러 번 실행하다 보면 어느 순간 수십 배 느려집니다.
원인
준비된 문장을 반복 실행하면 값과 무관한 범용 계획으로 바뀔 수 있습니다.
대응
plan_cache_mode와 확장 통계로 안정시킵니다.

ALTER TABLE 잠금

증상
컬럼 변경 중 뒤따르는 조회까지 줄줄이 멈춥니다.
원인
타입 변경은 테이블을 다시 쓰고, 잠금 대기가 뒤의 요청을 막습니다.
대응
lock_timeout, 단계적 변경, CREATE INDEX CONCURRENTLY로 운영 중 변경을 안전하게 합니다.

08 · Chemical Structures

화학 구조 데이터

JChem(상용)에서 RDKit(오픈소스) PostgreSQL 카트리지로 구조 검색을 옮길 때의 문제입니다.

RDKit 분자 변환실측

증상
화합물 292만 건을 RDKit 분자로 바꾸는 데 걸리는 시간과 실패 건을 미리 알아야 합니다.
원인
구조식 품질과 툴킷 해석 규칙이 결과를 좌우합니다.
대응
PostgreSQL 내부 4 병렬로 637초에 2,896,339건을 생성했습니다. 나머지는 원본 구조가 없거나 격리된 건입니다.

툴킷 간 해석 차이실측

증상
공개 원본 데이터 중 17건(약 0.0006%)을 RDKit이 거부했습니다. 과원자가 PF6⁻ 표기(F[PH](F)(F)(F)(F)F), 풀러렌 유도체 등입니다.
원인
JChem과 RDKit은 원자가·방향족성 해석 규칙이 다릅니다.
대응
“툴킷 차이” 사유 코드로 따로 격리해 업무 담당자가 판단하게 하고, 무결성 목표(예: 99.99%)의 분모·분자를 정할 때 별도로 셉니다.

오염 데이터 격리실측

증상
깨진 SMILES, 비정형 순도값, 잘못된 코드값이 섞여 들어옵니다.
원인
레거시 데이터에는 입력 검증이 없던 시절의 값이 남아 있습니다.
대응
일부러 심은 오류 10,740건을 전부 격리했고, 멀쩡한 데이터를 잘못 격리한 건은 0이었습니다.

구조 비교 기준

증상
표준 SMILES 문자열로 비교하면 같은 분자인데 불일치가 대량으로 나옵니다.
원인
JChem과 RDKit의 표준 SMILES 생성 방식이 다릅니다.
대응
InChIKey로 비교하거나, 양쪽을 같은 툴킷으로 다시 정규화해 비교합니다.

희귀 골격 검색이 느림실측

증상
흔한 부분구조(결과 100건)는 P95 1.5초, 유사도 Top100은 P95 0.76초로 목표를 충족했지만, 희귀 골격은 P50 6.1초로 목표(P95 3초)에 못 미쳤습니다.
원인
GiST 스크리닝을 통과한 거짓 후보 1.5만~15만 건을 실제 매칭으로 다시 확인합니다. 희귀 골격의 65%는 결과 100건을 못 채워 인덱스 전체를 순회합니다.
대응
검색 테이블과 인덱스(약 3.5GB)가 메모리에 들어가도록 확보하고, 느린 질의의 비동기 검색 전환, 자주 쓰는 골격 결과 캐시, 대표 질의 세트 합의를 대책으로 세웠습니다. 스크리닝 지문 크기 확대(2,048→4,096비트)는 실측 결과 거짓 후보가 1%도 줄지 않고 인덱스만 2배가 되어 대책에서 뺐습니다.

넓은 테이블에서 구조 검색실측

증상
원문·물성 컬럼이 함께 있는 테이블에서 같은 희귀 질의가 79.8초, 두 번째 23.8초로 편차가 컸습니다.
원인
원문 컬럼이 캐시를 잠식합니다.
대응
(id, 분자, 지문)만 둔 검색 전용 좁은 테이블로 분리하자 5.66초 · 5.79초로 안정됐습니다. 가장 효과가 큰 튜닝이었습니다.

병렬 검색의 역효과실측

증상
병렬 4워커를 강제하자 같은 질의가 48.3초로 오히려 느려졌습니다.
원인
4 vCPU 상한과 캐시 경합 때문입니다. 플래너도 병렬을 스스로 고르지 않았습니다.
대응
병렬을 강제하지 않고, 코어 수와 메모리에 맞춰 측정으로 결정합니다.

구조 인덱스 생성 시간실측

증상
292만 분자의 부분구조 GiST 인덱스를 단일 테이블에 만들면 35분 47초가 걸립니다.
원인
GiST 생성은 병렬화되지 않아 전환 시간 예산을 크게 잡아먹습니다.
대응
검색 전용 파티션 테이블 8개에 4개씩 동시에 만들어 7분 25초(4.8배 단축)로 줄였습니다. 지문 인덱스는 8분 5초 → 81초.

카트리지에 없는 InChI실측

증상
RDKit 카트리지 빌드에 InChI 함수가 없어 DB 안에서 InChIKey를 만들 수 없습니다.
원인
배포 패키지가 InChI 라이브러리를 포함하지 않고 빌드됐습니다.
대응
InChI · InChIKey는 애플리케이션 계층(Python RDKit)에서 계산해 저장합니다. InChIKey 조회는 P95 12ms였습니다.

지문 크기 설정 불일치

증상
세션 기본 지문 크기(512)와 저장한 지문(2048)이 어긋나면 검색 결과와 성능이 달라집니다.
원인
RDKit 지문 크기는 세션 설정을 따릅니다.
대응
지문 크기 설정을 DB 단위로 고정합니다.

Limits

이 실측의 한계

숫자를 믿을 수 있도록 무엇을 측정하지 않았는지도 적어 둡니다.

  1. 모사 구조레거시 스키마는 공개 자료로 재현한 구조입니다. 실제 운영 DB의 쿼리 패턴, 데이터 편향, 문자셋은 다를 수 있습니다.
  2. 공유 자원4 vCPU 서버에서 다른 작업과 자원을 나눠 쓰며 측정했습니다. 절대값보다 구성 간 비교에 의미가 있습니다.
  3. 작은 질의 세트검색 질의는 흔한 패턴 15종과 무작위 희귀 골격 20종입니다. P95는 질의 세트 구성에 민감합니다.
  4. 상용 툴킷과의 교차 비교JChem 검색 결과와 1만 건 이상 정답 세트로 교차 비교하는 작업은 상용 라이선스가 없어 하지 않았습니다.

Process

이 순서로 전환합니다

  1. 01

    평가

    ora2pg 리포트로 객체별 난이도와 공수를 뽑고, PL/SQL 규모를 파악합니다.

  2. 02

    스키마 변환

    타입 매핑과 명명 규칙을 정하고, 근거를 매핑정의서로 남깁니다.

  3. 03

    데이터 이관

    ora2pg · oracle_fdw · COPY로 옮기고, 필요하면 변경분을 동기화해 중단 시간을 줄입니다.

  4. 04

    정합성 검증

    행 수, 컬럼 체크섬, 업무 키를 대조합니다. 옮기지 못한 행은 격리 테이블에 사유와 함께 남깁니다.

  5. 05

    성능 검증

    주요 SQL의 실행계획을 두 DB에서 비교하고 인덱스를 다시 설계합니다.

  6. 06

    전환

    시퀀스 값을 보정하고, 리허설을 거쳐 롤백 경로를 준비한 상태로 넘어갑니다.

전환 상담 문의 Oracle 스키마 DDL이나 현황 자료를 보내 주시면 전환 난이도와 주의할 지점을 검토해 회신드립니다.