대용량 데이터를 다룰 때, 웹 애플리케이션이나 API에서 페이징(paging)은 필수적인 기능이다.
MySQL에서는 대표적으로 LIMIT과 OFFSET 키워드를 이용해 쉽게 페이징을 구현할 수 있지만, 이 방식은 데이터가 많아질수록 성능 저하가 발생할 수 있다.
이번 글에서는 MySQL의 LIMIT .. OFFSET .. 동작 원리와 한계, 그리고 이를 극복하는 커서 페이징(cursor pagination) 방식을 소개한다.
LIMIT OFFSET을 활용한 간단한 예제를 먼저 살펴보자.
다음은 LeetCode 176번 문제인 "두 번째로 높은 급여 조회"를 위한 SQL 쿼리이다.
SELECT (
SELECT DISTINCT salary
FROM Employee
ORDER BY salary DESC
LIMIT 1 OFFSET 1
) AS SecondHighestSalary;
ORDER BY salary DESC로 급여를 내림차순 정렬한다. DISTINCT로 중복을 제거한다. LIMIT 1 OFFSET 1을 통해 첫 번째(가장 높은) 급여를 건너뛰고, 두 번째로 높은 급여를 선택한다. NULL이 된다. 이와 같은 구조는 간단한 조회에는 유용하지만, 데이터가 많고 OFFSET 값이 커질수록 성능상 이슈가 발생할 수 있다.
이를 확인하기 위해 다음과 같은 페이징 쿼리를 살펴보자.
SELECT id
FROM comment
WHERE is_deleted = FALSE
LIMIT 15
OFFSET 10000;
is_deleted = FALSE 조건을 만족하는 데이터 중, 10001번째부터 15개를 조회하려는 쿼리이다. OFFSET 방식의 문제를 해결하기 위해, 커서 기반 페이징(cursor pagination)을 사용하는 방법이 있다.
이 방식은 마지막에 조회한 레코드의 고유값(예: id)을 커서(cursor)로 활용하여, 다음 페이지를 조회하는 쿼리이다.
예시 쿼리는 다음과 같다.
SELECT id
FROM comment
WHERE is_deleted = FALSE AND id < :last_id
ORDER BY id DESC
LIMIT 15;
id < :last_id 조건을 만족하는 데이터 중 가장 최신의 15개만 조회한다. | 구분 | LIMIT OFFSET | Cursor Pagination |
|---|---|---|
| 성능 | OFFSET이 커질수록 성능 저하 | 인덱스를 활용하여 빠른 조회 가능 |
| 데이터 변경 대응 | 중복 혹은 누락 발생 가능 | 일관성 유지에 유리 |
| 구현 복잡도 | 간단 | 약간 복잡 (마지막 커서 값 관리 필요) |
| 사용 사례 | 소규모, 변경이 적은 데이터에 적합 | 대용량 데이터, 실시간 데이터에 적합 |
LIMIT OFFSET 페이징은 간단하지만, 대용량 데이터 환경에서는 성능 저하와 일관성 문제가 발생할 수 있다. OFFSET 값이 클 경우, 무의미한 데이터 스캔 비용이 쿼리 성능을 저하시킨다.