5.

Django で降順に並べ替える|order_by の書き方と NULL の扱い

編集
この記事の要点
  • 降順は フィールド名の前にマイナスorder_by("-created_at")
  • 複数キーは並べるだけ。order_by("-score", "name")左が優先
  • order_by() を引数なしで呼ぶと、Meta.ordering を含めて並び順が消える
  • リレーション先は アンダースコア 2 個でたどる。order_by("-author__age")
  • NULL の位置は DB によって違う。揃えたいなら F("x").desc(nulls_last=True)

基本

Article.objects.order_by("created_at")     # 昇順(古い順)
Article.objects.order_by("-created_at")    # 降順(新しい順)

# 複数キー: 点数の高い順、同点なら名前の五十音順
Article.objects.order_by("-score", "title")

# 発行される SQL
print(Article.objects.order_by("-created_at").query)
# SELECT ... FROM "blog_article" ORDER BY "blog_article"."created_at" DESC

マイナスはフィールド名の直前に付けます。order_by("created_at DESC") のように SQL のキーワードを書くと FieldError になります。

並び順を後から変える・消す

qs = Article.objects.filter(published=True)

qs = qs.order_by("-created_at")     # 追加ではなく「置き換え」
qs = qs.order_by("title")           # 前の指定は消え、title 順になる

qs.reverse()                        # 指定済みの並びを逆にする
qs.order_by()                       # 並び順をすべて解除する

order_by()累積しません。2 回呼んだら後の指定だけが残ります。段階的に組み立てたい場合はリストにためて最後に一度だけ渡します。

order = []
if sort_by_score:
    order.append("-score")
order.append("-created_at")

qs = Article.objects.order_by(*order)

モデルの既定の並び順

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

    class Meta:
        ordering = ["-created_at"]        # 何も指定しなければこの順

Meta.ordering を付けると、すべてのクエリに ORDER BY が付きます。件数が多いテーブルでは、意図しないソートが性能に効いてくることがあります。

集計と組み合わせるときは特に注意が必要です。

from django.db.models import Count

# Meta.ordering のフィールドが GROUP BY に混ざり、結果が分かれてしまう
Article.objects.values("author").annotate(n=Count("id"))

# 集計の前に並び順を解除する
Article.objects.order_by().values("author").annotate(n=Count("id"))

リレーション先で並べる

Article.objects.order_by("-author__age")            # 著者の年齢の降順
Article.objects.order_by("author__name", "-score")  # 著者名順、同著者内は点数降順

# JOIN が増えるので select_related と併用する
Article.objects.select_related("author").order_by("-author__age")

アンダースコア 2 個でリレーションをたどります。1 個だとフィールド名として解釈され FieldError: Cannot resolve keyword になります。

集計値・計算値で並べる

from django.db.models import Count, F, Value
from django.db.models.functions import Coalesce, Lower

# コメント数の多い順
Article.objects.annotate(n=Count("comments")).order_by("-n")

# 計算した値で並べる
Article.objects.annotate(rate=F("likes") * 100 / F("views")).order_by("-rate")

# 大文字小文字を無視して並べる
Article.objects.order_by(Lower("title"))

# NULL を 0 として扱う
Article.objects.order_by(Coalesce("score", Value(0)).desc())

NULL の位置を制御する

from django.db.models import F

Article.objects.order_by(F("published_at").desc(nulls_last=True))
Article.objects.order_by(F("published_at").asc(nulls_first=True))
DB既定の NULL の位置(昇順)
PostgreSQL最後
MySQL / MariaDB最初
SQLite最初

開発が SQLite で本番が MySQL といった構成では、同じコードでも並びが変わりますnulls_last を明示すれば揃います。

ランダム・件数制限との組み合わせ

Article.objects.order_by("?")[:5]                    # ランダム 5 件(大量データでは重い)
Article.objects.order_by("-created_at")[:10]         # 新着 10 件
Article.objects.order_by("-created_at")[10:20]       # 11〜20 件目

Article.objects.latest("created_at")                 # 最も新しい 1 件
Article.objects.earliest("created_at")               # 最も古い 1 件

スライスした後は order_by()filter() を呼べませんCannot filter a query once a slice has been taken.)。絞り込みと並べ替えを済ませてから最後にスライスします。件数制限の詳細は limit, offsetの指定方法 を参照してください。

大量データで遅いとき

class Article(models.Model):
    created_at = models.DateTimeField(db_index=True)      # 単一キー

    class Meta:
        indexes = [
            models.Index(fields=["-created_at", "id"]),   # 複合+降順
        ]

ORDER BY に使う列にインデックスが無いと、DB は毎回すべての行を並べ替えます。並び順とインデックスの順序を一致させるのが基本です。実際に効いているかは qs.explain() で確認できます(SQLのログ出力方法)。

関連

編集
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)

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