같은 일을 하는 쿼리 문법이 Oracle·Tibero, MySQL, MSSQL 에서 어떻게 다른지 한 페이지에 모았습니다.
Ctrl+F로 기능 이름(예:OFFSET,MINUS,PIVOT,MERGE)을 검색하면 됩니다. 원리는 각 절 끝에 적은 레슨에서 다루고, 여기서는 바로 찾아 쓰는 표와 짧은 코드만 담습니다. Tibero 는 Oracle 호환이라 Oracle 과 묶고, 다른 점만 따로 적습니다.
자주 헷갈리는 10가지입니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 상위 N건 | 인라인 뷰 + ROWNUM |
LIMIT n |
TOP (n) |
OFFSET ... FETCH |
12c 부터 | 없음 (LIMIT만) |
2012 부터 |
FULL OUTER JOIN |
지원 | 어느 버전에도 없음 | 지원 |
USING 절 |
지원 | 지원 | 미지원 |
| 인라인 뷰 별칭 | AS 쓰면 오류 |
별칭 필수 | 별칭 필수 |
| 차집합 | MINUS (21c 부터 EXCEPT도) |
EXCEPT (8.0.31) |
EXCEPT |
| 재귀 CTE 키워드 | RECURSIVE 없이 |
WITH RECURSIVE 필수 |
RECURSIVE 없이 |
| 계층 조회 | CONNECT BY |
재귀 CTE | 재귀 CTE |
| 소계·총계 | ROLLUP·CUBE·GROUPING SETS |
WITH ROLLUP만 |
2008 부터 표준형 |
| 없으면 INSERT, 있으면 UPDATE | MERGE |
ON DUPLICATE KEY UPDATE |
MERGE (끝 ; 필수) |
핵심은 세 가지입니다.
OFFSET ... FETCH 도 FULL OUTER JOIN 도 없습니다.USING 이 없고, 재귀 CTE 에 RECURSIVE 를 쓰지 않습니다.AS 를 쓰면 오류입니다.정렬 후 앞의 N건, 또는 M번째부터 N건을 가져오는 문법입니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 앞 N건 | ROWNUM <= n (정렬 후 바깥에서) |
LIMIT n |
SELECT TOP (n) |
| 건너뛰고 N건 | OFFSET m ROWS FETCH NEXT n ROWS ONLY (12c) |
LIMIT n OFFSET m |
OFFSET m ROWS FETCH NEXT n ROWS ONLY (2012) |
OFFSET 조건 |
ORDER BY 권장(없으면 순서 비보장) |
해당 없음 | ORDER BY 필수 |
| 순번으로 자르기 | ROW_NUMBER() 인라인 뷰 |
ROW_NUMBER() (8.0) |
ROW_NUMBER() (2005) |
| Tibero | OFFSET FETCH 는 버전 확인 |
해당 없음 | 해당 없음 |
ROWNUM 은 WHERE 를 거치는 중에 번호가 붙습니다. 그래서 ROWNUM > n 은 항상 0건이고, 정렬도 바깥에서 해야 합니다. 아래 코드는 정렬한 뒤 앞 3건입니다.
-- Oracle · Tibero
SELECT
t.*
FROM (
SELECT
e.name
, e.sal
FROM emp e
ORDER BY e.sal DESC
) t
WHERE ROWNUM <= 3;
-- MySQL
SELECT e.name, e.sal FROM emp e ORDER BY e.sal DESC LIMIT 3;
-- MSSQL
SELECT TOP (3) e.name, e.sal FROM emp e ORDER BY e.sal DESC;OFFSET 은 뒤 페이지로 갈수록 느립니다. 앞 페이지의 마지막 값을 기억해 그다음부터 읽는 키셋 방식은 (정렬 컬럼, id) 인덱스를 탑니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
행 값 비교 (a,b) < (x,y) |
불가, OR 로 풀기 |
가능 | 불가, OR 로 풀기 |
-- 어느 DB 나 되는 조건 (정렬 sal DESC, id ASC 의 다음 페이지)
SELECT
e.id
, e.name
, e.sal
FROM emp e
WHERE e.sal < :last_sal
OR (e.sal = :last_sal AND e.id > :last_id)
ORDER BY e.sal DESC, e.id
FETCH FIRST 10 ROWS ONLY;마지막 줄은 Oracle 12c·MSSQL 2012 형태입니다. MySQL 은 LIMIT 10, MSSQL 은 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY 로 바꿉니다.
주의
ROWNUM > n은 0건입니다. MSSQL 의OFFSET은ORDER BY없이 쓸 수 없습니다.
자세한 설명은 실무 쿼리의 페이징 레슨을 봅니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 표준 외부 조인 | LEFT/RIGHT OUTER JOIN |
같음 | 같음 |
| 구식 외부 조인 | a.x = b.x(+) |
없음 | *= 는 2012 에서 제거 |
FULL OUTER JOIN |
지원 | 없음 | 지원 |
USING (col) |
지원 | 지원 | 미지원 |
| 인라인 뷰 별칭 | (...) t (AS t 는 오류) |
필수 | 필수 |
IN 리터럴 목록 |
1000개 초과 시 ORA-01795 | 해당 없음 | 해당 없음 |
| 스칼라 서브쿼리 | 결과 캐싱 있음 | 해당 없음 | 해당 없음 |
MSSQL 의 구식 *= 는 2005(호환성 수준 90)부터 쓸 수 없고, 2012 에서 완전히 제거됐습니다. 새 코드는 항상 OUTER JOIN 으로 씁니다.
MySQL 에는 FULL OUTER JOIN 이 없어서 왼쪽·오른쪽 조인을 UNION 으로 합칩니다.
-- MySQL: FULL OUTER JOIN 대체
SELECT
d.code
, e.name
FROM dept d
LEFT JOIN emp e ON e.dept = d.code
UNION
SELECT
d.code
, e.name
FROM dept d
RIGHT JOIN emp e ON e.dept = d.code;IN 목록이 1000개를 넘는 Oracle 쿼리는 목록을 나누거나 임시 테이블·서브쿼리로 바꿉니다.
주의MySQL 5.5 이하는
IN (서브쿼리)를 상관 서브쿼리로 바꿔 바깥 행마다 다시 실행했습니다(DEPENDENT SUBQUERY). 5.6 부터 세미 조인입니다.
자세한 설명은 중급 01 서브쿼리를 봅니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 합집합 | UNION / UNION ALL |
같음 | 같음 |
| 교집합 | INTERSECT |
8.0.31 부터 | 지원 |
| 차집합 | MINUS |
8.0.31 부터 EXCEPT |
EXCEPT |
EXCEPT 키워드 |
Oracle 21c 부터 | 8.0.31 | 지원 |
INTERSECT ALL·EXCEPT ALL |
Oracle 21c 부터 (MINUS ALL도) |
해당 없음 | 해당 없음 |
Tibero ALL 옵션 |
없음 | 해당 없음 | 해당 없음 |
UNION ALL 은 중복을 제거하지 않아 빠르고, UNION 은 중복을 제거합니다.
-- Oracle · Tibero
SELECT dept FROM emp
MINUS
SELECT code FROM dept;
-- MySQL 8.0.31+ · MSSQL
SELECT dept FROM emp
EXCEPT
SELECT code FROM dept;주의MySQL 8.0.31 에서 추가된 것은
INTERSECT와EXCEPT입니다.FULL OUTER JOIN이 아닙니다.
자세한 설명은 중급 01 서브쿼리를 봅니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
WITH 절 |
9i | 8.0 | 2005 |
| 재귀 CTE | 11gR2 | 8.0 | 2005 |
| 재귀 키워드 | WITH (RECURSIVE 없음) |
WITH RECURSIVE 필수 |
WITH (RECURSIVE 없음) |
| 컬럼 목록 | WITH c (a, b) 필수 |
선택 | 선택 |
| 재귀 한도 | 해당 없음 | cte_max_recursion_depth 1000 |
MAXRECURSION 100 (0 은 무제한) |
| 순환·정렬 | SEARCH·CYCLE 절 |
직접 조건 | 직접 조건 |
| 앞 문장과 구분 | 불필요 | 불필요 | 앞 문장 끝에 ; 필요 |
앵커 문자열 길이가 재귀부까지 고정되므로, 경로를 이어 붙일 때는 앵커에서 CAST 로 길이를 늘려 둡니다.
-- MySQL
WITH RECURSIVE t (id, name, lvl) AS (
SELECT id, name, 1 FROM emp WHERE mgr IS NULL
UNION ALL
SELECT e.id, e.name, t.lvl + 1
FROM emp e
JOIN t ON e.mgr = t.id
)
SELECT * FROM t;Oracle·MSSQL 은 WITH RECURSIVE 대신 WITH t (id, name, lvl) AS (...) 로 씁니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
INSERT |
INSERT ... WITH ... SELECT |
INSERT ... WITH ... SELECT |
WITH c ... INSERT |
UPDATE·DELETE |
서브쿼리 안에서 WITH |
WITH ... UPDATE/DELETE |
WITH c ... UPDATE c |
참고H2 는
WITH로 시작하는 DML 을 지원하지 않아 실행 근거로 쓸 수 없습니다.
자세한 설명은 고급 계층·재귀 쿼리와 WITH 절 레슨을 봅니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
순위 함수·OVER(PARTITION BY) |
8i | 8.0 | 2005 |
집계 OVER(ORDER BY)·프레임 |
8i | 8.0 | 2012 |
LAG·LEAD |
8i | 8.0 | 2012 |
FIRST_VALUE·LAST_VALUE |
8i | 8.0 | 2012 |
NTH_VALUE |
11gR2 | 8.0 | 없음 |
IGNORE NULLS |
지원 (LAG·LEAD 는 11gR2) |
미지원 (RESPECT NULLS만) |
2022 |
RATIO_TO_REPORT |
Oracle·Tibero 전용 | 없음 | 없음 |
RANGE 숫자·INTERVAL 오프셋 |
가능 | 8.0 가능 | 불가 (UNBOUNDED·CURRENT ROW만) |
MySQL 5.7 이하는 사용자 변수로 흉내 내며 평가 순서가 보장되지 않습니다.
SELECT·ORDER BY 에만 씁니다. WHERE 에 쓰려면 인라인 뷰나 CTE 로 감쌉니다.ORDER BY 만 있고 프레임을 생략하면 기본은 RANGE UNBOUNDED PRECEDING ~ CURRENT ROW 입니다.ROWS 를 씁니다.LAG·LEAD 는 프레임과 무관하지만, FIRST_VALUE·LAST_VALUE 는 프레임의 영향을 받습니다.LAST_VALUE 의 기본 프레임은 현재 행까지라서, 마지막 값이 아니라 현재 행 값이 나옵니다.-- 세 DB 공통 (MySQL 8.0 · MSSQL 2012 이상)
SELECT
e.name
, e.sal
, SUM(e.sal) OVER (ORDER BY e.hired, e.id
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW) AS running
FROM emp e;주의
RATIO_TO_REPORT는 Oracle·Tibero 에만 있습니다. 다른 DB 는sal / SUM(sal) OVER ()로 씁니다. MSSQL 은 정수 나눗셈이 잘리니* 100.0을 곱합니다.
자세한 설명은 고급 01 순위 함수와 고급 02 를 봅니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 계층 전개 | START WITH ... CONNECT BY PRIOR |
재귀 CTE (8.0) | 재귀 CTE (2005) |
| 루트·리프·순환 | CONNECT_BY_ROOT·ISLEAF·NOCYCLE·ISCYCLE (10g) |
직접 계산 | 직접 계산 |
| 순환 데이터 | NOCYCLE 없으면 오류 |
직접 조건 | 직접 조건 |
CONNECT BY 는 Oracle·Tibero 전용입니다. WHERE 는 전개가 끝난 뒤 걸러 내므로, 가지를 미리 자르려면 CONNECT BY 조건에 넣습니다.
-- Oracle · Tibero
SELECT
LEVEL
, e.name
FROM emp e
START WITH e.mgr IS NULL
CONNECT BY PRIOR e.id = e.mgr;| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
PIVOT (행→열) |
11g | 없음, 조건부 집계 | 2005 |
UNPIVOT (열→행) |
11g | 없음 | 2005 |
UNPIVOT 의 NULL |
기본 제외, INCLUDE NULLS |
해당 없음 | 늘 제외 |
| 열→행 다른 방법 | 해당 없음 | 해당 없음 | CROSS APPLY (VALUES ...) |
IN 목록 |
정적 | 해당 없음 | 정적 |
Tibero 의 PIVOT 은 버전 확인이 필요합니다. IN 목록은 고정된 값이라, 열이 바뀌면 동적 SQL 로 문장을 만들어야 합니다.
-- 조건부 집계 (MySQL 은 이 방법뿐, 다른 DB 도 가능)
SELECT
o.cust
, SUM(CASE WHEN o.status = 'PAID' THEN o.amt ELSE 0 END) AS paid
, SUM(CASE WHEN o.status = 'CANCEL' THEN o.amt ELSE 0 END) AS cancel
FROM orders o
GROUP BY o.cust;MSSQL PIVOT 은 나머지 모든 컬럼으로 묶습니다. 필요한 컬럼만 고른 파생 테이블을 PIVOT 에 넘깁니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
ROLLUP |
GROUP BY ROLLUP(a, b) (8i) |
GROUP BY a, b WITH ROLLUP |
표준형 2008, WITH ROLLUP 2005 이하 |
CUBE |
8i | 없음 | 표준형 2008, WITH CUBE 2005 이하 |
GROUPING SETS |
8i | 없음 | 2008 |
GROUPING(col) |
8i | 8.0 | 2005 이상 |
GROUPING_ID |
9i | 없음 | 2008 |
GROUPING_ID(a,b) 는 GROUPING(a)*2 + GROUPING(b) 와 같습니다. 소계 행의 위치는 ORDER BY GROUPING() 으로 고정합니다.
-- Oracle · Tibero · MSSQL 2008+
SELECT
e.dept
, COUNT(*) AS cnt
FROM emp e
GROUP BY ROLLUP(e.dept);
-- MySQL
SELECT
e.dept
, COUNT(*) AS cnt
FROM emp e
GROUP BY e.dept WITH ROLLUP;팁MySQL 은
CUBE·GROUPING SETS가 없으니GROUP BY를 여러 번 쓰고UNION ALL로 합칩니다. 소계 라벨에CAST를 쓸 때 MySQL 은VARCHAR대신CHAR를 씁니다.
자세한 설명은 고급 계층 쿼리, 피벗, 소계 레슨을 봅니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 없으면 INSERT, 있으면 UPDATE | MERGE (9i) |
INSERT ... ON DUPLICATE KEY UPDATE |
MERGE (2008) |
WHEN 절 |
10g 부터 하나만 써도 됨 | 해당 없음 | BY SOURCE 가능 |
| 삭제 | UPDATE 뒤 DELETE WHERE |
해당 없음 | WHEN MATCHED AND 조건 THEN DELETE |
끝 ; |
불필요 | 불필요 | 필수 |
| 원천 중복 키 | ORA-30926 | 해당 없음 | 오류 |
| ON 절 컬럼 UPDATE | ORA-38104 | 해당 없음 | 해당 없음 |
| 통째로 교체 | 해당 없음 | REPLACE (DELETE 후 INSERT) |
해당 없음 |
| 결과 확인 | 해당 없음 | 해당 없음 | OUTPUT $action |
Oracle MERGE 의 DELETE 는 방금 갱신된 행에만 적용됩니다. MSSQL 은 힌트를 별칭 앞에 씁니다.
-- Oracle · Tibero
MERGE INTO emp t
USING (SELECT 1 AS id, '김대표' AS name FROM dual) s
ON (t.id = s.id)
WHEN MATCHED THEN UPDATE SET t.name = s.name
WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);
-- MySQL
INSERT INTO emp (id, name)
VALUES (1, '김대표') AS new
ON DUPLICATE KEY UPDATE name = new.name;
-- MSSQL (힌트는 별칭 앞)
MERGE INTO emp WITH (HOLDLOCK) AS t
USING (SELECT 1 AS id, N'김대표' AS name) AS s
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET t.name = s.name
WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);MySQL 은 VALUES() 함수가 8.0.20 에서 폐기 예정이 되어, 8.0.19 부터 AS new 별칭을 씁니다. REPLACE 는 지우고 다시 넣습니다.
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 조인 UPDATE | 상관 서브쿼리·MERGE·조인 뷰 |
UPDATE t JOIN s ... SET |
UPDATE t SET ... FROM t JOIN s |
| 조인 DELETE | WHERE EXISTS·조인 뷰, 23ai 부터 DELETE ... FROM |
DELETE t FROM t JOIN s |
DELETE t FROM t JOIN s |
UPDATE ... FROM |
23ai 부터 | 해당 없음 | 지원 |
SET (a,b) = (SELECT ...) |
지원 | 없음 | 없음 |
| 원천 중복 행 | ORA-01427 | 해당 없음 | 오류 없이 임의 한 행 적용 |
Oracle 조인 뷰는 키 보존 테이블이 아니면 ORA-01779 입니다. 상관 UPDATE 에 WHERE EXISTS 가 없으면 짝이 없는 행이 NULL 로 바뀝니다.
-- Oracle · Tibero: 짝 있는 행만 갱신
UPDATE emp e
SET e.sal = (SELECT s.sal FROM sal_new s WHERE s.id = e.id)
WHERE EXISTS (SELECT 1 FROM sal_new s WHERE s.id = e.id);| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
INSERT ALL·INSERT FIRST |
9i | 없음 | 없음 |
여러 행 VALUES |
23ai 부터 | 지원 | 2008 (1000행) |
| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 건수 제한 | FETCH FIRST 는 서브쿼리 안 |
UPDATE/DELETE ... LIMIT n |
UPDATE/DELETE TOP (n) |
| 제한 사항 | UPDATE 에 FETCH FIRST 직접 불가 |
단일 표만 | 순서 비보장 |
| 서브쿼리 안 제한 | FETCH FIRST 가능 |
불가 (IN 안 LIMIT) |
해당 없음 |
-- Oracle · Tibero: 서브쿼리 안에서 제한
DELETE FROM orders
WHERE id IN (
SELECT
o.id
FROM orders o
WHERE o.status = 'CANCEL'
FETCH FIRST 1000 ROWS ONLY
);
-- MySQL
DELETE FROM orders WHERE status = 'CANCEL' LIMIT 1000;
-- MSSQL
DELETE TOP (1000) FROM orders WHERE status = 'CANCEL';| 기능 | Oracle·Tibero | MySQL | MSSQL |
|---|---|---|---|
| 변경한 행 값 | ... RETURNING col INTO :v |
해당 없음 | OUTPUT inserted.col |
| 삭제한 행 값 | DELETE ... RETURNING col INTO :v |
해당 없음 | OUTPUT deleted.* |
| 다른 표로 저장 | 해당 없음 | 해당 없음 | OUTPUT ... INTO |
| 채번 한 문장 | UPDATE ... RETURNING INTO |
LAST_INSERT_ID(식) |
UPDATE ... OUTPUT inserted.x |
자세한 설명은 실무 쿼리 데이터 이관·검증, 대용량·배치 06 대량 DML·배치 커밋, 08 대용량 삭제·아카이빙을 봅니다.
| 틀린 생각 | 실제 |
|---|---|
MySQL 8.0.31 에 FULL OUTER JOIN 이 생겼다 |
추가된 것은 INTERSECT·EXCEPT |
MySQL 도 OFFSET ... FETCH 를 쓴다 |
LIMIT 만. Oracle 12c·MSSQL 2012 부터 |
Tibero 도 ALL 옵션을 쓴다 |
Oracle 21c 부터만. Tibero 는 없음 |
MSSQL 은 *= 를 아직 쓸 수 있다 |
2005 부터 불가, 2012 에서 완전 제거 |
Oracle 인라인 뷰에 AS t 를 쓴다 |
오류. (...) t |
MSSQL 도 USING 을 쓴다 |
미지원 |
Oracle UPDATE 에 FETCH FIRST 를 바로 쓴다 |
불가. 서브쿼리 안에서 |
Oracle FOR UPDATE 와 FETCH FIRST 를 같이 쓴다 |
ORA-02014 |
MSSQL MERGE 힌트는 별칭 뒤에 쓴다 |
별칭 앞 MERGE INTO t WITH (HOLDLOCK) AS a |
MySQL 은 서브쿼리 안에서 FETCH FIRST 가 된다 |
불가 |
| 재귀 CTE 는 세 DB 가 똑같이 쓴다 | MySQL 만 RECURSIVE 필수, Oracle 은 컬럼 목록 필수 |
INT 컬럼 AVG·나눗셈이 소수로 나온다 |
MSSQL 은 정수로 잘림, * 100.0 |
별칭을 GROUP BY 에 쓴다 |
MySQL 가능, MSSQL 불가, Oracle 23ai 이전 불가 |
자기 표 NOT IN 삭제는 모든 DB 에서 된다 |
MySQL 은 ERROR 1093, 파생 테이블로 감싸기 |
분석 함수를 WHERE 에 쓴다 |
SELECT·ORDER BY 만, 인라인 뷰·CTE 로 감싸기 |
PIVOT 열 목록이 자동으로 만들어진다 |
IN 목록은 정적, 동적은 동적 SQL |
핵심버전 표를 다 외우지 못했다면 "MySQL 은
LIMIT·WITH ROLLUP·ON DUPLICATE KEY" 세 가지만 기억합니다. 나머지는 Oracle 과 MSSQL 이 비슷하고 MySQL 이 다릅니다.
자세한 설명은 고급 01 순위 함수와 실무 쿼리 레슨을 봅니다.