[Fixed]-Django annotate and count: how to filter the ones to include in count

39πŸ‘

βœ…

In a LEFT JOIN, every field of modela could be NULL because of the absence of corresponding row. So

modela.deleted_at IS NULL

…is not only true for the matching rows, but also true for those users whose have no corresponding modela rows.

I think the right SQL should be:

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`

In Django 1.8 this should be:

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,
            )
        )
    )
)

Notice:

@YAmikep discovered that a bug in Django 1.8.0 makes the generated SQL have an INNER JOIN instead of a LEFT JOIN, so you will lose rows without corresponding foreign key relationship. Use Django 1.8.2 or above version to fix that.

πŸ‘€Rockallite

2πŸ‘

In Django 1.8 I believe this can be achieved with conditional aggregation . However for previous versions I would do it with .extra

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

0πŸ‘

You can simply filter before you annotate:

from django.db.models import Q, Count

qs = ModelA.objects.filter(account__prop1__isnull=False).annotate(account_count=Count('account'))
πŸ‘€knbk

Leave a comment