Django の order_by の 注意点

## 概要

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

## 環境

- Django1.9
- MySQL

## 前提

- 下記のようなDjangoモデルがあるとする

```python
# 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)
```

- あらかじめこんなデータが入ってることとする

```python
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する

```python
# valuesで絞る --
Choice.objects.values('question')
```

```sql
-- 実行される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`を加える

```python
# distinct で 重複を削除する  --
Choice.objects.values('question').distinct()
```

```sql
-- 実行される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`を加える

```python
# valuesで絞ったフィールドとは別のフィールドで order_by する
Choice.objects.values('question').distinct().order_by('-id')
```

```sql
-- 実行される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`が現れるからまだ原因特定しやすい
- 下記のように`Meta`で`ordering`されてたりすると見つけづらくて**最悪**である。

```python
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()`を空で読んで無効化するなどの対処が必要になる。

```python
Choice.objects.values('question').distinct().order_by()
```

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

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

## Postgres の場合は DISTINCT ON が使える

- バックエンドがPostgresの場合は`DISTINCT ON`が使えるので、`distinct`メソッドに対象を渡すことができる

```python
Choice.objects.values('question').distinct('question').order_by('question', '-id')
```

## まとめ

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

## 参考

- [Djangoでpostgresのdistinct onを実行する](http://d.hatena.ne.jp/yuheiomori0718/20120914/1347634603)
- [Django1.9 > QuerySet API reference > distinct](https://docs.djangoproject.com/ja/1.9/ref/models/querysets/#distinct)
- [Django1.4(日本語訳) > QuerySet API reference > distinct](http://docs.djangoproject.jp/en/latest/ref/models/querysets.html#django.db.models.query.QuerySet.distinct)