6.

【django】limit, offsetの指定方法

編集
この記事の要点
  • SQL の LIMIT / OFFSETページネーションに使う
  • MySQL / PostgreSQL / SQLite: LIMIT n OFFSET m または LIMIT m, n (MySQL のみ)
  • Oracle: FETCH NEXT n ROWS ONLY OFFSET m ROWS (12c+) または ROWNUM
  • SQL Server: OFFSET m ROWS FETCH NEXT n ROWS ONLY
  • 大量データの OFFSET は遅い → カーソルベース(cursor pagination)推奨

 

基本構文

MySQL / PostgreSQL / SQLite

-- LIMIT のみ: 先頭 10 件
SELECT * FROM users LIMIT 10;

-- LIMIT + OFFSET: 11-20 件目 (1 ページ 10 件、2 ページ目)
SELECT * FROM users LIMIT 10 OFFSET 10;

-- MySQL 固有: カンマ区切り(OFFSET, LIMIT)
SELECT * FROM users LIMIT 10, 10;  -- ← 同じ意味

-- ORDER BY 必須 (順序保証)
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;

Oracle (12c+)

-- FETCH NEXT 構文
SELECT * FROM users
ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

-- 旧 (Oracle 11g 以前)
SELECT * FROM (
    SELECT t.*, ROWNUM AS rn
    FROM (SELECT * FROM users ORDER BY id) t
    WHERE ROWNUM <= 30
) WHERE rn > 20;

SQL Server (2012+)

SELECT * FROM users
ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

-- 旧 (SQL Server 2008)
SELECT TOP 10 * FROM (
    SELECT TOP 30 * FROM users ORDER BY id
) t ORDER BY id DESC;

ページネーションの計算

-- ページ番号 → OFFSET 変換
-- page = 1, pageSize = 20 → OFFSET 0
-- page = 2, pageSize = 20 → OFFSET 20
-- page = 3, pageSize = 20 → OFFSET 40

-- 計算式: OFFSET = (page - 1) * pageSize

-- ページ数取得
SELECT COUNT(*) FROM users;
-- 総件数 / pageSize で切り上げ

-- 1 クエリで取得 + 件数も
SELECT *,
    COUNT(*) OVER () AS total_count
FROM users
ORDER BY id
LIMIT 10 OFFSET 20;

JPA / Hibernate

// Spring Data JPA の Pageable
@GetMapping("/users")
public Page list(@RequestParam(defaultValue = "0") int page,
                       @RequestParam(defaultValue = "20") int size) {
    Pageable pageable = PageRequest.of(page, size, Sort.by("id"));
    return userRepository.findAll(pageable);
    // → Page オブジェクトに total count, page count 等含まれる
}

// JPQL で直接
@Query("SELECT u FROM User u ORDER BY u.id")
List findUsers(Pageable pageable);

// EntityManager
List users = em.createQuery("FROM User ORDER BY id", User.class)
    .setFirstResult(20)  // OFFSET
    .setMaxResults(10)   // LIMIT
    .getResultList();

Laravel

// Eloquent
$users = User::orderBy('id')->skip(20)->take(10)->get();
// または
$users = User::orderBy('id')->offset(20)->limit(10)->get();

// 自動ページネーション
$users = User::orderBy('id')->paginate(20);
// → LengthAwarePaginator (件数 + リンク生成)

// API レスポンス向け simple paginate (件数取得なし、高速)
$users = User::orderBy('id')->simplePaginate(20);

// Blade テンプレート
{{ $users->links() }}  // ページネーションリンク自動生成

Django

# views.py
from django.core.paginator import Paginator

def user_list(request):
    users = User.objects.all().order_by('id')
    paginator = Paginator(users, 20)  # 1 ページ 20 件
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)
    return render(request, "users.html", {"page_obj": page_obj})

# テンプレート
{% for user in page_obj %}
    {{ user.name }}
{% endfor %}

{% if page_obj.has_previous %}
    前へ
{% endif %}
ページ {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %}
    次へ
{% endif %}

OFFSET の罠(パフォーマンス)

大量データの後半ページはOFFSET が大きくなるほど遅い:

-- OFFSET 1000000 LIMIT 10
SELECT * FROM big_table ORDER BY id LIMIT 10 OFFSET 1000000;
-- → DB は 1000010 行スキャンしてから先頭 100 万を捨てる (遅い)

-- 対処 1: カーソルベースページネーション (推奨)
-- 前ページの最後の id を覚えておく
SELECT * FROM big_table
WHERE id > 1000000  -- ← 前ページの最後の id
ORDER BY id
LIMIT 10;
-- → INDEX が効く、高速

-- 対処 2: ID で範囲指定
SELECT * FROM big_table
WHERE id BETWEEN 1000001 AND 1000100;

カーソルベースページネーション(推奨)

-- 初回
GET /api/users?limit=20
SELECT * FROM users ORDER BY id LIMIT 20;
-- レスポンス: { users: [...], next_cursor: "20" }

-- 次ページ
GET /api/users?limit=20&cursor=20
SELECT * FROM users WHERE id > 20 ORDER BY id LIMIT 20;
-- → INDEX で高速

-- 複合キーの場合
SELECT * FROM users
WHERE (created_at, id) < ('2026-05-15 12:00:00', 100)  -- 前ページの最後
ORDER BY created_at DESC, id DESC
LIMIT 20;

# メリット:
# - パフォーマンス安定 (OFFSET 大きくても遅くならない)
# - INSERT が間に入っても重複・抜けなし
#
# デメリット:
# - ページ番号を直接指定できない (前後移動のみ)
# - SNS / 無限スクロール向き

大量データの効率的 SELECT (バッチ処理)

-- ❌ ダメ: OFFSET でループ
$offset = 0;
while (true) {
    $rows = $db->query("SELECT * FROM big_table LIMIT 1000 OFFSET $offset");
    if (empty($rows)) break;
    process($rows);
    $offset += 1000;
}
-- → OFFSET が大きくなるにつれて指数的に遅くなる

-- ✅ 良い: 最後の id で次バッチ
$lastId = 0;
while (true) {
    $rows = $db->query("SELECT * FROM big_table WHERE id > $lastId ORDER BY id LIMIT 1000");
    if (empty($rows)) break;
    process($rows);
    $lastId = end($rows)["id"];
}
-- → 各クエリが同じ速度

関連記事

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. MySQL/MariaDBへの接続
  2. sqliteへの接続
  3. SELECT, INSERT, UPDATE, DELETE
  4. 素のSQLを直接実行する方法
  5. Order by DESCの指定方法
  6. limit, offsetの指定方法
  7. filterの検索オプション
  8. django-filterのlookup_expr検索オプション
  9. モデルの内部結合(1対1)