Django ORMのLIMIT・OFFSETは QuerySet のスライスで指定します。qs[20:30] は20件飛ばして最大10件、つまり21〜30件目の範囲です。右端の30は件数ではなく終了位置です。
| この記事の要点 |
|---|
|
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も参照できます。
- Django公式:QuerySetの範囲指定
- Django公式:評価・順序・count・iterator
- Django公式:ページング
- Django公式:Paginatorの補正と性能上の注意
- Django 5.2.18:QuerySet実装
- SQLite公式:SELECT・ORDER BY・LIMIT
子ページはありません
人気ページ
- 1 Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法
- 2 tomcat の起動 / 停止ログと catalina.log・catalina.out の違い
- 3 JavaScript で base URL を取得する方法|window.location.origin
- 4 YouTube Data API v3 エラー一覧|403・400・404 の原因と対処
- 5 Laravel エラー一覧|500/Blade/DB 接続/ルーティングの代表エラー
- 6 3Dグラフィックスのソフト一覧|Blender・Metasequoiaと用途別の選び方
- 7 Spring Frameworkのアノテーション一覧
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- Visual Studio「デザイナーを表示できません」エラーの原因と対処 2026-10-28 12:06:00
- SSH 認証に失敗しました。再試行してください の対処 2026-10-28 12:06:00
- SSH2 秘密鍵の読み込みに失敗しました の原因と対処 2026-10-28 12:06:00
- TeraTerm のよくあるエラーと対処法完全ガイド — Connection refused / 2026-10-28 12:06:00
- Visual Studio エラー一覧|コンパイルエラー・警告の確認方法と対処 2026-10-28 11:39:57
- Whitelabel Error Page の原因と消し方|Spring Boot のエラーページ 2026-10-28 11:39:57
- Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法 2026-10-28 11:39:57
- SQL WHERE 句完全ガイド 2026-10-28 11:39:57
- SQL JOIN 完全ガイド (INNER/LEFT/RIGHT/FULL OUTER) 2026-10-28 11:39:57
- tomcat の起動 / 停止ログと catalina.log・catalina.out の違い 2026-10-28 10:24:23
- Tomcat の起動確認方法 5 つ|localhost:8080・ポート・プロセス・ログ 2026-10-28 10:24:23
- Oracle 19c COMMITの使い方: 暗黙コミット・自動確定・応答不明時の確認 2026-10-28 09:59:09
- Oracle 19c DELETE文|削除対象の確認・COMMIT前の判断・ROLLBACKの条件 2026-10-28 09:59:09
- Oracle 19c INSERT文|INSERT ALL・SELECT・RETURNINGとMERGEの使い分け 2026-10-28 09:59:09
- Oracle 19cのトランザクション制御|COMMIT・ROLLBACK・分離レベルと接続設定 2026-10-28 09:59:09