1219 words
6 minutes
Djangoでdistinct()しても重複するときに確認するポイント

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()は、

id
value
attribute_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` ASC

ORMではvalues()に指定していなかった、

`fjs_itempfatrgroup`.`sort`,
`fjs_attributegroup`.`sort`,
`fjs_attribute`.`sort`

までSELECTされている。

ここがポイント。

なぜdistinct()しても重複するのか#

例えばJOIN後のデータがこうなっていたとする。

idvalueattribute_group_iditem_pf_sort
10300061
10300062

values()だけを見ると、

10, 3000, 6
10, 3000, 6

なので同じデータに見える。

当然、

.distinct()

で1件になると思いたくなる。

ところが実際のSQLではitem_pf_sort相当のカラムもSELECTされている。

そのためDISTINCTから見ると、

10, 3000, 6, 1
10, 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に紛れ込んでいることがある。


実際にこの問題にハマって原因を調べた流れはこちら

Django ORMでdistinct()が効いてるはずなのに重複する。犯人はMySQLだった話