Django ORMで、
.distinct()を指定しているのに、なぜか同じデータが複数返ってくる。
そんなときは、JOINだけでなく**order_by()と実際に生成されているSELECT句**も確認した方がいい。
特に、
.values(...).order_by(...).distinct()を組み合わせている場合は要注意。
values()に書いていないフィールドが、実際のSQLではSELECTされていることがある。
まず確認するポイント
distinct()しているのに重複して見える場合、今回のケースでは次の部分がポイントになった。
.order_by( 'attribute_group__item_pf_atr_group__sort', 'attribute_group__sort', 'sort', 'id').values( 'id', 'value', 'attribute_group_id').distinct()注目するのは、values()とorder_by()で指定しているフィールドが違うこと。
values()は、
idvalueattribute_group_idだけ。
一方、order_by()ではJOIN先を含む複数のsortを使っている。
この状態で実際に発行されるSQLを確認する。
実際に生成されたSQL
今回、Django Debug Toolbarで確認したSQLがこちら。
SELECT DISTINCT `fjs_attribute`.`id`, `fjs_attribute`.`value`, `fjs_attribute`.`attribute_group_id`, `fjs_itempfatrgroup`.`sort`, `fjs_attributegroup`.`sort`, `fjs_attribute`.`sort` FROM `fjs_attribute` INNER JOIN `fjs_attributegroup` ON (`fjs_attribute`.`attribute_group_id` = `fjs_attributegroup`.`id`) LEFT OUTER JOIN `fjs_itempfatrgroup` ON (`fjs_attributegroup`.`id` = `fjs_itempfatrgroup`.`attribute_group_id`) WHERE ( `fjs_attribute`.`active` = 1 AND `fjs_attribute`.`attribute_group_id` IN (6, 7, 8) ) ORDER BY `fjs_itempfatrgroup`.`sort` ASC, `fjs_attributegroup`.`sort` ASC, `fjs_attribute`.`sort` ASC, `fjs_attribute`.`id` ASCORMではvalues()に指定していなかった、
`fjs_itempfatrgroup`.`sort`,`fjs_attributegroup`.`sort`,`fjs_attribute`.`sort`までSELECTされている。
ここがポイント。
なぜdistinct()しても重複するのか
例えばJOIN後のデータがこうなっていたとする。
| id | value | attribute_group_id | item_pf_sort |
|---|---|---|---|
| 10 | 3000 | 6 | 1 |
| 10 | 3000 | 6 | 2 |
values()だけを見ると、
10, 3000, 610, 3000, 6なので同じデータに見える。
当然、
.distinct()で1件になると思いたくなる。
ところが実際のSQLではitem_pf_sort相当のカラムもSELECTされている。
そのためDISTINCTから見ると、
10, 3000, 6, 110, 3000, 6, 2となる。
これは別の行なので、両方残る。
Django側で結果を見るとvalues()で指定したフィールドだけが見えるため、
{ "id": 10, "value": "3000", "attribute_group_id": 6,}という同じデータが複数返ってきたように見える。
つまり、
distinct()が効いていないのではなく、DISTINCTの対象になっているカラムが想定より多い
という状態。
Debug Toolbarで確認する
この現象を疑ったら、ORMだけを眺め続けるより実際のSQLを確認した方が早い。
Django Debug ToolbarなどでSQLを開いて、
SELECT DISTINCT ...のSELECT部分を見る。
例えばORMが、
.values( 'id', 'value', 'attribute_group_id')なのに、
SELECT DISTINCT id, value, attribute_group_id, some_sort, another_sortとなっていたら要注意。
values()に書いていないフィールドがDISTINCTの判定へ影響している可能性がある。
対処方法
今回のように、
- 重複をまとめたい
- JOIN先の値でも並べ替えたい
という場合は、単純なdistinct()ではなく、集約した値を使って並べる方法が考えられる。
例えばMin()を使う場合。
from django.db.models import Min
attrs_qs = ( Attribute.objects .filter( attribute_group__in=[g['id'] for g in groups], active=True ) .values( 'id', 'value', 'attribute_group_id' ) .annotate( pf_sort=Min( 'attribute_group__item_pf_atr_group__sort' ), group_sort=Min( 'attribute_group__sort' ), attr_sort=Min('sort'), ) .order_by( 'pf_sort', 'group_sort', 'attr_sort', 'id' ))values()でグルーピングし、並び順に必要な値をMin()などで代表値として取得する形。
ただし、Min()を使えば常に正解というわけではない。
JOIN先に複数のsortが存在する場合、
どの値を並び順として採用したいのか
はデータ構造や仕様によって変わる。
最小値を使うのか、最大値なのか、そもそもJOINの条件を絞るべきなのかは個別に判断する必要がある。
distinct()しても重複するときのチェックポイント
今回のケースから、まず確認したいのはこのあたり。
☑ JOINによって元データが複数行になっていないか
☑ values()には何を指定しているか
☑ order_by()でvalues()にないフィールドを使っていないか
☑ 実際のSELECT DISTINCTに何が含まれているか
☑ JOIN先のsortなど、行ごとに値が違うカラムがSELECTされていないか特に重要なのは、ORMだけで判断しないこと。
.values('id', 'value').distinct()だけを見ると、
「idとvalueでDISTINCTしている」
ように見える。
でも実際にDBへ送られているSQLがそうなっているとは限らない。
distinct()しているのに重複する場合は、
一度生成されたSQLのSELECT句を確認する。
これが一番手っ取り早い。
ORMが綺麗でも、SQLの裏側では知らんやつがSELECTに紛れ込んでいることがある。
実際にこの問題にハマって原因を調べた流れはこちら