2026년 1월 21일 작성
MySQL LIMIT와 OFFSET
LIMIT와 OFFSET은 query 결과의 행 수를 제한하고 시작 위치를 지정하는 절로, paging 구현에 주로 사용됩니다.
LIMIT와 OFFSET
LIMIT는 query 결과에서 반환할 행의 최대 개수를 지정합니다.OFFSET은 결과에서 건너뛸 행의 개수를 지정합니다.- 두 절을 함께 사용하여 pagination을 구현합니다.
SELECT * FROM users
ORDER BY id
LIMIT 10 OFFSET 20;
- 위 query는 21번째 행부터 10개의 행을 반환합니다.
OFFSET 20으로 처음 20개 행을 건너뜁니다.LIMIT 10으로 그 다음 10개 행만 가져옵니다.
기본 문법
LIMIT와OFFSET은SELECT문의 마지막에 위치합니다.
LIMIT만 사용
- 결과의 처음 N개 행만 반환합니다.
-- 상위 5개 행만 조회
SELECT * FROM products
ORDER BY price DESC
LIMIT 5;
LIMIT와 OFFSET 함께 사용
OFFSET으로 시작 위치를 지정하고LIMIT로 개수를 제한합니다.
-- 11번째부터 10개 행 조회
SELECT * FROM products
ORDER BY created_at DESC
LIMIT 10 OFFSET 10;
축약 문법
LIMIT offset, count형식으로 축약할 수 있습니다.- 첫 번째 값이 offset, 두 번째 값이 limit입니다.
- 순서가
LIMIT ... OFFSET ...문법과 반대이므로 주의가 필요합니다.
-- LIMIT 10 OFFSET 20과 동일
SELECT * FROM users
LIMIT 20, 10;
| 문법 | offset | count |
|---|---|---|
LIMIT 10 OFFSET 20 |
20 | 10 |
LIMIT 20, 10 |
20 | 10 |
Paging 구현
- page 번호와 page 크기를 기반으로
LIMIT와OFFSET값을 계산합니다.
OFFSET = (page 번호 - 1) × page 크기
LIMIT = page 크기
계산 예시
- page 크기가 10일 때 각 page의
OFFSET값입니다.
| Page | OFFSET | 조회 범위 |
|---|---|---|
| 1 | 0 | 1~10번째 행 |
| 2 | 10 | 11~20번째 행 |
| 3 | 20 | 21~30번째 행 |
| 5 | 40 | 41~50번째 행 |
Application Code 예시
- page 번호에서 1을 빼고 page 크기를 곱하여
OFFSET값을 계산합니다.
int pageNumber = 3;
int pageSize = 10;
int offset = (pageNumber - 1) * pageSize; // (3 - 1) * 10 = 20
String sql = "SELECT * FROM users ORDER BY id LIMIT ? OFFSET ?";
// LIMIT 10 OFFSET 20 → 21~30번째 행
성능 문제
OFFSET값이 커지면 성능이 급격히 저하됩니다.- MySQL은
OFFSET + LIMIT개의 행을 모두 읽은 후 앞의OFFSET개를 버립니다. OFFSET 100000이면 100,000개 행을 읽고 버리는 작업이 발생합니다.
- MySQL은
성능 저하 원인
-- page 10000 조회 (OFFSET 99990)
SELECT * FROM users
ORDER BY id
LIMIT 10 OFFSET 99990;
- MySQL은
OFFSET + LIMIT개의 행을 모두 읽은 후 앞부분을 버리기 때문에 비효율적입니다.ORDER BY id조건에 맞게 정렬합니다.- 처음부터 100,000개 행을 순차적으로 읽습니다.
- 앞의 99,990개 행을 버립니다.
- 남은 10개 행만 반환합니다.
OFFSET이 증가할수록 읽고 버리는 행의 수가 늘어나 응답 시간이 길어집니다.
| OFFSET | 읽는 행 수 | 버리는 행 수 | 반환 행 수 |
|---|---|---|---|
| 0 | 10 | 0 | 10 |
| 1,000 | 1,010 | 1,000 | 10 |
| 100,000 | 100,010 | 100,000 | 10 |
| 1,000,000 | 1,000,010 | 1,000,000 | 10 |
대안 : Cursor Based Paging
- cursor based paging(keyset pagination)은
OFFSET대신 마지막으로 조회한 값을 기준으로 다음 page를 조회합니다.OFFSET을 사용하지 않으므로 page 위치와 관계없이 일정한 성능을 유지합니다.- index를 활용하여 시작 위치를 빠르게 찾습니다.
동작 방식
- 이전 page의 마지막 행 값을 cursor로 사용합니다.
-- 첫 번째 page
SELECT * FROM users
ORDER BY id
LIMIT 10;
-- 다음 page (이전 page의 마지막 id가 10인 경우)
SELECT * FROM users
WHERE id > 10
ORDER BY id
LIMIT 10;
-- 그 다음 page (이전 page의 마지막 id가 20인 경우)
SELECT * FROM users
WHERE id > 20
ORDER BY id
LIMIT 10;
OFFSET 방식과의 비교
| 항목 | OFFSET 방식 | Cursor 방식 |
|---|---|---|
| Page 이동 | 모든 page로 직접 이동 가능 | 이전/다음 page만 이동 가능 |
| 성능 | page가 뒤로 갈수록 느려짐 | page 위치와 관계없이 일정 |
| Data 정합성 | 중간 data 추가/삭제 시 중복/누락 발생 | 중복/누락 없음 |
| 구현 복잡도 | 단순 | 상대적으로 복잡 |
| 적합한 용도 | 관리자 page, 소규모 data | 무한 scroll, 대규모 data |
Data 정합성 문제
OFFSET방식은 page 조회 중 data가 추가되거나 삭제되면 중복 조회나 누락이 발생합니다.
[초기 상태]
page 1 : A, B, C, D, E (id 1~5)
page 2 : F, G, H, I, J (id 6~10)
[page 1 조회 후 X가 id 3으로 추가됨]
page 1 : A, B, X, C, D (id 1~5)
page 2 : E, F, G, H, I (id 6~10) ← E가 page 2로 밀림
[결과]
- page 1에서 E를 봤는데, page 2에서 E를 다시 보게 됨 (중복)
- cursor 방식은
WHERE id > 5로 조회하므로 id 5 이후의 data만 가져와 중복이 발생하지 않습니다.
사용 시 고려 사항
ORDER BY없이LIMIT를 사용하면 결과 순서가 보장되지 않습니다.- 동일한 query를 실행해도 다른 결과가 반환될 수 있습니다.
- pagination에서는 항상
ORDER BY를 명시해야 합니다.
OFFSET값이 전체 행 수보다 크면 빈 결과가 반환됩니다.- error가 발생하지 않으므로 application에서 빈 결과를 처리해야 합니다.
- 정렬 기준 column에 중복 값이 있으면 결과가 일관되지 않을 수 있습니다.
ORDER BY created_at만 사용하면 같은 시간에 생성된 행의 순서가 바뀔 수 있습니다.ORDER BY created_at, id처럼 고유한 column을 추가하면 순서가 보장됩니다.