Django の order_by の 注意点

  • 9
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

  • 昔 Django の order_by でどハマりした時のメモが発掘されたので残しておく
  • Django で order_byvaluesdistinct を同時に使うと意図しない結果が得られてしまうので注意という話

環境

  • Django1.9
  • MySQL

前提

  • 下記のようなDjangoモデルがあるとする
# polls/models.py

class Question(models.Model):

    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):

    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
  • あらかじめこんなデータが入ってることとする
q1 = Question.objects.get(question_text='question1')
q2 = Question.objects.get(question_text='question2')

Choice.objects.create(question=q1, choice_text='choice1', votes=1)
Choice.objects.create(question=q1, choice_text='choice2', votes=1)
Choice.objects.create(question=q2, choice_text='choice3', votes=1)
Choice.objects.create(question=q2, choice_text='choice4', votes=1)

1.SELECTするカラムを絞る

  • Choice の外部キーであるquestion(=question_id)だけをSELECTする
# valuesで絞る --
Choice.objects.values('question')
-- 実行されるSQL
mysql> SELECT `polls_choice`.`question_id` FROM `polls_choice`;
+-------------+
| question_id |
+-------------+
|           1 |
|           1 |
|           2 |
|           2 |
+-------------+
4 rows in set (0.00 sec)

2. 1にdistinctを加える

# distinct で 重複を削除する  --
Choice.objects.values('question').distinct()
-- 実行されるSQL
mysql> SELECT DISTINCT `polls_choice`.`question_id` FROM `polls_choice`;
+-------------+
| question_id |
+-------------+
|           1 |
|           2 |
+-------------+
2 rows in set (0.00 sec)
  • ちゃんと重複排除できている

3. 2にorder_byを加える

# valuesで絞ったフィールドとは別のフィールドで order_by する
Choice.objects.values('question').distinct().order_by('-id')
-- 実行されるSQL
-- なぜか、order_byで指定したフィールドがSELECTされてる...
mysql> SELECT DISTINCT `polls_choice`.`question_id`, `polls_choice`.`id` FROM `polls_choice` ORDER BY `polls_choice`.`id` DESC;
+-------------+----+
| question_id | id |
+-------------+----+
|           2 |  4 |
|           2 |  3 |
|           1 |  2 |
|           1 |  1 |
+-------------+----+
4 rows in set (0.00 sec)
  • え? なぜかSELECT対象にidが追加されている。。。
  • おかげでdistinct question_idとしたつもりが出来ていない。。。。
  • 上記はコード上にorder_byが現れるからまだ原因特定しやすい
  • 下記のようにMetaorderingされてたりすると見つけづらくて最悪である。
class Choice(models.Model):

    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    class Meta:
        ordering = ('-id',)
  • もしデフォルトのorderingが設定されてる場合は、意図的にorder_by()を空で読んで無効化するなどの対処が必要になる。
Choice.objects.values('question').distinct().order_by()

Django でorder_byすると指定したフィールドは SELECT される

  • SELECT に含まれるのはしょうがないので、order_byvalues/only/distinct などと組み合わせる場合は結果の一意性を損なう可能性があるので気をつけなければならない。
  • この辺の話は、下記参考URLのDjangoのドキュメントに注意点として記載されている。

Postgres の場合は DISTINCT ON が使える

  • バックエンドがPostgresの場合はDISTINCT ONが使えるので、distinctメソッドに対象を渡すことができる
Choice.objects.values('question').distinct('question').order_by('question', '-id')

まとめ

  • distinct/values/only/defer + order_by を使う時はどういうSQLが発行されるか注意しような > 俺。

参考