Django는 Datetime 필드에 대하여 Date 기준으로 필터링하기 위해 타입 캐스팅을 제공합니다.
date
For datetime fields, casts the value as date. Allows chaining additional field lookups. Takes a date value.
Example:
Entry.objects.filter(pub_date__date=datetime.date(2005, 1, 1)) Entry.objects.filter(pub_date__date__gt=datetime.date(2005, 1, 1))(No equivalent SQL code fragment is included for this lookup because implementation of the relevant query varies among different database engines.)
참조
해당 필터를 통해 Post에서 posted_at가 최근 3일 이내를 필터링하는 경우 다음과 같이 손쉽게 쿼리를 구성할 수 있습니다.
Post.objects.filter(posted_at__date__gte=datetime.now() - timedelta(days=3))
하지만 위와 같은 쿼리셋은 Postgresql 데이터베이스를 사용하는 경우 성능 문제를 일으킬 수 있습니다.
def func_1():
start_time = time.time()
queryset = Post.objects.filter(posted_at__date__gte=datetime.today() - timedelta(days=3))
list(queryset)
print("Time elapsed: ", time.time() - start_time)
def func_2():
start_time = time.time()
queryset = Post.objects.filter(posted_at__gte=(datetime.today() - timedelta(days=3)).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=pytz.utc))
list(queryset)
print("Time elapsed: ", time.time() - start_time)
위의 함수에서 호출되는 queryset은 동일하지만 func_1은 __date를 통한 필터링에 걸리는 시간을, func_2는 datetime을 통한 필터링에 걸리는 시간을 측정하고 있습니다.
>> func_1()
7.6200413703918462
>> func_2()
0.4673142433166504
하지만 func_2가 func_1에 비하여 훨씬 실행 시간이 짧은 것을 확인할 수 있습니다.
왜 이런 결과가 도출될까요?
>> query1 = Post.objects.filter(posted_at__date__gte=datetime.today() - timedelta(days=3))
>> print(query1.query)
SELECT "posts_post"."id", "posts_post"."posted_at", "posts_post"."content" FROM "posts_post" WHERE ("posts_post"."posted_at" AT TIME ZONE UTC)::date >= 2024-08-22
>> query2 = Post.objects.filter(posted_at__gte=(datetime.today() - timedelta(days=3)).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=pytz.utc))
>> print(query2.query)
SELECT "posts_post"."id", "posts_post"."posted_at", "posts_post"."content" FROM "posts_post" WHERE "posts_post"."posted_at" >= 2024-08-22 00:00:00+00:00
Django가 호출하는 쿼리를 살펴보면 __date는 postgresql에서 제공하는 ::date를 통한 타입 캐스팅 기능을 사용하여 두 쿼리가 달리지는 것을 볼 수 있습니다.
postgresql에서 제공하는 타입 캐스팅은 내부가 블랙박스로 되어있어 성능의 저하가 발생할 수 있게 됩니다.
또한, 다음과 같이 posted_at에 인덱싱이 적용되어있다고 하더라도 ::date는 인덱스가 적용되지 않을 수 있습니다.
>> print(query1.explain())
Seq Scan on posts_post (cost=0.00..104717.29 rows=1502748 width=119)
Filter: (((posted_at AT TIME ZONE 'UTC'::text))::date >= '2024-08-22'::date)
>> print(query2.explain())
Bitmap Heap Scan on posts_post (cost=415.18..28017.53 rows=37000 width=119)
Recheck Cond: (posted_at >= '2024-08-22 00:00:00+00'::timestamp with time zone)
-> Bitmap Index Scan on posts_post_posted__370606_idx (cost=0.00..405.93 rows=37000 width=0)
Index Cond: (posted_at >= '2024-08-22 00:00:00+00'::timestamp with time zone)