Typeorm Pagination에 관한 정리
offset/limit 과 skip/take 차이, JOIN쿼리에 skip/take 사용시 1+1 쿼리 이슈
Typeorm querybuilder 에는 offset/limit 과 skip/take 가 존재합니다.
offset 메서드 설명을 확인해보면
join을 사용할 경우에는 기대한대로 동작하지 않을 수 있다고 합니다. 대신 skip을 사용하라고 합니다.
둘 다 SQL offset/limit 동작과 같아보이는데 어떤 점이 다를까요?
동작을 비교해보기 위해서 간단하게 1:N 관계를 가지는 Writer, Post 엔티티를 생성했습니다.
JOIN 없는 쿼리
우선 조인없이 offset/limit을 사용해서 유저를 가져오는 쿼리를 확인해보겠습니다.
글쓴이들을 id순 정렬로, offset/limit을 각각 10/30을 준 뒤 ORM 쿼리를 실행시켰습니다.
예상했던 그대로 쿼리가 생성되어 실행됩니다.
이번엔 동일한 ORM 쿼리를 skip/take로 변경해서 실행시켜보겠습니다.
offset/limit 를 사용한 ORM쿼리와 똑같은 쿼리가 생성됩니다.
JOIN 쿼리
이번엔 JOIN 쿼리를 통해 비교해보겠습니다.
Writer 1번, 2번이 각각 3개의 포스팅을 작성했다고 할 때 2명의 Writer와 그들이 작성한 Post도 같이 가져오도록 의도한 JOIN 쿼리를 사용해보겠습니다.
예상과는 다르게 1번 Writer의 Post 2개만 가져왔습니다.
SQL 쿼리를 살펴보면 Writer와 Post를 Left Join을 통해 엮는데, 이 때 쿼리 결과값에는
Post 갯수만큼 Writer도 중복되어 늘어나기 때문에 offset/limit 사용시 원하는대로 값을 얻을 수 없습니다.
이번에는 skip/take를 사용해보겠습니다.
offset/limit을 사용했을 때와 다르게 하는대로 데이터를 얻을 수 있었습니다.
skip/take는 offset/limit에서의 duplicate rows 이슈를 해결하기 위해 JOIN 쿼리에서 2번의 쿼리를 실행시킵니다.
첫번째 쿼리에서 offset/limit 를 제외한 쿼리를 서브쿼리로 실행시킨 후 DISTINCT를 이용해서 중복되지 않는 writer_id를 뽑아냅니다.
두번째 쿼리에서는 첫번째 쿼리에서 찾은 중복제거된 writer_id를 WHERE IN 쿼리를 이용해서 원하는 값을 SELECT 하게 됩니다.
기존에 의도했던 2명의 유저와 그들이 작성한 POST 3개를 모두 가져오는 결과를 얻었습니다.
하지만 결국 하나의 결과를 얻기 위해서 1+1 쿼리를 날리는 셈이죠.
원하는 결과를 얻었지만 1+1 쿼리를 참을 수 없으니 해결해보도록 하겠습니다.
먼저 offset/limit, order by를 적용한 sub_writer 서브쿼리를 만들어서 Writer와 SELF JOIN을 사용했습니다. 해당 서브쿼리에는 Post와 JOIN이 걸려있지 않기 때문에 조건에 맞는 Writer만을 정확하게 조회할 수 있습니다.
또한 인덱싱되어 있는 PK(id) 만을 가져오기 때문에 빠르고 안정적인 성능을 보장할 수 있습니다.
offset/limit 을 사용해서 row를 조회하려고 할 때, 값이 커질수록 성능이 느려짐. why? 설정한 수 만큼 full scan 하기 때문. PK(id)에는 인덱싱이 되어있기 때문에 offset/limit, order by 시에도 빠른 속도를 보장받음.
그런 뒤에 필요했던 Post를 JOIN해준다면 1번의 쿼리로 원하는 쿼리를 빠르고 안정적이게 실행시킬 수 있게 됩니다.
결론
offset/limit에서 나타나는 JOIN 쿼리 이슈를 해결하기위해 skip/take 메서드가 추가된 듯 합니다.
skip/take 를 사용하면 JOIN 쿼리에서 원하는 결과를 충분히 얻을 수 있지만 내부적으로 1+1 쿼리를 실행하고 있기 때문에 불편하다면 개선하면 더 좋다.
끝!