장고의 orm은 아주 편리한 도구이다.
쿼리에 대한 이해도가 아주 높지 않아도 쉽게 데이터에 대해 접근할 수 있기 때문이다.
다만 진짜 쿼리가 어떻게 날아가는지 모른다면 성능 저하를 일으키는 부분이 되기도 한다.
또한 높은 트래픽을 처리하기 위해 DB 캐시를 이용하기도 하는데
쿼리를 분석하면서 어떤 식으로 캐시 히트율을 올릴 수 있는지 알아본다.
해당 쿼리들은 특정 쿼리셋 혹은 모델 객체를 가져올 때 사용하는 쿼리들이다.
>> User.objects.get(id=1)
SELECT `users_user`.`id` ... FROM `users_user` WHERE `users_user`.`id` = 1
>> User.objects.filter(id=1)
SELECT `users_user`.`id` ... FROM `users_user` WHERE `users_user`.`id` = 1 LIMIT 21
>> User.objects.filter(id__in=[1])
SELECT `users_user`.`id` ... FROM `users_user` WHERE `users_user`.`id` IN (1) LIMIT 21
>> User.objects.filter(id=1).first()
SELECT `users_user`.`id` ... FROM `users_user` WHERE `users_user`.`id` = 1 ORDER BY `users_user`.`id` ASC LIMIT 1
>> User.objects.filter(id__in=[1]).first()
SELECT `users_user`.`id` ... FROM `users_user` WHERE `users_user`.`id` IN (1) ORDER BY `users_user`.`id` ASC LIMIT 1
>> User.objects.filter(id=1).last()
SELECT `users_user`.`id` ... FROM `users_user` WHERE `users_user`.`id` = 1 ORDER BY `users_user`.`id` DESC LIMIT 1
>> User.objects.filter(id__in=[1]).last()
SELECT `users_user`.`id` ... FROM `users_user` WHERE `users_user`.`id` IN (1) ORDER BY `users_user`.`id` DESC LIMIT 1
위 쿼리들에서 중요한 건 쿼리를 해서 나온 결과물은 거의 비슷하다.
다만 겹치는 쿼리가 단 한 개도 없기 때문에 DB 캐시를 사용하고 있다면(라이브러리마다 다르긴 하다.) 히트율이 0%이다.
아래 쿼리는 길이와 존재 여부를 확인하는 쿼리이다.
>> User.objects.filter(id=1).count()
SELECT COUNT(*) AS `__count` FROM `users_user` WHERE `users_user`.`id` = 1
>> len(User.objects.filter(id=1))
SELECT `users_user`.`id` ... FROM `users_user` WHERE `users_user`.`id` = 1
>> User.objects.filter(id=1).exists()
SELECT (1) AS `a` FROM `users_user` WHERE `users_user`.`id` = 1 LIMIT 1
위 퀴리들도 마찬가지로 겹치지 않기 때문에 히트율이 0%이다.
해당 쿼리들을 보다 보면 보통 이런 생각이 들게 된다.
공식 문서에서 적혀있는 용도에 맞게 orm을 사용하기만 하면 결과적으로 동일한 내용이 나오긴 한다.
다만 효율성의 측면에선 많은 의문점이 생기게 된다.
예를 들어 장고 orm 중 데이터 개수를 알고 싶을 때 쓰는 count나 중복 제거를 위해 사용하는 distinct는
데이터 개수에 따라 심각한 성능 저하가 발생할 수 있다.
따라서 용도에 맞고 원하는 결과가 나온다고 그냥 쓰기보단 실제 쿼리를 보면서 최적화하는 것이 중요하다.
보통 여러 조건을 걸때 chaining으로 조건을 걸게 되는데 이 부분도 순서에 따른 차이가 존재한다.
>> User.objects.filter(id=1, username='username')
SELECT `users_user`.`id` ... FROM `users_user` WHERE (`users_user`.`id` = 1 AND `users_user`.`username` = 'kidsnote') LIMIT 21
>> User.objects.filter(username='username', id=1)
쿼리 호출 X
>> User.objects.filter(id=1).filter(username='username')
쿼리 호출 X
>> User.objects.filter(username='username').filter(id=1)
SELECT `users_user`.`id` ... FROM `users_user` WHERE (`users_user`.`username` = 'kidsnote' AND `users_user`.`id` = 1) LIMIT 21
하나의 filter 조건에 적혀있는 조건은 순서가 바뀌어도 동일한 쿼리가 날아가게 된다.
또한 filter 조건을 chain을 걸었을 경우 하나의 filter로 걸었을 경우와 순서가 같다면 동일한 쿼리가 된다.
다만 chain에서 순서가 다를 경우 다른 쿼리로 인식하게 된다.
마지막으로 중복 조건에 경우인데 하나의 filter에선 중복으로 조건을 걸어도 하나로 나가게 된다.
다만 filter chain일 경우 동일 조건을 여러 개 걸면 AND가 되어 중복이 되니 이것도 성능 저하의 원인이 된다.
where절에 한 번만 확인해도 되는 조건을 2번씩 확인하게 되기 때문이다.
>> User.objects.filter(id=1)
SELECT `users_user`.`id` ... WHERE `users_user`.`id` = 1 LIMIT 21
>> User.objects.filter(id=1).filter(id=1)
SELECT `users_user`.`id` ... FROM `users_user` WHERE (`users_user`.`id` = 1 AND `users_user`.`id` = 1) LIMIT 21
위 쿼리들을 보면서 정리하고 싶은 내용은
DB 캐시 사용 시 원하는 의도에 맞는 결과가 나온다면 동일한 쿼리가 나오게 수정
EX) exists의 경우 존재 여부에 따라 T/F를 주지만 단순히 filter와 bool의 조합으로도 T/F가 가능하기 때문에 filter만 사용하게 변경
filter chain을 할 경우 중복 조건을 조심해야 한다. 중복 조건은 추후 성능 저하의 원인이 될 수 있다.