Django에서 icontains의 번역

요약

Django에서 PostgreSQL을 사용할 때 <something>.filter(<field>__icontains=<target>)ilike가 아니라 UPPER(field) like UPPER(target)으로 변환된다. field에 인덱스를 걸어놓은 경우 인덱스를 타지 못하고 ilike를 이용해야 인덱스를 탄다. 이런 부분은 날 것 그대로의 SQL을 이용해야 한다.

기대

from django.db import models
from django.contrib.postgres.indexes import GinIndex

class Post(models.Model):
    title = models.CharField(blank=False, null=False, max_length=50)
    content = models.TextField(blank=True, null=False)
    
    class Meta:
        indexes = [
            GinIndex(
                name="title_idx", fields=["title"], opclasses=["gin_trgm_ops"]
            ),
        ]

위와 같이 Post 모델의 title에 대해 인덱스를 걸어놓고 다음과 같은 SQL을 날려보자.

SELECT "board_post"."id",
       "board_post"."issued_date",
       "board_post"."last_modified",
       "board_post"."title",
       "board_post"."content"
  FROM "board_post"
 WHERE ("board_post"."issued_date" >= '2020-12-28T19:51:54.775082+09:00'::timestamptz AND "board_post"."issued_date" <= '2021-06-28T19:51:54.775025+09:00'::timestamptz AND "board_post"."title"::text ILIKE '%테스트%')
 ORDER BY "board_post"."issued_date" DESC
 LIMIT 21;

39ms이 걸렸고, 모두 인덱스를 잘 활용하는 것을 볼 수 있다.

장고는 UPPER를 이용

앞선 쿼리와 동일하게 동작하지만 생긴 것만 살짝 다른 쿼리를 날려보자.

SELECT "board_post"."id",
       "board_post"."issued_date",
       "board_post"."last_modified",
       "board_post"."title",
       "board_post"."content"
  FROM "board_post"
 WHERE ("board_post"."issued_date" >= '2020-12-28T19:51:54.775082+09:00'::timestamptz AND "board_post"."issued_date" <= '2021-06-28T19:51:54.775025+09:00'::timestamptz AND UPPER("board_post"."title"::text) ILIKE UPPER('%테스트%'))
 ORDER BY "board_post"."issued_date" DESC
 LIMIT 21;

위 쿼리는 얼마나 걸릴까? 285ms가 걸렸다. 어째서?


테이블에서 인덱스를 전혀 활용하지 못하고 Full scan을 하고 있기 때문이다. 그리고 장고는 위와 같이 icontains를 번역한다.

q = Post.objects.filter(title__icontains="테스트")
print(q.query)

어떤 결과가 나올까?

UPPER("board_post"."title"::text) LIKE UPPER(%테스트%))

위와 같이 양 변에 upper를 적용한 sql이 나오게 된다. 따라서 인덱스를 타지 못한다.

고치기

위 상황은 extra를 이용해서 조건문에 쿼리문을 직접 삽입함으로써 고칠 수 있다. 다음과 같이 쓰자.

<queryset>.extra(
	where=[""""board_post"."title"::text ILIKE %s"""], params=[f"%테스트%"]
    )

위와 같이 강제로 ilike를 사용하게 함으로써 고칠 수 있다. 이제는 처음 기대했던 것과 같은 쿼리가 나와 인덱스를 탈 수 있다.

좋은 웹페이지 즐겨찾기