web-dev-qa-db-ja.com

django注釈とカウント:カウントに含めるものをフィルタリングする方法

クエリセットを指定して、関連するオブジェクト(ModelA)の数を次のように追加します。

qs = User.objets.all()
qs.annotate(modela__count=models.Count('modela'))

しかし、基準を満たすだけのModelAを数える方法はありますか?たとえば、deleted_atがnullであるModelAをカウントしますか?

正しく機能しない2つの解決策を試しました。

1)@knbkが提案したように、注釈を付ける前にフィルターを使用します。

qs = User.objects.all().filter(modela__deleted_at__isnull=True).annotate(modela__count=models.Count('modela', distinct=True))

Djangoによって生成されたクエリの簡略版は次のとおりです。

SELECT COUNT(DISTINCT "modela"."id") AS "modela__count", "users".*
FROM "users"
LEFT OUTER JOIN "modela" ON ( "users"."id" = "modela"."user_id" ) 
WHERE "modela"."deleted_at" IS NULL 
GROUP BY "users"."id"

問題はWHERE句にあります。確かに、LEFT JOINがありますが、後のWHERE条件により、プレーンJOINになりました。意図したとおりに機能させるには、条件をJOIN句にプルアップする必要があります。

だから、代わりに

LEFT OUTER JOIN "modela" ON ( "users"."id" = "modela"."user_id" ) 
WHERE "modela"."deleted_at" IS NULL

プレーンSQLで直接実行すると機能する次のものが必要です。

LEFT OUTER JOIN "modela" ON ( "users"."id" = "modela"."user_id" ) 
AND "modela"."deleted_at" IS NULL

生のクエリを実行せずにこれを取得するようにクエリセットを変更するにはどうすればよいですか?

2)他の人が示唆しているように、条件付き集計を使用できます。

私は以下を試しました:

qs = User.objects.all().annotate(modela__count=models.Count(Case(When(modela__deleted_at__isnull=True, then=1))))

これは次のSQLクエリになります。

SELECT COUNT(CASE WHEN "modela"."deleted_at" IS NULL THEN 1 ELSE NULL END) AS "modela__count", "users".*
FROM "users" LEFT OUTER JOIN "modela" ON ( "users"."id" = "modela"."user_id" )
GROUP BY "users"."id"

そうすることで、すべてのユーザーを取得します(したがって、LEFT JOINは正しく機能します)が、ModelAをまったく持っていないすべてのユーザーのmodela__countに対して(0ではなく)「1」を取得します。カウントするものがないのに、なぜ0ではなく1を取得するのですか?どうすればそれを変えることができますか?

17
Michael

LEFT JOINでは、対応する行がないため、modelaのすべてのフィールドがNULLになる可能性があります。そう

modela.deleted_at IS NULL

...一致する行だけでなく、対応するusers行がないmodelaにも当てはまります。

適切なSQLは次のようになります。

SELECT COUNT(
    CASE
      WHEN
        `modela`.`user_id` IS NOT NULL  -- Make sure modela rows exist
        AND `modela`.`deleted_at` IS NULL
        THEN 1
      ELSE NULL
    END
  ) AS `modela__count`,
  `users`.*
FROM `users`
LEFT OUTER JOIN `modela`
  ON ( `users`.`id` = `modela`.`user_id` )
GROUP BY `users`.`id`

Django 1.8では、これは次のようになります。

from Django.db import models
qs = User.objects.all().annotate(
    modela_count=models.Count(
        models.Case(
            models.When(
                modela__user_id__isnull=False,
                modela__deleted_at__isnull=True,
                then=1,
            )
        )
    )
)

通知

@YAmikepは、Django 1.8.のバグにより、生成されたSQLにINNER JOINではなくLEFT JOINが含まれることを発見しました。対応する外部キー関係のない行を失います。Django 1.8.2以降バージョンを使用して修正します。

37
Rockallite

In Django 1.8これは 条件付き集計 で実現できると思います。ただし、以前のバージョンでは .extra

ModelA.objects.extra(select={
    'account_count': 'SELECT COUNT(*) FROM account WHERE modela.account_id = account.id AND account.some_prop IS NOT NULL'
})
2
Todor

注釈を付ける前に、単純にフィルタリングできます。

from Django.db.models import Q, Count

qs = ModelA.objects.filter(account__prop1__isnull=False).annotate(account_count=Count('account'))
0
knbk