본문 바로가기
Database & Storage

오라클 SQL을 PostgreSQL로 바꾸는 규칙 20가지: NVL·DECODE·ROWNUM·MERGE까지 한 번에

by yamoojin83 2025. 12. 21.

서론: “문법만 바꾸면 되겠지”가 가장 위험한 착각

오라클에서 PostgreSQL로 DB를 옮길 때, 가장 먼저 마주치는 벽은 SQL이다.
겉으로는 비슷해 보여도 함수, NULL 처리, 페이징, UPSERT, 날짜/타임존까지
작은 차이가 쌓이면 결과가 달라지고, 그게 운영 장애로 이어진다.

이 글은 실무에서 가장 많이 부딪히는 변환 포인트를 규칙 20가지로 정리했다.
NVL/DECODE/ROWNUM/MERGE 같은 대표 케이스는 물론,
GROUP BY, 문자열/날짜 함수, outer join 구문까지 한 번에 정리한다.

목표는 단순하다.
“오라클 쿼리를 보고, PostgreSQL로 즉시 치환할 수 있는 수준”이다.

 

Oracle → PostgreSQL SQL 변환 맵


규칙 1) NVL → COALESCE

오라클의 NVL(a, b)는 NULL이면 b로 치환한다.
PostgreSQL에서는 coalesce(a, b)를 사용한다.


-- Oracle
SELECT NVL(col, 0) FROM t;

-- PostgreSQL
SELECT COALESCE(col, 0) FROM t;

규칙 2) NVL2 → CASE WHEN

NVL2(x, a, b)는 x가 NULL이 아니면 a, NULL이면 b.
PostgreSQL에서는 CASE가 가장 명확하다.


-- Oracle
SELECT NVL2(col, 'Y', 'N') FROM t;

-- PostgreSQL
SELECT CASE WHEN col IS NOT NULL THEN 'Y' ELSE 'N' END FROM t;

규칙 3) DECODE → CASE WHEN

DECODE는 Postgres에 없다. CASE WHEN으로 바꾼다.


-- Oracle
SELECT DECODE(status, 'A', 'ACTIVE', 'I', 'INACTIVE', 'UNKNOWN') FROM t;

-- PostgreSQL
SELECT CASE
  WHEN status = 'A' THEN 'ACTIVE'
  WHEN status = 'I' THEN 'INACTIVE'
  ELSE 'UNKNOWN'
END
FROM t;

규칙 4) SYSDATE → NOW()

오라클 SYSDATE는 서버 현재 시간이다. Postgres는 now().


-- Oracle
SELECT SYSDATE FROM dual;

-- PostgreSQL
SELECT NOW();

규칙 5) SYSTIMESTAMP → CURRENT_TIMESTAMP

타임스탬프를 명시적으로 쓰려면 CURRENT_TIMESTAMP가 안전하다.


-- Oracle
SELECT SYSTIMESTAMP FROM dual;

-- PostgreSQL
SELECT CURRENT_TIMESTAMP;

규칙 6) dual 테이블 제거

PostgreSQL에서는 dual이 필요 없다.


-- Oracle
SELECT 1 FROM dual;

-- PostgreSQL
SELECT 1;

규칙 7) ROWNUM 페이징 → LIMIT/OFFSET

오라클의 ROWNUM 기반 페이징은 Postgres에서 LIMIT/OFFSET로 치환한다.


-- Oracle (예시)
SELECT * FROM (
  SELECT a.*, ROWNUM rnum
  FROM (SELECT * FROM t ORDER BY id DESC) a
  WHERE ROWNUM <= 20
) WHERE rnum > 10;

-- PostgreSQL
SELECT *
FROM t
ORDER BY id DESC
LIMIT 10 OFFSET 10;

단, OFFSET은 페이지가 뒤로 갈수록 느려질 수 있다.
대량 데이터에서는 키셋 페이징(규칙 8)을 고려한다.


규칙 8) 대용량 페이징은 키셋 페이징으로

OFFSET 기반 페이징은 뒤 페이지로 갈수록 비용이 커진다.
Postgres에서는 기준 키를 사용해 “이후 데이터”를 조회하는 방식이 효율적이다.


-- PostgreSQL keyset paging
SELECT *
FROM t
WHERE id < :last_id
ORDER BY id DESC
LIMIT 10;

규칙 9) MERGE → INSERT ... ON CONFLICT (UPSERT)

오라클 MERGE는 Postgres에서 INSERT ... ON CONFLICT로 바꾼다.
단, Postgres는 충돌 기준(UNIQUE/PK)이 반드시 있어야 한다.


-- Oracle MERGE (개념)
MERGE INTO t USING src
ON (t.id = src.id)
WHEN MATCHED THEN UPDATE SET t.val = src.val
WHEN NOT MATCHED THEN INSERT (id, val) VALUES (src.id, src.val);

-- PostgreSQL UPSERT
INSERT INTO t (id, val)
VALUES (:id, :val)
ON CONFLICT (id)
DO UPDATE SET val = EXCLUDED.val;

 

MERGE → UPSERT 개념도


규칙 10) OUTER JOIN (+) → LEFT JOIN

오라클의 (+) 문법은 Postgres에서 지원되지 않는다. ANSI JOIN으로 바꾼다.


-- Oracle
SELECT a.*, b.name
FROM a, b
WHERE a.bid = b.id(+);

-- PostgreSQL
SELECT a.*, b.name
FROM a
LEFT JOIN b ON a.bid = b.id;

규칙 11) 문자열 연결 || 는 동일하지만 NULL에 주의

오라클과 Postgres 모두 ||로 문자열 결합이 가능하다.
하지만 NULL 처리에서 결과가 달라지는 케이스가 있으니, 안전하게 COALESCE를 섞는다.


-- safer
SELECT COALESCE(first_name,'') || ' ' || COALESCE(last_name,'') FROM t;

규칙 12) SUBSTR → SUBSTRING

오라클 SUBSTR은 Postgres에서 SUBSTRING으로 대응된다.


-- Oracle
SELECT SUBSTR(col, 1, 3) FROM t;

-- PostgreSQL
SELECT SUBSTRING(col FROM 1 FOR 3) FROM t;

규칙 13) INSTR → POSITION

문자열 위치 찾기는 Postgres의 POSITION 또는 STRPOS를 쓴다.


-- Oracle
SELECT INSTR(col, 'abc') FROM t;

-- PostgreSQL
SELECT POSITION('abc' IN col) FROM t;
-- or
SELECT STRPOS(col, 'abc') FROM t;

규칙 14) LENGTH는 동일, BYTE/CHAR 의미 차이에 주의

멀티바이트 문자(한글 등)에서 BYTE 기준이 필요한지 확인해야 한다.
Postgres에서는 기본적으로 문자 길이를 다루며, 인코딩/콜레이션 정책을 먼저 확정하는 것이 안전하다.


규칙 15) TO_CHAR / TO_DATE / TO_TIMESTAMP 패턴 차이

날짜 포맷 변환은 유사하지만 포맷 토큰이 일부 다를 수 있다.
특히 주(week), 분기(quarter), 타임존 토큰에서 차이가 날 수 있으니
실제 샘플 데이터를 넣고 결과를 비교하는 절차가 필요하다.


규칙 16) TRUNC(date) → date_trunc()

오라클 TRUNC(date)는 “일자 절삭”에 자주 쓰인다.
Postgres는 date_trunc로 명시한다.


-- Oracle
SELECT TRUNC(created_at) FROM t;

-- PostgreSQL (day)
SELECT date_trunc('day', created_at) FROM t;

규칙 17) 시퀀스 nextval 문법 차이

오라클: seq.NEXTVAL
Postgres: nextval('seq')


-- Oracle
SELECT my_seq.NEXTVAL FROM dual;

-- PostgreSQL
SELECT nextval('my_seq');

규칙 18) LISTAGG → string_agg

오라클의 LISTAGG는 Postgres의 string_agg로 대응된다.


-- Oracle
SELECT LISTAGG(name, ',') WITHIN GROUP (ORDER BY name) FROM t;

-- PostgreSQL
SELECT string_agg(name, ',' ORDER BY name) FROM t;

규칙 19) 분석 함수는 대부분 유지되지만 NULL 정렬 규칙 확인

ROW_NUMBER, RANK, DENSE_RANK 같은 윈도우 함수는 Postgres에서도 지원한다.
다만 ORDER BY 시 NULL이 앞/뒤로 오는 규칙이 다를 수 있으니 NULLS FIRST/LAST를 명시하는 습관이 안전하다.


SELECT
  id,
  ROW_NUMBER() OVER (ORDER BY score DESC NULLS LAST) AS rn
FROM t;

규칙 20) GROUP BY 엄격성(선택 컬럼 포함 여부) 확인

Postgres는 GROUP BY가 더 엄격하게 동작한다.
오라클에서 “묵시적으로 허용되던” 쿼리가 Postgres에서는 에러가 날 수 있다.
따라서 SELECT 컬럼 중 집계가 아닌 컬럼은 GROUP BY에 반드시 포함해야 한다.


-- PostgreSQL (correct)
SELECT dept_id, COUNT(*)
FROM emp
GROUP BY dept_id;

실무 팁: 변환 후 반드시 해야 하는 검증 5가지

SQL 변환은 “컴파일이 된다”가 끝이 아니다.
특히 아래 5가지는 운영에서 사고를 줄이는 필수 점검이다.

  • NULL 처리 결과가 동일한지(특히 문자열 결합/정렬)
  • 날짜/타임존 변환 결과 비교(샘플 데이터로)
  • 페이징 성능(OFFSET vs 키셋)
  • MERGE/UPSERT 충돌 기준(UNIQUE/PK) 확정
  • 실행계획(EXPLAIN)으로 인덱스 사용 여부 확인

결론: SQL 변환은 “규칙 + 검증”이 세트다

오라클 SQL을 PostgreSQL로 바꾸는 일은 규칙만 외우면 끝나는 문제가 아니다.
결과가 동일한지 검증하고, 성능이 유지되는지 확인해야 진짜 전환이다.

이 글의 20가지 규칙은 가장 자주 마주치는 변환 포인트를 모아둔 것이다.
다음 글에서는 실제 마이그레이션에서 가장 난이도 높은 주제인
CDC 기반 무중단 전환(Expand→Migrate→Contract)를 다룰 예정이다.