전역(GLOBAL) 인덱스 없음
- 증상
- 파티션 키 없이 조회하면 모든 파티션의 인덱스를 차례로 훑습니다. 파티션이 많을수록 실행계획 수립 시간까지 늘어납니다.
- 원인
- PostgreSQL 파티션 인덱스는 전부 파티션별(로컬) 인덱스입니다. 전체를 관통하는 단일 인덱스가 없습니다.
- 대응
- 조회 조건에 파티션 키를 넣고, 파티션 수를 줄이거나 파티셔닝 자체를 할지 성능을 측정해 결정합니다. 꼭 필요하면 상용판(EDB)을 검토합니다.
Oracle → PostgreSQL
총 3,697만 행 규모의 레거시 DB를 직접 옮기며 겪은 것과 설계 단계에서 먼저 막아야 하는 것을 영역별로 정리했습니다. 문제마다 증상 · 원인 · 대응을 적었고, 실제로 측정해 확인한 31건에는 실측 표시를 붙였습니다.
데이터: 공개 ChEMBL 데이터를 레거시 구조(스키마 3개 · 테이블 16개 · 총 36,975,841행)로 재배치. 다른 작업과 자원을 나눠 쓰는 4 vCPU 서버에서 측정해, 절대값보다 구성 간 비교에 의미가 있습니다.
01 · Index · Partition
같은 인덱스라도 두 DB가 찾는 방식이 다릅니다. 이관 뒤 갑자기 풀스캔이 되는 원인은 대부분 여기 있습니다.
unique constraint on partitioned table must include all partitioning columns 오류. id 하나로 PK나 FK를 걸 수 없습니다.IS NULL 조회를 태우려고 만든 (col, 0) 같은 꼼수 인덱스가 그대로 넘어옵니다.WHERE col IS NOT NULL 부분 인덱스로 바꿉니다.NULLS NOT DISTINCT를 쓰되, 모두 NULL인 행까지 막는 차이가 있어 필요하면 부분 유니크 인덱스로 맞춥니다.name LIKE '아스피%'가 인덱스를 타지 않고 풀스캔이 됩니다. 같은 쿼리인데 이름 목록 순서가 전환 전과 달라집니다.COLLATE "C"뿐입니다.text_pattern_ops 인덱스로 한글명 100만 행 기준 64.8ms → 0.06ms를 확인했습니다. 목록 순서를 지켜야 하는 컬럼은 COLLATE "C"를 매핑 정의서에 명시하고, 중간 일치는 pg_trgm GIN 인덱스로 대응합니다.TO_CHAR(reg_date, 'YYYY') 같은 인덱스가 생성 단계에서 실패합니다.GENERATED ALWAYS AS … STORED)에 인덱스를 걸거나 날짜 범위 조건으로 바꿉니다.INCLUDE 커버링 인덱스, 해시 파티션으로 대체합니다.NOT VALID 외래키를 허용하지 않습니다./*+ … */ 힌트를 무시합니다.CREATE STATISTICS)로 다시 잡고, 꼭 필요한 곳만 pg_hint_plan을 씁니다.02 · Data Types
에러 없이 옮겨지지만 값이 달라지는 문제들입니다. 가장 늦게 발견되고 가장 비쌉니다.
IS NULL 조회가 그 행을 놓치고, NVL 결과가 달라집니다.CHECK (col <> '')로 막고, 코드의 빈 문자열 비교를 전수 검색합니다.'a' || NULL이 Oracle에서는 'a', PostgreSQL에서는 NULL입니다. 이름·주소 조합 결과가 통째로 비어 나옵니다.concat()을 쓰거나 COALESCE로 감쌉니다.PG_NUMERIC_TYPE 0으로 numeric(p,s)를 유지하고, 정수는 integer/bigint로 가려 매핑합니다. 정밀도 없는 NUMBER를 전부 numeric으로 두면 느립니다.statement_timestamp()로 바꿉니다. 실제 현재 시각이 필요하면 clock_timestamp()를 씁니다.LPAD(숫자)가 생성은 되지만 실행하면 실패합니다. varchar_id = 12345 같은 비교는 오류가 나거나 인덱스를 못 탑니다.to_date(…, 'YYYYMMDD'))으로 고칩니다.1 ⁄ 4 세 글자로 쪼개져 8,432행이 순도 1%로 저장됐습니다.>95%는 95로 제대로 복구되는 것을 확인했습니다.03 · SQL
자동 변환이 가장 많이 건드리는 영역이지만, 결과가 같은지는 따로 대조해야 합니다.
WHERE ROWNUM <= 10 ORDER BY x를 LIMIT으로 바꾸면 결과가 달라집니다.schema.table)으로 바꿔 순서 의존을 없앴습니다.INSERT … ON CONFLICT로 옮깁니다. orafce 확장으로 일부를 그대로 쓸 수도 있습니다.04 · PL/SQL → PL/pgSQL
생성까지 성공하고 실행에서 실패하는 문제가 몰려 있는 영역입니다. 그래서 모든 객체를 실제로 실행해 보는 것을 검증 기준으로 삼습니다.
PRAGMA AUTONOMOUS_TRANSACTION을 자동 변환이 dblink 자기 접속으로 흉내 냅니다.ON CONFLICT, 사전 검증 후 격리 테이블로 다시 짭니다. JDBC는 autosave=conservative를 씁니다.05 · Migration Run
도구가 알려 주지 않는 절차상의 함정입니다. 리허설에서 한 번씩 다 밟았습니다.
setval합니다.value too long으로 실패했습니다.VACUUM (FREEZE, ANALYZE)를 절차에 넣고(실측 237초 · 144초) 전환 시간 예산에 포함합니다. 성능은 그 뒤에 측정합니다.06 · Verification
행 수만 맞춰서는 부족합니다. 비교 규칙부터 정해야 검증 결과를 믿을 수 있습니다.
07 · Operations
PostgreSQL로 옮긴 뒤에야 드러나는 문제입니다. 전환할 때 설정과 감시를 같이 넘겨야 합니다.
plan_cache_mode와 확장 통계로 안정시킵니다.lock_timeout, 단계적 변경, CREATE INDEX CONCURRENTLY로 운영 중 변경을 안전하게 합니다.08 · Chemical Structures
JChem(상용)에서 RDKit(오픈소스) PostgreSQL 카트리지로 구조 검색을 옮길 때의 문제입니다.
F[PH](F)(F)(F)(F)F), 풀러렌 유도체 등입니다.Limits
숫자를 믿을 수 있도록 무엇을 측정하지 않았는지도 적어 둡니다.
Process
ora2pg 리포트로 객체별 난이도와 공수를 뽑고, PL/SQL 규모를 파악합니다.
타입 매핑과 명명 규칙을 정하고, 근거를 매핑정의서로 남깁니다.
ora2pg · oracle_fdw · COPY로 옮기고, 필요하면 변경분을 동기화해 중단 시간을 줄입니다.
행 수, 컬럼 체크섬, 업무 키를 대조합니다. 옮기지 못한 행은 격리 테이블에 사유와 함께 남깁니다.
주요 SQL의 실행계획을 두 DB에서 비교하고 인덱스를 다시 설계합니다.
시퀀스 값을 보정하고, 리허설을 거쳐 롤백 경로를 준비한 상태로 넘어갑니다.