TypeORM을 사용하다 보면 QueryBuilder에 skip&take가 있고, offset&limit이 있다는 것을 알 수 있다.
이에 대해 상세히 설명해주는 글이 많으니 여기선 이것에 대해 깊게 다루진 않겠지만, 보다싶이 JOIN에 대한 skip&take에선 이미 성능 문제가 있다.
근데 여기서 추가적으로, 다른 테이블의 JSON stringify된 데이터를 기준으로 orderBy를 정렬하려고 할 때도 이슈가 있다.
아래부턴 실제 사내에서 쓰이던 구조와 비슷하게 임의로 수정한 코드이기 때문에 휴먼에러가 존재할 수 있습니다.
또한 TypeORM 0.3.17버전을 기준으로 작성되었으며 버전에 따라 다른 결과를 보일 수 있습니다.
유저-포스트의 1:N(혹은 1:1) 관계가 있으며, 포스트의 meta_data라는 열에 여러 정보를 json으로 관리하고 있다. 이 중 subject라는 필드를 기준으로 정렬하려고 한다.
await this.createQueryBuilder('user')
.leftJoinAndSelect('user.post', 'post')
.select([
'user.id',
`JSON_EXTRACT(post.meta_data, '$.subject') AS post_subject`,
])
.where(`${searchType} like :searchValue`, { searchValue: `%${searchValue}%` })
.skip(skipCount)
.take(showCount)
.orderBy('post_subject', 'DESC')
.getManyAndCount()
# 실제로 발생하는 쿼리
SELECT DISTINCT `distinctAlias`.`user_id` AS `ids_user_id`
FROM (
SELECT `user`.`id` AS `user_id`, JSON_EXTRACT(`post`.`meta_data`, '$.subject') AS post_subject
FROM `user` `user` LEFT JOIN `post` `post` ON `post`.`id`=`user`.`post_id`
WHERE `user`.`name` like ?
) `distinctAlias`
ORDER BY post_subject DESC, `user_id` ASC LIMIT 10
# 실행 결과
[ERROR] Expression #1 of ORDER BY clause is not in SELECT list, references column 'distinctAlias.post_subject' which is not in SELECT list; this is incompatible with DISTINCT
성능 문제는 둘째치고 실행조차 되지 않게 된다.
DISTINCT와 ORDER BY를 사용할 때 ORDER BY에서 참조하는 모든 걸럼은 SELECT 리스트에 포함되어야 한다고 한다.
실제로 존재하는 열에 대한 orderBy를 사용할 땐 SELECT DISTINCT에 추가되는 모습을 볼 수 있다.
await this.createQueryBuilder('user')
.leftJoinAndSelect('user.post', 'post')
.select([
'user.id',
`JSON_EXTRACT(post.meta_data, '$.subject') AS post_subject`,
'user.create_date'
])
.where(`${searchType} like :searchValue`, { searchValue: `%${searchValue}%` })
.skip(skipCount)
.take(showCount)
.orderBy('user.create_date', 'DESC')
.getManyAndCount()
# 실제로 발생하는 쿼리
SELECT DISTINCT `distinctAlias`.`user_id` AS `ids_user_id`, `distinctAlias`.`user_create_date`
FROM (
SELECT `user`.`id` AS `user_id`, JSON_EXTRACT(`post`.`meta_data`, '$.subject') AS post_subject, `user`.`create_date` AS `user_create_date`
FROM `user` `user` LEFT JOIN `post` `post` ON `post`.`id`=`user`.`post_id`
WHERE `user`.`name` like ?
) `distinctAlias`
ORDER BY `distinctAlias`.`user_create_date` DESC, `user_id` ASC LIMIT 10
# 실행 결과
# ... id IN을 실행하는 select 한번, COUNT를 위한 select 한번
이를 보아 알 수 있는 점은 JSON_EXTRACT 등 가상의 컬럼을 이용한 OrderBy를 사용하고 싶어도 TypeORM에서 자동으로 DISTINCT의 SELECT에 넣어주지 못하기 때문에 불가능하다는 것이다.
그럼 이를 어떻게 해결해야할까?
하나의 방법으로는 위에 공유된 글처럼 innerJoin 서브쿼리를 이용하면 해결이 될 것 같다. (실제로도 이 방법이 옳을 것 같다)
하지만 필자는 해당 방법을 사용하지 않고 offset&limit을 사용했다.
이유는 굉장히 단순한데 실제 사내에서 발생한 조인 테이블의 관계가 One-To-One이었기 때문이다.
Github의 관련 이슈 댓글에서도 언급되었듯이 One-To-One 관계에서는 DISTINCT가 전혀 필요하지 않으며, 마찬가지로 offset&limit을 사용했을 때 발생하는 중복 열 이슈도 발생되지 않는다.
라고 결론을 내렸고 일단 현재로서는 아무 문제도 발생하지 않았으나 예상하지 못한 부분에서 문제가 발생할지도 모른다. JSON_EXTRACT를 사용하는 비슷한 케이스에서의 OrderBy에 대해 고민하고 있을 때 참고해볼 수 있도록 작성하게 되었다.