django silk로 n+1 problem을 detect 해 봅시다.

웹/장고 2022. 8. 13. 02:47

 이 글에서는 prefetch_related랑 select_related의 차이를 다루지 않습니다. n+1 problem을 silk 분석 툴로 분석해 보는 것이 목표입니다.

 


 먼저, 이런 상황을 생각해 봅시다. Post가 있습니다. 포스트 전체를 긁어오려고 합니다. 그런데, 잘 생각해 보면, 포스트 정보를 긁어오기 위해서, 작성자의 닉네임 등을 긁어올 겁니다. 그러면, Post에는 User 테이블을 참조하는 FK가 들어갈 겁니다.

 

 대략 이런 구조입니다. post는 22k개가 있고, 유저는 3명이 있습니다.

 

 index 요청을 날려서, 이 구문들을 수행해 봅시다. 어떻게 될까요? 22k번 만큼의 쿼리가 날라가게 됩니다. N+1 problem이라고 많이 이야기를 하는데요. silk로 해당 request가 수행되었을 때 쿼리가 어떻게 날라갔는지 보도록 하겠습니다.

 

 

 먼저, myapp_post에 있는 것을 모두 얻어오게 됩니다.

 

 그 다음에, auth_user로부터 데이터를 얻어오게 되는데요. auth_user.id가 2인 유저를 얻어온다고 되어 있습니다. 이런 쿼리를 N번 수행하게 되는데요. post.user가 호출이 될 때 마다 db에 query를 날리는 꼴이 되어 버립니다. 

 

 

 request 탭을 보시면, 22006 query를 날렸다고 되어 있는데요. 포스트의 개수가 22k개였으니, 포스트의 갯수에 비례해서 날린 셈이 되겠습니다.

 


 prefetch_related를 써서 다시 프로파일링을 해 봅시다. post와 user에 대한 데이터를 얻어오고, python 내부에서 join 처리를 합니다. 문서를 보시면 그래 설명이 되어 있는데요. does a separate lookup for each relationship. 다시 말해, 각 릴레이션에 대해 조회를 합니다. 그리고, does the 'joining' in python. 파이썬에서 결과를 합친다고 되어 있습니다. query가 어떻게 날라가는지만 보겠습니다.

 

 

 먼저, myapp_post에 있는 것들을 모두 가져옵니다.

 

 다음에, 쿼리 하나를 더 날리게 되는데요. auth_user에서 뭔가를 가져옵니다. 그런데, IN(1, 2, 3)이 있습니다.

 

 테이블에는 user_id가 저장되어 있으니, 여기서 user_id를 읽어냈을 겁니다. 읽어낸 user_id값을 가지는 유저들을 모두 불러오는 쿼리는 in 연산자로 쉽게 처리할 수 있습니다. 그러니, 아. 일단 포스트에서 데이터들을 얻어오고, 포스트를 쓴 사람의 유저 id 값들을 얻어온 다음에, in 쿼리로 특정 id가 있는 유저들을 얻어옵니다. 내부에서 join을 하는 것은 그 후에 할 거라는 추론을 할 수 있습니다. 문서를 자세히 읽지 않더라도.

 

 22k개의 쿼리를 날리는 거에서 단 2번만 쿼리를 날리게 되었으니 상당히 빨라졌을 겁니다.

 


 여담으로 silk_sqlquery에는 각 요청당 날라간 query에 대한 정보가 들어가는데요. 유저 수와 포스트 수를 200k로 대폭 증가시켜 보겠습니다. 다음에, 모든 유저가 포스트를 하나 이상씩 썼습니다. 이 상황에서 다시, index 요청을 날리면 어떻게 될까요?

 

 

 query가 보이는데요. 앞에 length는 쿼리 길이를 제가 출력한 것입니다. 148만이라는 어마무시한 길이를 볼 수 있는데요. 이는 20만에 달하는 모든 유저들의 id 값을 in query에 넣기 때문입니다. 문서에 나오는 성능 저하와도 깊은 연관이 있습니다.