[Django] Date Casting 성능 문제

홍준식·2024년 8월 25일

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_2datetime을 통한 필터링에 걸리는 시간을 측정하고 있습니다.

>> func_1()
7.6200413703918462
>> func_2()
0.4673142433166504

하지만 func_2func_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)

0개의 댓글