Oracle·MySQL·PostgreSQL·SQL Server 문법 차이: SQL 이식 시 결과 검증


1장. SQL은 실행됐는데 결과가 달라졌다#

Oracle에서 사용하던 SQL을 PostgreSQL로 옮겼습니다.

문법 오류도 없습니다.

쿼리도 실행됩니다.

하지만 운영 검증에서 문제가 발견됐습니다.

Oracle 보고서:

최신 주문

13
12

새 시스템:

최신 주문

12
13

둘 다 같은 두 주문입니다.

하지만 순서가 다릅니다.

다른 화면에서는 더 심각한 문제가 나타났습니다.

기존 시스템에서는:

빈 문자열
→ 미입력

으로 처리되던 값이 새 DB에서는:

빈 문자열
→ 실제 빈 문자열

로 남았습니다.

또 다른 기능에서는 업서트 문법을 바꿨더니:

어떤 고유 키가 충돌했는가?

에 따라 갱신되는 행의 의미가 달라졌습니다.

SQL 마이그레이션에서 가장 위험한 생각은 이것입니다.

문법 오류 없이 실행되면 이식이 끝났다.

그렇지 않습니다.

SQL 이식은 최소한 다음 네 가지를 비교해야 합니다.

문법

결과 값

결과 자료형

변경 후 데이터 상태

2장. SQL 표준이 있어도 DBMS별 차이는 사라지지 않는다#

관계형 데이터베이스는 공통적으로 SQL을 사용합니다.

그래서 다음 문장은 대체로 익숙합니다.

SELECT
    customer_id,
    customer_name
FROM customer
WHERE status = 'ACTIVE';

하지만 실제 애플리케이션에서는 기본 조회만 사용하지 않습니다.

다음 기능이 들어갑니다.

상위 N건

페이지 조회

자동 번호

문자열 연결

NULL 대체

날짜 계산

업서트

계층 조회

JSON

형 변환

문자열 검색

이 영역부터 제품별 SQL 방언 차이가 크게 드러납니다.


3장. SQL 이식에서는 문법 호환과 의미 호환을 분리해야 한다#

문법 호환#

다음 SQL이 대상 DBMS에서 실행되는가?

LIMIT 10

FETCH FIRST 10 ROWS ONLY

TOP 10

의미 호환#

실행된 결과가 기존 시스템과 같은가?

같은 10행인가?

같은 순서인가?

동점 처리도 같은가?

NULL 처리도 같은가?

문법이 달라도 의미는 같을 수 있습니다.

반대로 문법이 비슷해도 결과는 달라질 수 있습니다.


4장. 마이그레이션의 기준 데이터부터 만든다#

SQL을 옮기기 전에 작은 기준 데이터를 준비하는 것이 좋습니다.

예를 들어 주문 테이블:

주문ID 생성시각 금액
11 2026-09-01 09:00 100
12 2026-09-02 10:00 200
13 2026-09-02 10:00 300
14 2026-08-31 08:00 400

일부러:

동점 시각

을 넣었습니다.

이런 경계값이 있어야 SQL 차이를 발견할 수 있습니다.


5장. 좋은 이식 테스트 데이터에는 정상값만 넣지 않는다#

최소한 다음을 포함시키는 편이 좋습니다.

NULL

빈 문자열

공백 문자열

동점

중복 키

월말

윤년

0

음수

소수점

없는 부모

빈 결과

정상 데이터만 넣으면 제품 차이가 잘 드러나지 않습니다.


6장. 네 DBMS의 대표 SQL 차이를 먼저 정리해 보자#

기능 Oracle MySQL PostgreSQL SQL Server
행 제한 FETCH FIRST LIMIT LIMIT, FETCH TOP, OFFSET FETCH
문자열 연결 ` , CONCAT` CONCAT
NULL 대체 NVL, COALESCE IFNULL, COALESCE COALESCE ISNULL, COALESCE
자동 번호 Identity·Sequence AUTO_INCREMENT Identity·Sequence IDENTITY
업서트 MERGE ON DUPLICATE KEY UPDATE ON CONFLICT, MERGE MERGE 등
계층 조회 CONNECT BY, 재귀 CTE 재귀 CTE 재귀 CTE 재귀 CTE
문자열 위치 INSTR INSTR, LOCATE POSITION, strpos CHARINDEX
날짜 가감 ADD_MONTHS 등 DATE_ADD INTERVAL DATEADD
부분 문자열 SUBSTR SUBSTRING substring SUBSTRING

이 표는 단순 변환 사전이 아닙니다.

실제 이식에서는 각 기능의 결과 의미를 다시 확인해야 합니다.


7장. 패턴 1 — 최신 주문 2건 가져오기#

질문:

가장 최근 주문 두 건을 가져와라.

정렬:

created_at DESC

만 사용하면 주문 12와 13은 같은 시각입니다.

12
2026-09-02 10:00

13
2026-09-02 10:00

둘 중 어떤 주문이 먼저 나와야 하는지 정해져 있지 않습니다.


8장. 업무 순서를 고유하게 만든다#

예:

ORDER BY
    created_at DESC,
    order_id DESC

이라고 정하겠습니다.

예상 순서:

13

12

11

14

입니다.

상위 두 건:

13

12

입니다.


9장. PostgreSQL 대표 표현#

SELECT
    order_id,
    amount
FROM orders
ORDER BY
    created_at DESC,
    order_id DESC
LIMIT 2;

10장. MySQL 대표 표현#

SELECT
    order_id,
    amount
FROM orders
ORDER BY
    created_at DESC,
    order_id DESC
LIMIT 2;

이 문제에서는 PostgreSQL과 매우 비슷하게 표현할 수 있습니다.


11장. Oracle 대표 표현#

SELECT
    order_id,
    amount
FROM orders
ORDER BY
    created_at DESC,
    order_id DESC
FETCH FIRST 2 ROWS ONLY;

12장. SQL Server 대표 표현#

SELECT TOP (2)
    order_id,
    amount
FROM orders
ORDER BY
    created_at DESC,
    order_id DESC;

네 쿼리 모두 기대하는 결과는:

13

12

입니다.


13장. 문법보다 ORDER BY가 더 중요할 수 있다#

다음처럼 쓰면:

ORDER BY created_at DESC

12와 13의 순서는 완전히 고정되지 않습니다.

따라서:

Oracle에서는 12, 13

PostgreSQL에서는 13, 12

가 나왔다고 해서 DBMS가 잘못된 것은 아닙니다.

SQL에서 고유한 순서를 요구했다면 정렬 조건을 충분히 제공해야 합니다.


14장. ROWNUM을 옛 방식으로 옮길 때도 주의한다#

Oracle의 오래된 코드에서 다음 형태를 볼 수 있습니다.

WHERE ROWNUM <= 2

중요한 것은:

어느 단계에서 행이 제한되는가?

입니다.

정렬과 행 제한의 순서를 잘못 배치하면:

원하는 최신 두 행

이 아니라:

먼저 선택된 두 행을 나중에 정렬

할 수 있습니다.


15장. 페이지 조회에서는 OFFSET의 의미도 검증해야 한다#

페이지당 10건이라고 하겠습니다.

PostgreSQL#

LIMIT 10
OFFSET 20

MySQL#

LIMIT 10
OFFSET 20

또는 지원되는 문법 형태에 맞춰 표현합니다.

Oracle#

OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY

SQL Server#

ORDER BY created_at DESC, order_id DESC
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;

16장. 문법이 같아도 OFFSET 페이지의 구조적 문제는 같다#

1페이지 조회 후 새 주문이 들어왔습니다.

기존:

1  A
2  B
3  C

새 주문 X가 맨 앞에 들어오면:

1  X
2  A
3  B
4  C

이 됩니다.

2페이지에서 OFFSET을 적용하면 이미 본 행이 다시 나타나거나 기존 행이 밀릴 수 있습니다.

이것은 특정 DBMS 문법 문제가 아닙니다.

페이지 방식 자체의 특성입니다.


17장. 패턴 2 — 문자열 연결#

다음 값을 연결하겠습니다.

first_name = 가람

last_name = 김

원하는 결과:

김가람

입니다.


18장. PostgreSQL에서는 ||를 사용할 수 있다#

SELECT
    last_name || first_name
FROM member;

또는:

SELECT
    concat(
        last_name,
        first_name
    )
FROM member;

을 사용할 수 있습니다.

하지만 NULL 처리 의미가 서로 같다고 가정하면 안 됩니다.


19장. Oracle에서도 ||를 흔히 볼 수 있다#

SELECT
    last_name || first_name
FROM member;

기존 Oracle SQL을 PostgreSQL로 옮기면 문법상 그대로 동작하는 경우가 많습니다.

하지만 빈 문자열과 NULL 처리 차이는 별도로 확인해야 합니다.


20장. MySQL에서는 CONCAT을 사용하는 것이 일반적이다#

SELECT
    CONCAT(
        last_name,
        first_name
    )
FROM member;

특히 NULL 인자가 들어왔을 때 결과를 검증해야 합니다.


21장. SQL Server에는 + 연결도 존재한다#

SELECT
    last_name + first_name
FROM member;

또는:

SELECT
    CONCAT(
        last_name,
        first_name
    )
FROM member;

을 사용할 수 있습니다.

같은 DBMS 안에서도 연산자와 함수의 NULL 처리 의미가 다를 수 있으므로 단순 문자열 치환은 위험합니다.


22장. NULL 문자열을 실제로 넣어보자#

입력:

last_name = 김

first_name = NULL

입니다.

업무 요구:

김

을 보여줘야 한다고 하겠습니다.

단순 문자열 연결 결과가 제품이나 사용한 함수에 따라 달라질 수 있으므로 명시적인 NULL 정책을 두는 편이 안전합니다.

예:

COALESCE(first_name, '')

입니다.


23장. 빈 문자열은 더욱 조심해야 한다#

입력:

nickname = ''

입니다.

업무에서는:

빈 문자열도 미입력으로 본다.

고 정했다고 하겠습니다.

그렇다면 다음과 같이 의도를 직접 표현할 수 있습니다.

COALESCE(
    NULLIF(nickname, ''),
    '미입력'
)

24장. 이 SQL의 업무 의미#

먼저:

NULLIF(nickname, '')

는 빈 문자열을 NULL로 만듭니다.

그다음:

COALESCE(..., '미입력')

으로 표시합니다.

따라서:

NULL
→ 미입력

''
→ 미입력

'가람'
→ 가람

이라는 계약을 만들 수 있습니다.


25장. 공백 한 칸은 빈 문자열이 아니다#

입력:

' '

은 일반적으로:

''

과 다릅니다.

따라서 업무가 앞뒤 공백도 미입력으로 보려면 별도의:

TRIM

규칙이 필요할 수 있습니다.

하지만 사용자 입력 공백을 무조건 삭제해도 되는지는 업무 정책에 따라 결정해야 합니다.


26장. 패턴 3 — NULL 대체 함수#

제품별로 다음 함수를 볼 수 있습니다.

Oracle#

NVL(value, default_value)

또는:

COALESCE(...)

MySQL#

IFNULL(value, default_value)

또는:

COALESCE(...)

PostgreSQL#

COALESCE(...)

SQL Server#

ISNULL(value, default_value)

또는:

COALESCE(...)

27장. 함수 이름만 바꾸면 충분하지 않을 수 있다#

다음도 확인해야 합니다.

반환 자료형

암시적 변환

문자 길이

정밀도

NULL과 빈 문자열의 의미

특히 애플리케이션이 결과 자료형에 의존하면 화면은 같아 보여도 프로그램 동작은 달라질 수 있습니다.


28장. 패턴 4 — 형 변환#

문자열:

2026-10-01

을 날짜로 바꾼다고 하겠습니다.

제품마다:

CAST

CONVERT

TO_DATE

::date

등의 표현을 볼 수 있습니다.


29장. 가장 중요한 것은 잘못된 날짜 입력이다#

정상:

2026-10-01

만 시험하면 차이가 잘 드러나지 않습니다.

다음을 넣어야 합니다.

2026-02-29

2026-13-01

01/10/2026

20261001

그리고 확인합니다.

오류가 나는가?

암시적으로 변환되는가?

어떤 날짜로 해석되는가?

30장. 01/10/2026은 특히 위험하다#

이 값은 사람에 따라:

2026년 1월 10일

로 볼 수도 있고:

2026년 10월 1일

로 볼 수도 있습니다.

DB 세션 설정이나 애플리케이션 지역 설정에 기대지 말고 가능한 한 명확한 날짜 표현과 바인딩을 사용하는 것이 좋습니다.


31장. 패턴 5 — 날짜에 한 달 더하기#

날짜:

2026-01-31

에 한 달을 더한다고 하겠습니다.

질문은 단순해 보입니다.

하지만 먼저 정해야 합니다.

한 달 뒤라는 것이 정확히 무엇인가?


32장. 1월 31일의 다음 달 같은 일자는 존재하지 않는다#

2월에는 31일이 없습니다.

따라서 DBMS의 날짜 함수는 나름의 규칙으로 결과를 정합니다.

이런 이유로 날짜 마이그레이션에서는 최소한 다음을 시험하는 것이 좋습니다.

1월 31일

2월 28일

윤년 2월 29일

3월 31일

33장. 날짜 함수 이름만 대응시키지 말자#

Oracle:

ADD_MONTHS

MySQL:

DATE_ADD

PostgreSQL:

INTERVAL

SQL Server:

DATEADD

같은 기능을 사용할 수 있습니다.

하지만 실제 월말 결과가 기존 업무 규칙과 같은지는 작은 테스트 데이터로 확인해야 합니다.


34장. 패턴 6 — 자동 번호#

테이블에 신규 주문 번호를 자동으로 생성한다고 하겠습니다.

제품별로 대표적으로:

Oracle
Identity / Sequence

MySQL
AUTO_INCREMENT

PostgreSQL
Identity / Sequence

SQL Server
IDENTITY

를 사용할 수 있습니다.


35장. 자동 번호는 연속성을 보장하는 회계 번호가 아니다#

다음 번호가 생성됐다고 하겠습니다.

101

102

104

103이 없습니다.

가능한 이유:

트랜잭션 롤백

재시도

캐시

동시 삽입

실패한 INSERT

등이 있을 수 있습니다.


36장. “번호가 빠지면 안 된다”는 요구는 별도 문제다#

영수증 번호처럼:

1

2

3

4

가 반드시 연속되어야 하는 업무라면 일반적인 자동 증가 PK에 그 요구를 그대로 얹어서는 안 될 수 있습니다.

기술 식별자

업무 문서번호

를 분리하는 방식도 고려할 수 있습니다.


37장. 자동 번호의 마지막 값을 읽는 방식도 제품별로 다르다#

애플리케이션에서:

INSERT
↓
방금 생성된 ID 얻기

가 필요합니다.

이때 제품별 기능과 드라이버 API가 다릅니다.

가장 위험한 방법은:

MAX(id)

입니다.


38장. MAX(id)가 위험한 이유#

세션 A:

주문 삽입
ID 100

세션 B:

주문 삽입
ID 101

A가:

SELECT MAX(id)

를 하면:

101

을 받을 수 있습니다.

자기가 생성한 ID가 아닙니다.

생성 ID를 반환하는 DBMS·드라이버 기능을 사용해야 합니다.


39장. 패턴 7 — 업서트#

재고:

sku = A
qty = 5

가 있습니다.

새 이벤트:

sku = A
qty = 2

가 들어왔습니다.

먼저 업무 규칙을 정해야 합니다.

교체#

최종 수량
2

증가#

최종 수량
7

입니다.

SQL 문법이 이 결정을 대신하지 않습니다.


40장. PostgreSQL에서는 ON CONFLICT를 사용할 수 있다#

누적 예:

INSERT INTO stock (
    sku,
    qty
)
VALUES (
    'A',
    2
)
ON CONFLICT (sku)
DO UPDATE
SET qty =
    stock.qty
    + EXCLUDED.qty;

결과:

A
7

입니다.


41장. MySQL에서는 ON DUPLICATE KEY UPDATE 계열을 볼 수 있다#

핵심 개념은:

INSERT 시도
↓
PRIMARY KEY 또는 UNIQUE 충돌
↓
기존 행 갱신

입니다.

하지만 여러 UNIQUE 키가 있다면:

어느 충돌 때문에 갱신됐는가?

를 특히 주의해서 검증해야 합니다.


42장. Oracle에서는 MERGE를 사용할 수 있다#

개념적으로:

MERGE INTO stock t
USING source_stock s
ON (
    t.sku = s.sku
)
WHEN MATCHED THEN
    UPDATE SET
        t.qty = t.qty + s.qty
WHEN NOT MATCHED THEN
    INSERT (
        sku,
        qty
    )
    VALUES (
        s.sku,
        s.qty
    );

처럼 조건부 갱신·삽입을 표현할 수 있습니다.


43장. SQL Server에도 MERGE 구문이 있지만 “같은 이름 = 같은 운영 특성”은 아니다#

DBMS마다:

동시성

제약 처리

트리거

영향받은 행

오류 처리

의 세부 동작을 검증해야 합니다.

같은 MERGE라는 단어가 있다고 동일한 구현이라고 보면 안 됩니다.


44장. MySQL REPLACE는 일반적인 UPDATE형 업서트와 같은 개념으로 취급하지 말자#

기존 행을:

그대로 UPDATE

하는 것과:

기존 행을 제거하고 새 행을 만드는 효과

는 다를 수 있습니다.

다음이 영향을 받을 수 있습니다.

외래키

트리거

자동 번호

변경 시각

감사 이력

따라서 이름만 보고 MERGE의 대응 구문이라고 기계적으로 바꾸면 위험합니다.


45장. 업서트에서는 중복 요청도 검증해야 한다#

API 요청:

재고 +2

가 네트워크 재시도로 두 번 도착했습니다.

첫 번째:

5 → 7

두 번째:

7 → 9

가 됩니다.

하지만 실제 업무 이벤트는 한 번이었다면 잘못된 결과입니다.


46장. 업서트와 멱등성은 다른 문제다#

업서트는:

행이 있으면 갱신

없으면 삽입

을 해결합니다.

멱등성은:

같은 업무 요청이 두 번 와도
한 번만 반영

을 해결합니다.

둘을 같은 문제로 보면 안 됩니다.


47장. 패턴 8 — 외부 조인#

고객:

1

2

3

이 있습니다.

주문은 고객 1에게만 있습니다.

요구:

paid 주문이 없는 고객도 결과에 남겨라.


48장. 상태 조건을 ON에 두는 방법#

SELECT
    c.customer_id,
    o.order_id
FROM customer c
LEFT JOIN orders o
  ON o.customer_id = c.customer_id
 AND o.status = 'paid';

고객 2와 3은:

order_id = NULL

상태로 남습니다.


49장. 상태 조건을 WHERE로 이동하면 결과가 달라진다#

SELECT
    c.customer_id,
    o.order_id
FROM customer c
LEFT JOIN orders o
  ON o.customer_id = c.customer_id
WHERE o.status = 'paid';

NULL 확장 행은:

o.status = NULL

이므로 조건을 통과하지 못합니다.

결과적으로 주문 없는 고객이 사라집니다.


50장. 이 문제는 네 DBMS에서 공통적으로 중요한 SQL 논리다#

DBMS를 바꾸면서:

옛 Oracle (+) 구문

을 ANSI JOIN으로 변경한다고 하겠습니다.

단순 기호 치환보다 중요한 것은:

어떤 조건이 조인 조건인가?

어떤 조건이 최종 필터인가?

입니다.


51장. LEFT JOIN은 고객당 한 행을 보장하지 않는다#

고객 1에게 paid 주문이 두 건 있습니다.

결과:

고객 1 / 주문 A

고객 1 / 주문 B

고객 2 / NULL

고객 3 / NULL

총 4행입니다.

LEFT JOIN의 의미는:

왼쪽 행 보존

이지:

왼쪽 행당 정확히 한 결과

가 아닙니다.


52장. 주문 수를 세려면 COUNT 컬럼을 사용한다#

SELECT
    c.customer_id,
    COUNT(o.order_id)
        AS paid_order_count
FROM customer c
LEFT JOIN orders o
  ON o.customer_id = c.customer_id
 AND o.status = 'paid'
GROUP BY
    c.customer_id;

주문 없는 고객:

0

으로 계산됩니다.


53장. COUNT 별표를 쓰면 결과가 달라진다#

외부 조인에서는 주문 없는 고객도:

NULL을 가진 보존 행 1개

가 존재합니다.

따라서:

COUNT(*)

은 1로 나올 수 있습니다.

이 문제 역시 DBMS 이식 시 같은 예상 결과로 검증해야 합니다.


54장. 패턴 9 — 문자열 내부 위치 찾기#

문자열:

ABC-123-XYZ

에서:

123

의 시작 위치를 찾고 싶습니다.

제품마다 대표 함수가 다릅니다.

Oracle
INSTR

MySQL
INSTR / LOCATE

PostgreSQL
POSITION / strpos

SQL Server
CHARINDEX

55장. 단순히 숫자가 같다는 것만 확인하면 부족하다#

다음도 확인할 수 있습니다.

찾는 문자열이 없을 때

빈 문자열 검색

대소문자 차이

유니코드 문자열

결합 문자

특히 정렬 규칙과 문자열 비교 설정이 검색 결과에 영향을 줄 수 있습니다.


56장. 패턴 10 — 문자열 자르기#

다음 코드:

ABC-123-XYZ

에서:

123

을 자릅니다.

제품별로:

SUBSTR

SUBSTRING

계열을 사용합니다.


57장. 시작 위치와 길이 경계값을 시험한다#

최소 테스트:

시작 1

길이 0

문자열 끝

범위를 넘는 길이

멀티바이트 문자

입니다.

문자 기반인지 바이트 기반인지도 기능별로 확인해야 합니다.


58장. 패턴 11 — 계층 조회#

직원 관계:

가람
├─ 나래
│  └─ 라온
└─ 다온

를 조회한다고 하겠습니다.


59장. Oracle에서는 CONNECT BY 계열 기존 코드가 많다#

Oracle 애플리케이션에서는:

START WITH

CONNECT BY PRIOR

구문을 사용하는 코드를 만날 수 있습니다.

다른 DBMS로 옮길 때는 재귀 CTE로 바꾸는 경우가 많습니다.


60장. PostgreSQL·MySQL·SQL Server에서는 재귀 CTE를 사용할 수 있다#

개념 구조:

WITH RECURSIVE org AS (
    -- 시작 행

    UNION ALL

    -- 자식 행
)
SELECT ...

DBMS별 정확한 구문 차이는 있지만 중요한 것은 결과 구조입니다.


61장. 계층 조회에서는 순환이 핵심 경계 조건이다#

잘못된 데이터:

A → B

B → C

C → A

가 있습니다.

마이그레이션 전 시스템에서는 순환 방지 기능이 있었는데 새 SQL에서는 없을 수 있습니다.

따라서:

방문 노드

깊이 제한

순환 검출

오류 정책

을 함께 검증해야 합니다.


62장. 계층 결과 순서도 별도 문제다#

다음 두 결과는 같은 관계를 표현할 수 있습니다.

A
B
C

와:

A
C
B

하지만 화면이 트리 순서를 요구한다면 명시적인 정렬 규칙이 필요합니다.

재귀가 실행된 순서가 곧 업무 표시 순서라고 가정해서는 안 됩니다.


63장. 날짜와 시간대는 SQL 이식에서 가장 위험한 영역 중 하나다#

원본 시스템:

Asia/Seoul

기준으로 주문일을 계산했다고 하겠습니다.

새 시스템에서는 DB 세션 시간대가:

UTC

입니다.

같은 시각이라도 날짜가 달라질 수 있습니다.


64장. 한국 자정과 UTC 날짜는 다를 수 있다#

한국:

2026-10-01 00:30 KST

는 UTC로:

2026-09-30 15:30 UTC

입니다.

따라서:

주문일 = 2026-10-01

이라는 계산이 DB 시간대에 따라 9월 30일로 바뀔 수 있습니다.


65장. 날짜 컬럼 이식에서는 세 가지를 구분해야 한다#

날짜만 의미하는 값

지역 벽시계 시간

절대적인 한 시점

예:

생일
→ 날짜

매장 영업 시작
→ 지역 시간

결제 발생 순간
→ 절대 시점

입니다.

이 의미를 구분하지 않고 데이터 타입만 변환하면 문제가 생깁니다.


66장. 소수 계산과 반올림도 비교해야 한다#

상품 금액:

10

3

을 나눈다고 하겠습니다.

원하는 값:

3.33

입니다.

하지만 입력 자료형이 정수인지 소수인지에 따라 연산 결과의 자료형과 값이 달라질 수 있습니다.


67장. 마이그레이션 테스트에는 자료형까지 포함한다#

다음만 비교하지 않습니다.

화면
3.33

다음도 확인합니다.

정밀도

스케일

반올림 위치

오버플로

드라이버 반환 타입

특히 금융 데이터에서는 중요합니다.


68장. 문자열 정렬도 DBMS·Collation에 따라 달라질 수 있다#

이름:

A

a

가

각

등의 정렬은:

Collation

언어 설정

대소문자 구분

악센트 구분

등의 영향을 받을 수 있습니다.


69장. “ORDER BY name”이 있다고 모든 DB에서 같은 순서라는 뜻은 아니다#

SQL 문법은 같습니다.

ORDER BY name

하지만 문자열 비교 규칙이 다르면 결과 순서가 달라질 수 있습니다.

정렬 결과 자체가 업무 계약이라면 대상 DB의 collation 정책을 확인해야 합니다.


70장. 식별자 대소문자도 주의한다#

테이블을:

CustomerOrder

로 만들었습니다.

DBMS와 생성 방법, 인용 식별자 사용 여부에 따라:

CustomerOrder

customerorder

CUSTOMERORDER

처리가 달라질 수 있습니다.

ORM이 생성한 SQL에서도 문제가 나타날 수 있습니다.


71장. 예약어 충돌도 마이그레이션에서 자주 발견된다#

기존 컬럼:

user

order

rank

같은 이름이 대상 제품에서 예약어 또는 특별한 의미를 가질 수 있습니다.

자동 변환 도구가 인용부호를 붙여 실행 가능하게 만들더라도 장기 유지보수 관점에서 이름 변경을 검토할 수 있습니다.


72장. DDL의 자료형 대응도 1:1 변환이 아니다#

예를 들어 다음 자료형 이름은 제품별로 다르게 표현될 수 있습니다.

문자열

대형 문자열

불리언

날짜

시간대 포함 시각

자동 번호

따라서:

VARCHAR2 → VARCHAR

NUMBER → NUMERIC

처럼 이름만 바꾸는 것보다 실제 값 범위와 애플리케이션 사용법을 확인해야 합니다.


73장. BOOLEAN도 제품별 역사적 차이가 있었다#

어떤 기존 시스템에서는:

0 / 1

Y / N

CHAR(1)

로 불리언을 표현했을 수 있습니다.

대상 DBMS가 직접 불리언 타입을 지원한다고 해서 단순 타입 변환만 하면 되는 것은 아닙니다.

기존 API 값과 보고서 조건을 함께 변경해야 합니다.


74장. 트랜잭션 오류 처리도 반드시 시험한다#

트랜잭션:

A 성공

B 오류

C 실행

이라는 상황을 만듭니다.

질문:

B가 실패한 뒤
같은 트랜잭션에서 C를 계속 실행할 수 있는가?

트랜잭션은 어떤 상태인가?

무엇을 롤백해야 하는가?

입니다.

제품별 오류 후 트랜잭션 처리와 클라이언트 라이브러리 동작을 확인해야 합니다.


75장. 자동 커밋 설정도 마이그레이션 결과를 바꿀 수 있다#

개발자는 같은 SQL을 실행했다고 생각합니다.

하지만:

원본 연결
Auto Commit OFF

대상 연결
Auto Commit ON

이면 장애 시 결과가 달라질 수 있습니다.

DBMS뿐 아니라 드라이버와 커넥션 풀 설정도 비교해야 합니다.


76장. 격리 수준의 이름만 같은지도 확인할 필요가 있다#

다음 이름을 여러 제품에서 볼 수 있습니다.

READ COMMITTED

REPEATABLE READ

SERIALIZABLE

하지만 내부 구현은:

잠금

MVCC

스냅샷

충돌 처리

에서 차이가 있을 수 있습니다.


77장. 격리 수준 이식은 두 세션 테스트가 가장 확실하다#

세션 A:

값 읽기

세션 B:

값 수정
COMMIT

세션 A:

다시 읽기

를 수행합니다.

기존 DB와 새 DB에서 결과를 비교합니다.

정의표를 보는 것과 실제 업무 흐름을 검증하는 것은 다릅니다.


78장. SQL 함수 자동 변환 도구는 출발점이지 완료 판정 도구가 아니다#

자동 변환:

NVL
→ COALESCE

ROWNUM
→ LIMIT

CONNECT BY
→ WITH RECURSIVE

등을 수행할 수 있습니다.

하지만 도구가 다음까지 자동으로 이해한다고 가정하면 안 됩니다.

업무상 동점 규칙

NULL 의미

정렬 계약

충돌 키 의미

시간대 기준

79장. 자동 변환 후에는 회귀 데이터셋을 돌려야 한다#

예:

정상 행

NULL

빈 문자열

동점

중복 키

월말

대량 데이터

동시 요청

을 기존·신규 DB 모두에 넣습니다.

그리고 결과를 비교합니다.


80장. SQL 이식 결과는 네 단계로 비교할 수 있다#

1. 행 수#

원본
10행

대상
10행

2. 행 값#

모든 키와 계산값 동일

3. 출력 스키마#

컬럼 자료형·정밀도 확인

4. 변경 상태#

INSERT·UPDATE 이후 DB 상태 동일

SELECT 결과만 같다고 DML 이식이 끝난 것은 아닙니다.


81장. 정렬이 없는 결과는 행 순서를 비교하지 않는다#

다음 SQL:

SELECT *
FROM customer;

에 ORDER BY가 없습니다.

Oracle:

1
2
3

PostgreSQL:

3
1
2

로 나와도 SQL 의미상 문제가 아닐 수 있습니다.


82장. 하지만 페이지·TOP N에서는 순서가 계약의 일부다#

최신 10건

급여 상위 3명

다음 페이지

같은 기능에서는 정렬 순서가 결과의 일부입니다.

따라서 고유한 ORDER BY를 반드시 검증합니다.


83장. 비교 테스트에는 기대값을 먼저 적어야 한다#

예:

입력
주문 11,12,13,14

정렬
created_at DESC, order_id DESC

상위 2건 기대
13,12

이렇게 기대값을 먼저 적습니다.

그다음 각 DBMS 결과를 비교합니다.


84장. 결과가 다르면 문법보다 먼저 원인을 분류한다#

가능한 원인:

정렬 규칙 차이

NULL 처리 차이

자료형 변환 차이

시간대 차이

동점 처리 누락

잘못된 문법 변환

실제 데이터 차이

원인을 분류하면 마이그레이션 수정이 쉬워집니다.


85장. SQL 이식 검증표를 만들자#

기능 원본 SQL 대상 SQL 기대 결과 실제 결과 판정
최신 2건 기존 구문 대상 구문 13,12 13,12 통과
빈 별칭 NULL 규칙 변환 규칙 미입력 미입력 통과
업서트 기존 방식 대상 방식 qty=7 qty=7 통과
월말 기존 함수 대상 함수 정의값 확인값 판정
외부 조인 기존 SQL 대상 JOIN 고객 1·2·3 확인 판정

이런 표는 이식의 완료 조건을 명확하게 합니다.


86장. 성능도 별도의 이식 항목이다#

결과는 완전히 같습니다.

하지만:

원본 DB
20ms

대상 DB
2초

라면 업무 기능은 같아도 운영 품질은 같지 않습니다.


87장. 인덱스 설계도 그대로 복사하면 안 될 수 있다#

DBMS마다:

옵티마이저

통계

클러스터 구조

인덱스 기능

부분 인덱스

표현식 인덱스

지원과 비용 모델이 다릅니다.

기존 인덱스 목록을 기계적으로 복사하기보다 실제 SQL 실행 계획을 다시 검증해야 합니다.


88장. 성능 비교는 같은 데이터와 같은 부하에서 한다#

가능하면 다음을 맞춥니다.

데이터 건수

데이터 분포

SQL 바인드값

동시 사용자

캐시 조건

반환 행

그래야 DBMS 간 성능 차이를 의미 있게 비교할 수 있습니다.


89장. 애플리케이션 코드도 함께 확인해야 한다#

SQL만 변경했는데 애플리케이션에서 오류가 발생할 수 있습니다.

예:

Oracle NUMBER
→ Java BigDecimal

대상 DB 타입
→ 다른 드라이버 매핑

이 발생할 수 있습니다.

또 날짜·불리언·UUID 등의 타입 매핑도 달라질 수 있습니다.


90장. ORM이 있다고 SQL 차이가 사라지는 것은 아니다#

ORM은 여러 DBMS의 차이를 추상화할 수 있습니다.

하지만 다음에서는 제품별 차이가 다시 나타날 수 있습니다.

Native Query

DDL

인덱스

JSON

락

페이지네이션

Sequence

업서트

ORM 생성 SQL도 실제 대상 DB에서 검증해야 합니다.


91장. SQL 이식 핵심 체크리스트#

  1. 원본·대상 DBMS와 버전을 기록했는가?
  2. 같은 기준 데이터를 사용했는가?
  3. NULL을 포함했는가?
  4. 빈 문자열을 포함했는가?
  5. 동점 데이터를 포함했는가?
  6. 월말·윤년 날짜를 포함했는가?
  7. 중복 고유 키를 시험했는가?
  8. 정렬 결과가 고유하게 정의되어 있는가?
  9. LIMIT·TOP·FETCH 결과가 같은가?
  10. 문자열 연결의 NULL 결과가 같은가?
  11. 형 변환 실패 결과를 확인했는가?
  12. 자동 번호 공백을 허용하는가?
  13. 업서트의 충돌 기준이 같은가?
  14. 외부 조인에서 NULL 확장 행이 유지되는가?
  15. 집계에서 COUNT 별표 의미가 같은가?
  16. 계층 조회의 순환을 시험했는가?
  17. 날짜 시간대 기준이 같은가?
  18. 반환 자료형과 정밀도가 같은가?
  19. 트랜잭션 오류 후 상태를 시험했는가?
  20. 성능과 실행 계획을 다시 측정했는가?

92장. 가장 흔한 실수 1 — 함수 대응표만 만든다#

예:

NVL
→ COALESCE

ROWNUM
→ LIMIT

INSTR
→ POSITION

만 정리하고 이식을 끝냅니다.

그러나 함수 이름을 바꾸는 것과 업무 결과를 보존하는 것은 다른 작업입니다.


93장. 가장 흔한 실수 2 — 정상 입력만 테스트한다#

정상 문자열:

가람

정상 날짜:

2026-10-01

정상 키:

100

만 시험하면 대부분 비슷하게 동작합니다.

차이는 경계값에서 나타납니다.

NULL

''

동점

월말

중복 키

0으로 나누기

를 반드시 포함시키는 것이 좋습니다.


94장. 가장 흔한 실수 3 — 실행된다는 사실을 정확하다는 뜻으로 본다#

SQL:

문법 오류 없음

은 매우 낮은 단계의 검증입니다.

그다음 확인해야 합니다.

행 수가 같은가?

값이 같은가?

순서가 같은가?

자료형이 같은가?

변경 결과가 같은가?

95장. 가장 흔한 실수 4 — DBMS만 바꾸고 세션 설정은 보지 않는다#

결과에는 DBMS 제품뿐 아니라:

시간대

Collation

SQL Mode

자동 커밋

트랜잭션 격리

날짜 형식

등도 영향을 줄 수 있습니다.

따라서 이식 환경 설정을 기록해야 합니다.


96장. 가장 흔한 실수 5 — SELECT만 검증한다#

조회가 맞아도:

INSERT

UPDATE

DELETE

MERGE

UPSERT

의 동시성·오류 처리·영향 행 수가 다를 수 있습니다.

DML은 변경 후 테이블 상태까지 검증해야 합니다.


97장. 같은 결과를 검증하는 가장 좋은 방법#

각 SQL마다 다음을 적습니다.

입력 데이터

업무 질문

예상 행

예상 순서

예상 값

예상 타입

변경 후 상태

그리고 네 DBMS 결과를 대조합니다.

이렇게 하면 특정 DBMS 문법 지식을 넘어 실제 업무 동등성을 검증할 수 있습니다.


98장. 핵심 정리#

Oracle·MySQL·PostgreSQL·SQL Server는 모두 SQL을 사용합니다.

그러나:

SQL을 사용한다.

와:

같은 SQL 의미를 가진다.

는 같은 말이 아닙니다.

가장 쉽게 드러나는 차이는 행 제한입니다.

Oracle
FETCH FIRST

MySQL
LIMIT

PostgreSQL
LIMIT / FETCH

SQL Server
TOP / OFFSET FETCH

처럼 표현이 다릅니다.

하지만 실제 이식에서 더 중요한 것은 문법보다:

ORDER BY가 결과를 완전히 결정하는가?

입니다.

동점이 존재하는 상태에서 상위 N건을 가져오면 DBMS가 바뀌면서 선택되는 행이 달라질 수 있습니다.

문자열에서는 더 세밀한 차이가 나타납니다.

NULL

빈 문자열

공백 문자열

을 같은 것으로 가정하면 안 됩니다.

업무상 빈 문자열도 미입력으로 볼 필요가 있다면:

COALESCE(
    NULLIF(value, ''),
    '미입력'
)

처럼 의도를 직접 표현하는 편이 이식에 유리합니다.

날짜 함수도 마찬가지입니다.

1월 31일 + 1개월

처럼 월말 경계값을 사용해야 실제 동작 차이를 확인할 수 있습니다.

시간대가 포함되면:

한국 기준 날짜

UTC 기준 날짜

도 달라질 수 있습니다.

자동 번호는 단순히:

Oracle Sequence

MySQL AUTO_INCREMENT

PostgreSQL Identity

SQL Server IDENTITY

라는 대응표로 끝나지 않습니다.

롤백·동시 삽입·실패 때문에 번호가 비어도 되는지, 자동 번호를 업무상 연속 문서번호로 사용하고 있지는 않은지까지 확인해야 합니다.

업서트에서도 구문 이름보다 업무 규칙이 중요합니다.

충돌하면 교체할 것인가?

기존 값에 더할 것인가?

어떤 UNIQUE 키 충돌을 기준으로 할 것인가?

같은 요청이 두 번 오면 어떻게 할 것인가?

를 먼저 정해야 합니다.

외부 조인에서는 SQL 문법 자체보다:

조건이 ON에 있는가?

WHERE에 있는가?

에 따라 주문 없는 고객의 보존 여부가 달라집니다.

재귀 조회에서는:

순환

깊이

출력 순서

까지 시험해야 합니다.

결국 SQL 이식의 완료 기준은:

대상 DB에서 SQL이 실행되는가?

가 아닙니다.

다음 질문에 모두 답할 수 있어야 합니다.

같은 입력에서 같은 행이 나오는가?

같은 정렬 결과가 나오는가?

NULL과 빈 문자열 의미가 유지되는가?

날짜와 시간대 의미가 유지되는가?

계산값과 자료형이 같은가?

INSERT·UPDATE 이후 데이터 상태가 같은가?

동시 실행과 오류 상황에서도 업무 규칙이 유지되는가?

좋은 SQL 마이그레이션은 함수 이름을 번역하는 작업이 아닙니다.

원본 DBMS에 숨어 있던 정렬·NULL·날짜·자료형·트랜잭션의 가정을 찾아내고, 대상 DBMS에서도 같은 업무 결과가 만들어지도록 다시 명시하는 작업입니다.

이 페이지의 목차