ORM

Django ORMのスライス入門|指定範囲のデータを取得する方法

Django ORM

Django ORMのスライス入門
指定範囲のデータを取得する方法

Django ORMでPythonのスライス構文を使って、指定した範囲のデータを取得する方法を解説します。ページネーションの実装にも活用できます。

こんな人向けの記事です

  • Django ORMでデータの件数を制限して取得したい人
  • LIMITやOFFSETの使い方を知りたい人
  • ページネーションを実装したい人

Step 1スライスの基本(LIMIT)

Django ORMではPythonのスライス構文[start:stop]を使って取得範囲を指定できます。SQLのLIMIT句に変換されます。

Python
# models.py
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    view_count = models.IntegerField(default=0)

    def __str__(self):
        return self.title
Python
# 最初の5件を取得(LIMIT 5)
articles = Article.objects.all()[:5]
for a in articles:
    print(a.title)

# order_by()と組み合わせて、閲覧数の多い上位3件を取得
top_articles = Article.objects.order_by("-view_count")[:3]
for a in top_articles:
    print(f"{a.title}: {a.view_count}回")
実行結果
Django入門: 1500回
Python基礎: 1200回
ORM活用術: 980回

スライスはSQLのLIMITに変換されるため、全データをメモリに読み込んでから制限するのではなく、データベースレベルで件数を制限します。

Step 2オフセット付きスライス(OFFSET)

スライスの開始位置を指定すると、SQLのOFFSETに変換されます。

Python
# 6件目から10件目を取得(OFFSET 5 LIMIT 5)
articles = Article.objects.all()[5:10]

# 11件目から20件目を取得
articles = Article.objects.all()[10:20]

# 最新の記事を、3件目から5件取得
articles = Article.objects.order_by("-created_at")[2:7]
for a in articles:
    print(a.title)

# インデックスで1件取得(OFFSET n LIMIT 1)
third_article = Article.objects.order_by("-view_count")[2]
print(f"3位: {third_article.title}")
実行結果
3位: ORM活用術
負のインデックスは使えない
Django ORMのスライスではArticle.objects.all()[-1]のような負のインデックスはサポートされていません。最後のデータを取得したい場合はorder_by()で逆順にするか、last()メソッドを使ってください。

Step 3first()とlast()

最初の1件または最後の1件だけを取得するメソッドです。

Python
# 最初の1件を取得(該当なしの場合はNone)
article = Article.objects.order_by("created_at").first()
if article:
    print(f"最古: {article.title}")

# 最後の1件を取得
article = Article.objects.order_by("created_at").last()
if article:
    print(f"最新: {article.title}")

# filter()と組み合わせ
latest = Article.objects.filter(
    view_count__gte=100
).order_by("-created_at").first()

# first()とスライスの違い
# first(): データがない場合はNoneを返す
# [0]: データがない場合はIndexErrorが発生する
article = Article.objects.filter(id=9999).first()  # None
# article = Article.objects.filter(id=9999)[0]     # IndexError!
実行結果
最古: はじめてのPython
最新: Django ORM活用術
first()とlast()の安全性
first()last()はデータが存在しない場合にNoneを返すため、[0]のインデックスアクセスよりも安全です。常にfirst()を使うことを推奨します。

Step 4ページネーションの実装

Djangoには標準のPaginatorクラスがあり、スライスを内部的に活用してページネーションを実現します。

Python
# views.py
from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Article

def article_list(request):
    # 全記事を作成日降順で取得
    articles_all = Article.objects.order_by("-created_at")

    # 1ページあたり10件でページネーション
    paginator = Paginator(articles_all, 10)

    # URLパラメータからページ番号を取得
    page_number = request.GET.get("page", 1)
    page_obj = paginator.get_page(page_number)

    context = {
        "page_obj": page_obj,
        "total_count": paginator.count,      # 総件数
        "total_pages": paginator.num_pages,  # 総ページ数
    }
    return render(request, "article/list.html", context)
Python(テンプレート)
<!-- templates/article/list.html -->
{% for article in page_obj %}
  <div class="article">
    <h2>{{ article.title }}</h2>
    <p>{{ article.created_at }}</p>
  </div>
{% endfor %}

<!-- ページネーションリンク -->
<div class="pagination">
  {% if page_obj.has_previous %}
    <a href="?page={{ page_obj.previous_page_number }}">前へ</a>
  {% endif %}

  <span>{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>

  {% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}">次へ</a>
  {% endif %}
</div>

Step 5パフォーマンスの注意点

Python
# OFFSETが大きいとパフォーマンスが低下する
# BAD: 100万件目から10件取得(遅い)
articles = Article.objects.all()[1000000:1000010]

# GOOD: カーソルベースのページネーション(高速)
# 前のページの最後のIDを基準にする
last_id = request.GET.get("last_id", 0)
articles = Article.objects.filter(
    id__gt=last_id
).order_by("id")[:10]

# exists()で存在チェック(count()より高速)
if Article.objects.filter(view_count__gte=1000).exists():
    print("人気記事があります")

# スライス後のQuerySetではfilter()が使えない
# BAD: スライス後にfilter()
# articles = Article.objects.all()[:10].filter(view_count__gte=100)  # エラー

# GOOD: filter()してからスライス
articles = Article.objects.filter(view_count__gte=100)[:10]
カーソルベースのページネーション
大量データのページネーションでは、OFFSETベースよりもカーソルベース(前ページの最後のIDを基準にする方式)が高速です。Django REST Frameworkにはカーソルページネーションが標準で用意されています。

まとめ

  • Pythonのスライス構文[:n]でLIMIT、[m:n]でOFFSET+LIMITを指定できる
  • first()last()で安全に先頭・末尾のデータを取得できる
  • DjangoのPaginatorクラスで簡単にページネーションを実装できる
  • スライスはデータベースレベルで制限されるため効率的
  • 大量データではカーソルベースのページネーションを検討する