SQL에서 페이징이나 N번째 데이터를 구할 때 자주 사용하는 LIMIT과 OFFSET.
그러나 이 구문을 CTE(Common Table Expression) 또는 서브쿼리에서 사용할 경우, "결과가 없을 때"의 동작 방식이 서로 다르다는 사실을 알고 있는가?
이번 글에서는 "두 번째로 높은 급여"를 구하는 예제를 통해, LIMIT OFFSET 사용 시 결과가 없을 때 NULL을 반환하는 경우와 그렇지 않은 경우를 비교하여 살펴본다.
WITH cte AS (
SELECT salary AS SecondHighestSalary
FROM Employee
ORDER BY 1 DESC
LIMIT 1 OFFSET 1
)
SELECT *
FROM cte;
LIMIT 1 OFFSET 1은 두 번째 행을 의미한다.Employee 테이블에 급여가 하나뿐이라면, 두 번째 급여가 없으므로 CTE는 빈 결과를 반환한다.SELECT * FROM cte는 0행을 반환하며 아무 값도 출력되지 않는다.NULL이 아닌, 결과 자체가 존재하지 않는 상태가 된다.SELECT (
SELECT DISTINCT salary
FROM Employee
ORDER BY salary DESC
LIMIT 1 OFFSET 1
) AS SecondHighestSalary;
LIMIT 1 OFFSET 1으로 결과가 없으면, NULL 값을 반환한다.NULL 값은 SecondHighestSalary 컬럼에 담겨 1행으로 반환된다.| 구조 | 결과 없을 때 반환 | 리턴 row 수 | 반환값 |
|---|---|---|---|
WITH cte ... SELECT * FROM cte | 아무 것도 없음 (빈 결과 집합) | 0행 | 없음 |
SELECT (SELECT ... LIMIT 1 OFFSET 1) | NULL | 1행 | NULL |
아래와 같이 UNION ALL과 NOT EXISTS를 사용하면, 결과가 없을 때 강제로 NULL을 반환할 수 있다.
WITH cte AS (
SELECT salary AS SecondHighestSalary
FROM Employee
ORDER BY salary DESC
LIMIT 1 OFFSET 1
)
SELECT COALESCE(SecondHighestSalary, NULL) AS SecondHighestSalary
FROM cte
UNION ALL
SELECT NULL
WHERE NOT EXISTS (SELECT 1 FROM cte)
LIMIT 1;
NOT EXISTS로 CTE 결과가 없을 경우를 감지한다.UNION ALL과 LIMIT 1 조합으로 결과가 있을 때는 그 값을, 없으면 NULL 하나만 반환된다.SQL에서 서브쿼리를 사용할 때, 서브쿼리 결과가 없으면 NULL을 반환할까? 아니면 아예 결과 행이 없을까?
이 차이는 SQL 설계에서 매우 중요하다.
CREATE TABLE product (
id INT,
name VARCHAR(20)
);
CREATE TABLE review (
product_id INT,
score INT
);
INSERT INTO product VALUES (1, 'A'), (2, 'B');
INSERT INTO review VALUES (1, 5);
SELECT
p.id,
p.name,
(
SELECT r.score
FROM review r
WHERE r.product_id = p.id AND r.score >= 10
) AS high_score
FROM product p;
이 쿼리는 각 상품마다 리뷰 점수가 10 이상인 것을 찾는다.
하지만 score >= 10을 만족하는 리뷰가 없으므로, 내부 서브쿼리는 결과를 반환하지 않는다.
그럼에도 불구하고, 쿼리 결과는 아래와 같이 NULL이 채워진다.
| id | name | high_score |
|---|---|---|
| 1 | A | NULL |
| 2 | B | NULL |
이 동작은 SQL에서 스칼라 서브쿼리(scalar subquery)로 감쌌기 때문에 발생하는 것이다.
SELECT (...)처럼 값을 하나 반환하는 서브쿼리이다.SELECT (
SELECT DISTINCT salary
FROM Employee
ORDER BY salary DESC
LIMIT 1 OFFSET 1
) AS SecondHighestSalary;
이 쿼리에서 내부 서브쿼리의 결과가 없더라도, 외부에서는 아래와 같이 1행이 존재하고 값은 NULL로 채워진다.
| SecondHighestSalary |
|---|
| NULL |
이 차이를 이해하는 것이 중요하다:
| 상황 | 반환 행 수 | 반환 값 | 설명 |
|---|---|---|---|
SELECT DISTINCT salary ... LIMIT 1 OFFSET 1 (단독 실행) | 0행 | 없음 | 빈 결과 집합 |
| 스칼라 서브쿼리 내부에서 결과 없음 | 1행 | NULL | 값이 없음을 NULL로 치환 |
즉, 빈 결과(empty result)는 "행 자체가 없음"이고,
NULL 반환은 "행은 있는데 값이 없음"을 의미한다.
SELECT (...)로 감싸면, SQL은 "반드시 하나의 값"을 기대한다.NULL로 자동 변환하여 결과를 채운다.