◀ 6.

【django】limit, offsetの指定方法

▶

Django ORMのLIMIT・OFFSETは QuerySet のスライスで指定します。qs[20:30] は20件飛ばして最大10件、つまり21〜30件目の範囲です。右端の30は件数ではなく終了位置です。

一意な順序、スライスによる範囲、評価時のSQL実行を分ける図

先に順序を決めます。元の User 例はスライス構文だけの断片で、モデルの定義や並び順を省略しています。実用時は User.objects.order_by("pk")[:100] のように一意な順序を指定します。順位等が同じになる列なら order_by("rank", "pk") とし、同順位の最後を主キーで確定します。順序の固定だけで同時更新中のページを固定できるわけではありません。
この記事の要点
  • Django ORM でLIMIT / OFFSET を指定する方法(Python のスライス)
  • LIMIT のみ: User.objects.all()[:100]
  • LIMIT + OFFSET: User.objects.all()[20:30](= LIMIT 10 OFFSET 20)
  • QuerySet は遅延評価。未評価の通常スライスではまだSQLを実行せず、反復や list() 等で実行する(step付き等は別)

 

LIMITのみ

users = User.objects.all()[:100]

上記の例はユーザーを最大100名取得する範囲指定。100件未満なら存在する件数だけ返る。

 

LIMITとOFFSET

users = User.objects.all()[20:30]

LIMIT 10 OFFSET 20 と同義。21番目(offsetは0始まりなので)から30番目の10件を取得するということ。

スライスとSQLの対応

未評価の qs に対する操作意味・評価のタイミング
qs[:10] / qs[0:10]先頭から最大10件。OFFSET 0相当。SQLではOFFSET句が省略されることもある。
qs[20:30]LIMIT 10 OFFSET 20相当。件数は終了位置−開始位置。
qs[20:]20件飛ばし、残りすべて。上限なしは大量取得になり得る。実際のSQL表記はバックエンド依存。
qs[:0] / qs[3:3] / qs[5:2]空範囲。今回の実装では評価してもSQL 0本。qs[100:110] のようなデータ外の位置は、DB検索をして空になる(1本)。
qs[2]3件目のオブジェクトを直ちに取得。存在しなければ IndexError。qs[2:3] はQuerySetなので、存在しなければ評価結果が空になる。
qs[-1] / qs[:-1]負のインデックス・開始/終了位置は ValueError。評価済みQuerySetでも同じ。list(qs)[-1] はPythonのlist操作であり別物。
qs[2:8:2]step付き。範囲の6件をDBから取得後、Pythonで間引いてlistを返す。SQLで1行おきに取得する構文ではない。

ここでいうSQLは結果の意味の対応です。DB製品の構文一覧や生SQLを組み立てる手順ではありません。Djangoはバックエンドに応じてSQLを生成します。str(qs.query) はSQLの組立てを確認するだけで実行回数や速度の証明にはならず、条件に秘密値があれば公開しないでください。

list化・キャッシュ・stepを混同しない

未評価なら qs[20:30] 自体はSQL 0本で、list(qs[20:30]) が範囲だけを取得します。一方 list(qs)[20:30] は先にqsの全対象をメモリへ読み、その後でlistを切ります。先にqsを全件評価してある場合、qs[20:30] もキャッシュに対するlistスライスとなりSQLは増えません。別のQuerySetへ自動的に共有されるキャッシュではありません。

負のstepは負の位置とは別です。5.2.18の未評価 qs[:4:-1] は先頭4件を読み、Pythonで逆順にしたlistになります。DBの降順取得の代替にせず、降順はスライス前の order_by("-rank", "-pk") で指定します。step=0も使用しません。今回の実装では未評価の qs[:4:0] は通常の範囲指定として残る一方、評価済みqsではPython側の ValueError になりました。この実装差に依存した入力検証をせず、外部入力から任意のstepを受け付けない設計にします。

filter・order_byは範囲を切る前に

Entry.objects.filter(rank__gte=13).order_by("rank", "pk")[:2] の順に、対象→順序→範囲を決めます。実際に上限/オフセットのあるQuerySetへ後から filter()・order_by()・reverse() を追加すると TypeError になります。「先に3件取得してから、その3件をDBで絞る」という意味にはなりません。取得済みのlistを絞るならPython処理であり、DBのページングとは別です。[0:] のように制限が設定されないケースまで一律の禁止と解釈しません。

count・len・iteratorの取得範囲

未評価の qs.count() はCOUNTクエリで件数を調べ、結果オブジェクトのキャッシュは埋めません。スライス済みqsの count() はその範囲の件数であり、絞込み前の全体件数ではありません。len(qs) はそのQuerySetの全対象を取得してキャッシュします。十分に評価済みなら count() と len() はキャッシュを使いSQL 0本です。件数だけが必要なら、未評価の全件をlenで読む必要はありません。

iterator(chunk_size=2) はQuerySetの結果キャッシュを使わず、評価済みでも再実行します。チャンクサイズはページ件数でもSQLのLIMITでもなく、SQL実行回数が2件ごとに増える指定ではありません。今回の30件検体では、反復を2回始めればSELECTも2回です。DBドライバーのバッファや関連取得によるメモリ・クエリ差まで、この小さなSQLite試験から保証しません。

ページ番号とPaginator

1始まりのページ番号 page と1ページの size から、offset = (page - 1) * size を計算します。size=10なら page=1 は0、2は10、3は20です。qs[offset:offset+size] とし、負数・文字列・大きすぎるsizeを検査します。次の完成例ではboolも整数ページとして通さず、sizeを1〜100に制限します。page自体の上限やアクセス制限は実サービスの要件で別途定めます。

Djangoの Paginator(ordered(), 10) は件数とページ数の管理を追加します。今回の未評価30件では構築だけでSQL 0本、get_page("2") でCOUNT 1本、ページの反復で範囲のSELECT 1本でした。get_page("bad") は最初、範囲外の "999" や "0" は最後のページへ補正します。厳格に拒否したい場合の page() と方針が違います。空データでは既定で空の第1ページを許し、設定により扱いが変わります。

生SQLの COUNT(*) OVER () による全体件数の付加は別の設計です。OFFSET後の結果が0行なら、その行から全体件数を受け取れません。ここではウィンドウ関数や他製品の構文をORMスライスの代用品として実装していません。

OFFSETとカーソル方式の限界

深いOFFSETはDBによって読み飛ばしの負担が大きくなり得ますが、「必ず指数的に遅い」「常にOFFSET+LIMIT行だけを走査」とは断定できません。実データ・索引・条件・実行計画で測定します。単なるID範囲指定は、IDの欠番や検索条件によって件数が変わり、位置指定と同じではありません。

rank, pk の昇順なら、前ページ末尾の組より後ろ、すなわち rank が大きいか、rank が同じで pk が大きい条件を使うキースト方式を選べます。例の after_pair() はNULLのない固定昇順専用で、rankの同順位も取りこぼさない比較です。rank順なのにpkだけをカーソルにしてはいけません。降順・NULL・照合順序・検索条件が変われば比較設計も変わります。

今回の連続実行検体では、先頭2件取得後に前方へ1件挿入すると次のOFFSETページに既出の2が再登場し、前方の1件を削除すると未読の3を飛ばしました。固定した組の境界はこの移動を避けましたが、読了行のrankを後ろへ更新すると再登場し、未読行のrankを前へ更新すると欠落します。カーソルも同時更新に対するスナップショットではなく、重複・抜けなしや一定速度を保証しません。

公開APIにするならカーソルへ並び順・検索条件を対応させ、改ざん対策、認可、件数上限、期限、必要なら一貫性の設計も必要です。下の関数はページ番号への任意ジャンプや公開トークンを実装していません。DBのサーバーサイドカーソルや iterator() と、ページングの境界トークンも別の意味です。

単独で実行するSQLite教材

次は既存アプリへ読み込まず、新しいPythonプロセスで動かすファイル全体です。Python 3.12.10 / Django 5.2.18 / SQLite 3.49.1で確認しました。専用 :memory: テーブルを作成し、同順位を含む30件で検証後に破棄します。外部DB・本番データ・既存マイグレーションは使いません。Userモデルの代わりに教材のEntryモデルを使います。

"""Standalone teaching fixture. Never import project settings or run migrations."""

from contextlib import contextmanager

from django.conf import settings

if settings.configured:
    raise RuntimeError("Run in a fresh process, not inside an existing Django app")

settings.configure(
    INSTALLED_APPS=[],
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
    SECRET_KEY="isolated-paging-fixture-not-a-production-secret",
    DEFAULT_AUTO_FIELD="django.db.models.AutoField",
)

import django

django.setup()

from django.core.paginator import Paginator
from django.db import connection, models
from django.db.models import Q


class Entry(models.Model):
    rank = models.IntegerField()

    class Meta:
        app_label = "isolated_paging"
        db_table = "isolated_paging_entry"


@contextmanager
def fixture():
    # Only a new private in-memory table; no external database or migration.
    with connection.schema_editor() as editor:
        editor.create_model(Entry)
    try:
        Entry.objects.bulk_create(
            [Entry(id=pk, rank=(pk - 1) // 2) for pk in range(1, 31)]
        )
        yield
    finally:
        with connection.schema_editor() as editor:
            editor.delete_model(Entry)


def ordered():
    return Entry.objects.order_by("rank", "pk")


def offset_page(qs, page, size=10):
    # page is 1-based; offset is 0-based. Bound untrusted input separately.
    if type(page) is not int or page < 1:
        raise ValueError("page must be a positive integer")
    if type(size) is not int or not 1 <= size <= 100:
        raise ValueError("size must be between 1 and 100")
    offset = (page - 1) * size
    return qs[offset : offset + size]


def after_pair(rank, pk, size=10):
    # Fixed ascending, non-null fields. This is not a public cursor protocol.
    if type(size) is not int or not 1 <= size <= 100:
        raise ValueError("size must be between 1 and 100")
    return ordered().filter(Q(rank__gt=rank) | Q(rank=rank, pk__gt=pk))[:size]


def demo():
    qs = ordered()
    page = qs[20:30]  # Still unevaluated; equivalent to LIMIT 10 OFFSET 20.
    print("slice", [row.pk for row in page])
    print("offset", [row.pk for row in offset_page(ordered(), 3)])
    print("step", [row.pk for row in ordered()[:6:2]])  # Immediate list.
    print("cursor", [row.pk for row in after_pair(0, 1, size=3)])
    paginator = Paginator(ordered(), 10)
    print("paginator", paginator.count, paginator.num_pages)
    print("last", [row.pk for row in paginator.get_page("999")])


if __name__ == "__main__":
    with fixture():
        demo()

ビューとテンプレートでのPaginator利用

以下は既存の説明用断片です。views.py 部分には別途 from django.shortcuts import render と、利用するUserモデルのimportが必要です。「# テンプレート」以降はPythonではなく users.html に分けます。Userにname属性がある前提で、単独ファイルとしては実行できません。順序は一意なid、get_page()は不正値を補正する方式です。実ビューの認可・検索条件・テンプレート描画は、この隔離ORM教材の検証対象外です。

# 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 %}
    <a href="?page={{ page_obj.previous_page_number }}">前へ</a>
{% endif %}
ページ {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}">次へ</a>
{% endif %}

確認範囲と関連ガイド

2026年10月26日確認。隔離ORMの結果・SQL本数・キャッシュ・例外と、順次の挿入/削除/順位変更による境界の移動を検査しています。実MySQL/PostgreSQL/Oracle/SQL Server、並列トランザクション、性能、索引の効果、実ビューとテンプレート、公開カーソルの認可は未検証です。

データベースへの接続と操作配下の個別ガイドとして、検索条件はfilterの検索オプション、SQLの実行観測はSQLのログ出力方法、生SQLとの境界は素のSQLを直接実行する方法を参照してください。ページ数・リンクの管理にはDjango公式のPaginatorも参照できます。

子ページ

子ページはありません

同階層のページ
  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)

最近更新/作成されたページ