소스: sql-src/work_01_paging/02_dialect_paging.sql, 03_keyset.sql. 아래 방언 예제는 모두 페이지 2(17번부터 8번, 10행)를 가져옵니다.
SET MODE Oracle 로 실행했습니다. 안쪽에서 정렬하고, 그 결과에 ROWNUM 을 붙여 <= 20 으로 자른 뒤, 바깥에서 rn > 10 으로 앞 10 행을 버립니다. 인라인 뷰의 별칭에는 AS 를 쓰지 않습니다(Oracle 은 오류이고 H2 에서 재현 안 됨, 문법 검토만).
SELECT
id
, title
, created_at
FROM (
SELECT
t.id
, t.title
, t.created_at
, ROWNUM rn
FROM (
SELECT id, title, created_at
FROM board
ORDER BY created_at DESC, id DESC
) t
WHERE ROWNUM <= 20
)
WHERE rn > 10
ORDER BY rn;ID | TITLE | CREATED_AT
---+-----------+-----------
17 | 게시글 17 | 2026-09-17
16 | 게시글 16 | 2026-09-16
15 | 게시글 15 | 2026-09-15
14 | 게시글 14 | 2026-09-14
13 | 게시글 13 | 2026-09-13
12 | 게시글 12 | 2026-09-12
11 | 게시글 11 | 2026-09-11
10 | 게시글 10 | 2026-09-10
9 | 게시글 09 | 2026-09-09
8 | 게시글 08 | 2026-09-07
(10행)안쪽 ROWNUM <= 20 은 정렬된 전체 행에 번호를 붙이지 않고 20 행에서 멈추게 하려는 것입니다. 흔한 실수는 ROWNUM 을 바깥에서 바로 비교하는 것입니다.
SELECT id, title FROM board WHERE ROWNUM > 10;ID | TITLE
---+------
(0행)첫 행에 ROWNUM 1 이 붙고 1 > 10 이 거짓이라 버려집니다. 다음 행이 다시 1 을 받으므로 끝까지 한 행도 통과하지 못합니다. ROWNUM > 1 이나 ROWNUM = 2 도 마찬가지로 0건입니다.
표준 분석 함수(고급 01 순위 함수)로 번호를 매기고 범위로 거릅니다. WHERE 에는 분석 함수를 직접 못 쓰므로 인라인 뷰가 필요합니다. ROWNUM 보다 읽기 쉽고 Oracle·MySQL 8.0·MSSQL 에서 모두 됩니다.
SELECT
id
, title
, created_at
FROM (
SELECT
id
, title
, created_at
, ROW_NUMBER() OVER (ORDER BY created_at DESC, id DESC) AS rn
FROM board
) t
WHERE rn BETWEEN 11 AND 20
ORDER BY rn;ID | TITLE | CREATED_AT
---+-----------+-----------
17 | 게시글 17 | 2026-09-17
16 | 게시글 16 | 2026-09-16
15 | 게시글 15 | 2026-09-15
14 | 게시글 14 | 2026-09-14
13 | 게시글 13 | 2026-09-13
12 | 게시글 12 | 2026-09-12
11 | 게시글 11 | 2026-09-11
10 | 게시글 10 | 2026-09-10
9 | 게시글 09 | 2026-09-09
8 | 게시글 08 | 2026-09-07
(10행)바깥에 ORDER BY rn 이 없으면 인라인 뷰의 정렬 순서가 유지된다고 보장되지 않으므로 항상 붙입니다. 전체 행에 번호를 붙이는 비용이 있어서 표준 OFFSET FETCH 가 되는 DB 라면 그쪽이 낫습니다.
SET MODE MySQL, SET MODE MSSQLServer 로 실행했고 아래 세 문장은 모두 예제 6 과 같은 10 행을 돌려줍니다.
-- MySQL
SELECT id, title, created_at FROM board ORDER BY created_at DESC, id DESC LIMIT 10 OFFSET 10;
SELECT id, title, created_at FROM board ORDER BY created_at DESC, id DESC LIMIT 10, 10;-- MSSQL
SELECT id, title, created_at FROM board ORDER BY created_at DESC, id DESC OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;MSSQL 의 TOP 은 첫 페이지 전용입니다. 앞을 건너뛰는 기능이 없어서 페이지 2 이상은 OFFSET FETCH(2012 부터) 나 ROW_NUMBER() 를 씁니다.
SELECT TOP 10 id, title, created_at FROM board ORDER BY created_at DESC, id DESC;ID | TITLE | CREATED_AT
---+-----------+-----------
27 | 게시글 27 | 2026-09-26
26 | 게시글 26 | 2026-09-24
25 | 게시글 25 | 2026-09-24
24 | 게시글 24 | 2026-09-24
23 | 게시글 23 | 2026-09-23
22 | 게시글 22 | 2026-09-22
21 | 게시글 21 | 2026-09-21
20 | 게시글 20 | 2026-09-20
19 | 게시글 19 | 2026-09-19
18 | 게시글 18 | 2026-09-17
(10행)주의MSSQL 의
OFFSET FETCH는ORDER BY가 없으면 문법 오류입니다. MySQL 의LIMIT는ORDER BY없이도 실행되지만 결과 순서가 불확실합니다. H2 의 방언 모드는 이 차이를 완전히 재현하지 않으므로 실제 DB 동작은 위 규칙을 기준으로 봅니다.
페이지 1 의 마지막 행이 (2026-09-17, 18) 이라고 하고, 그 뒤 10 행을 가져옵니다. 조건값은 WITH 의 파라미터 CTE 로 두었습니다. 실제로는 바인드 변수 :last_created, :last_id 로 받습니다.
WITH cur AS (SELECT DATE '2026-09-17' AS last_created, 18 AS last_id)
SELECT
b.id
, b.title
, b.created_at
FROM board b
CROSS JOIN cur
WHERE b.created_at < cur.last_created
OR (b.created_at = cur.last_created AND b.id < cur.last_id)
ORDER BY b.created_at DESC, b.id DESC
FETCH FIRST 10 ROWS ONLY;ID | TITLE | CREATED_AT
---+-----------+-----------
17 | 게시글 17 | 2026-09-17
16 | 게시글 16 | 2026-09-16
15 | 게시글 15 | 2026-09-15
14 | 게시글 14 | 2026-09-14
13 | 게시글 13 | 2026-09-13
12 | 게시글 12 | 2026-09-12
11 | 게시글 11 | 2026-09-11
10 | 게시글 10 | 2026-09-10
9 | 게시글 09 | 2026-09-09
8 | 게시글 08 | 2026-09-07
(10행)OFFSET 이 없습니다. 페이지 2 와 결과가 같지만 앞 행을 읽고 버리는 대신 조건으로 위치를 찾습니다. 17일 동점 행인 17번도 빠짐없이 나옵니다. 다음 호출은 방금 받은 마지막 행 (2026-09-07, 8) 을 넘깁니다.
WITH cur AS (SELECT DATE '2026-09-07' AS last_created, 8 AS last_id)
SELECT
b.id
, b.title
, b.created_at
FROM board b
CROSS JOIN cur
WHERE b.created_at < cur.last_created
OR (b.created_at = cur.last_created AND b.id < cur.last_id)
ORDER BY b.created_at DESC, b.id DESC
FETCH FIRST 10 ROWS ONLY;ID | TITLE | CREATED_AT
---+-----------+-----------
7 | 게시글 07 | 2026-09-07
6 | 게시글 06 | 2026-09-06
5 | 게시글 05 | 2026-09-05
4 | 게시글 04 | 2026-09-04
3 | 게시글 03 | 2026-09-03
2 | 게시글 02 | 2026-09-02
1 | 게시글 01 | 2026-09-01
(7행)7행만 돌아왔으니 마지막입니다. 10행을 요청해 10행보다 적게 오면 다음 페이지가 없다는 신호입니다.
이전 페이지로 가려면 부등호와 정렬 방향을 모두 뒤집어 읽고 화면에서 순서를 뒤집습니다. 소스 파일의 세 번째 쿼리가 그 예입니다. 페이지 2 의 첫 행 (2026-09-17, 17) 보다 앞선 3 행을 오름차순으로 읽으면 18번, 19번, 20번이 나옵니다.