#34840: Django 4.2 casts text fields when testing IS NULL, preventing use of
partial indexes
-------------------------------------+-------------------------------------
Reporter: Alex | Owner: nobody
Vandiver |
Type: | Status: new
Uncategorized |
Component: Database | Version: 4.2
layer (models, ORM) |
Severity: Normal | Keywords:
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
The Zulip project has a model with unique constraints which are expressed
as two non-overlapping partial indexes:
{{{
class UserCount(models.Model):
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
realm = models.ForeignKey(Realm, on_delete=models.CASCADE)
property = models.CharField(max_length=32)
subgroup = models.CharField(max_length=16, null=True)
end_time = models.DateTimeField()
value = models.BigIntegerField()
class Meta:
constraints = [
UniqueConstraint(
fields=["user", "property", "subgroup", "end_time"],
condition=Q(subgroup__isnull=False),
name="unique_user_count",
),
UniqueConstraint(
fields=["user", "property", "end_time"],
condition=Q(subgroup__isnull=True),
name="unique_user_count_null_subgroup",
),
]
}}}
However, since commit 09ffc5c1212d4ced58b708cbbf3dfbfb77b782ca in Django
4.2, a query of the form:
{{{
UserCount.objects.get(
property="messages_read::hour",
subgroup=None,
end_time=timezone_now(),
user_id=user_profile.id,
realm_id=realm.id,
)
}}}
...generates this SQL:
{{{
SELECT "analytics_usercount"."id", "analytics_usercount"."property",
"analytics_usercount"."subgroup", "analytics_usercount"."end_time",
"analytics_usercount"."value", "analytics_usercount"."user_id",
"analytics_usercount"."realm_id" FROM "analytics_usercount" WHERE
("analytics_usercount"."end_time" =
'2023-09-13T19:16:34.195355+00:00'::timestamptz AND
"analytics_usercount"."property" = 'messages_read::hour' AND
"analytics_usercount"."realm_id" = 4715 AND
"analytics_usercount"."subgroup"::text IS NULL AND
"analytics_usercount"."user_id" = 428054) LIMIT 21
}}}
The cast of `"analytics_usercount"."subgroup"::text IS NULL` causes
PostgreSQL to not be able to use the unique partial index:
{{{
Limit (cost=48.30..49.42 rows=1 width=61)
-> Bitmap Heap Scan on analytics_usercount (cost=48.30..49.42 rows=1
width=61)
Recheck Cond: (((property)::text = 'messages_read::hour'::text)
AND (realm_id = 4715) AND (end_time = '2023-09-13 19:00:00+00'::timestamp
with time zone) AND (user_id = 428054))
Filter: ((subgroup)::text IS NULL)
-> BitmapAnd (cost=48.30..48.30 rows=1 width=0)
-> Bitmap Index Scan on
analytics_usercount_property_591dbec1_idx (cost=0.00..4.88 rows=158
width=0)
Index Cond: (((property)::text =
'messages_read::hour'::text) AND (realm_id = 4715) AND (end_time =
'2023-09-13 19:00:00+00'::timestamp with time zone))
-> Bitmap Index Scan on analytics_usercount_e8701ad4
(cost=0.00..43.17 rows=3626 width=0)
Index Cond: (user_id = 428054)
}}}
Dropping the explicit cast causes it to use the index:
{{{
Limit (cost=0.57..2.80 rows=1 width=61)
-> Index Scan using unique_user_count_null_subgroup on
analytics_usercount (cost=0.57..2.80 rows=1 width=61)
Index Cond: ((user_id = 428054) AND ((property)::text =
'messages_read::hour'::text) AND (end_time = '2023-09-13
19:00:00+00'::timestamp with time zone))
Filter: (realm_id = 4715)
}}}
...and improving the query runtime significantly.
It's not clear to me from 09ffc5c1212d4ced58b708cbbf3dfbfb77b782ca what
about psycopg3 requires this cast.
--
Ticket URL: <https://code.djangoproject.com/ticket/34840>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/0107018a90021122-80b7551b-4b8d-4be0-b477-fd00b2983ae4-000000%40eu-central-1.amazonses.com.