Backend/NestJS

TypeORM - select distinct 이슈

hou27 2023. 2. 21. 20:05

문제 발견

혼자 작업하던 중 팀원과 성능 관련 얘기를 하던게 생각나서

로깅되던 Query문을 자세히 살펴봤는데,

작성한 쿼리보다 많은 쿼리문이 실행되고 있는 것을 발견하게 되었다.

 

TypeORM 의도치 않은 중복 필터링 문제 개선

 

TypeORM 의도치 않은 중복 필터링 문제 개선 · Issue #168 · Quickchive/quickchive-backend

아래와 같은 코드 동작 시 const { categories } = await this.users.findOneOrFail({ where: { id: user.id }, relations: { categories: true, }, }); 쿼리가 2개 실행되는 현상 발생 메인 쿼리를 중복 필터링 쿼리로 래핑하여 수

github.com

 

N + 1 문제

아래와 같은 코드 동작 시

const { categories } = await this.users.findOneOrFail({
  where: { id: user.id },
  relations: {
    categories: true,
  },
});



쿼리가 2개 실행되는 현상이 발생했는데, 실행된 쿼리는 다음과 같다.

query: SELECT DISTINCT "distinctAlias"."User_id" AS "ids_User_id" FROM (SELECT "User"."id" AS "User_id", "User"."createdAt" AS "User_createdAt", "User"."updatedAt" AS "User_updatedAt", "User"."name" AS "User_name", "User"."email" AS "User_email", "User"."role" AS "User_role", "User"."verified" AS "User_verified", "User__User_categories"."id" AS "User__User_categories_id", "User__User_categories"."createdAt" AS "User__User_categories_createdAt", "User__User_categories"."updatedAt" AS "User__User_categories_updatedAt", "User__User_categories"."name" AS "User__User_categories_name", "User__User_categories"."slug" AS "User__User_categories_slug", "User__User_categories"."parentId" AS "User__User_categories_parentId", "User__User_categories"."userId" AS "User__User_categories_userId" FROM "user" "User" LEFT JOIN "category" "User__User_categories" ON "User__User_categories"."userId"="User"."id" WHERE ("User"."id" = $1)) "distinctAlias" ORDER BY "User_id" ASC LIMIT 1 -- PARAMETERS: [1]
query: SELECT "User"."id" AS "User_id", "User"."createdAt" AS "User_createdAt", "User"."updatedAt" AS "User_updatedAt", "User"."name" AS "User_name", "User"."email" AS "User_email", "User"."role" AS "User_role", "User"."verified" AS "User_verified", "User__User_categories"."id" AS "User__User_categories_id", "User__User_categories"."createdAt" AS "User__User_categories_createdAt", "User__User_categories"."updatedAt" AS "User__User_categories_updatedAt", "User__User_categories"."name" AS "User__User_categories_name", "User__User_categories"."slug" AS "User__User_categories_slug", "User__User_categories"."parentId" AS "User__User_categories_parentId", "User__User_categories"."userId" AS "User__User_categories_userId" FROM "user" "User" LEFT JOIN "category" "User__User_categories" ON "User__User_categories"."userId"="User"."id" WHERE ( ("User"."id" = $1) ) AND ( "User"."id" IN (1) ) -- PARAMETERS: [1]

 

말로만 듣던 N + 1 문제인가? 하고

바로 구글링해보았지만

상단에 뜨는 포스트가 말하는 eager 또는 lazy 관련 문제는 아닌 것 같았다.

 

그래서 실행된 Query문을 한번더 자세히 살펴보니

메인 쿼리를 중복 필터링 쿼리로 래핑하여 수행하고 있다는 사실을 알 수 있었다.

 

그렇다면 왜 따로 옵션을 주지도 않았던 중복 제거 작업이 수행되고 있던 것일까?

 

TypeORM Issue

Typeorm add select distinct in query

 

Typeorm add select distinct in my query. I dont want select distinct. Help! · Issue #4998 · typeorm/typeorm

Issue type: [x] question [x] postgres Hello I want to make a query where I get the data above, but the typeorm is putting select distinct in the query and because of this is giving error. const [re...

github.com

TypeORM의 이슈를 살펴보니 바로 답을 알 수 있었다.

 

that's like pre-select query. typeorm does this because JOINs may cause multiple rows be returned for a single row in the original entity table, making it impossible to properly apply LIMIT. typeorm selects distinct ids applying limits to ids only, and then second (real select) applies WHERE id IN instead of LIMIT, so that you get both JOINs and LIMIT working properly at the same time.

 

즉, JOIN 작업이 있을 때 자체적으로 동작하는 사전 쿼리였던 것인데,

그 과정에서 정렬 작업도 수행될 뿐만 아니라 애초에 의도하지 않았던 쿼리이기 때문에

추후 예기치 못한 문제가 발생할 수도 있겠단 생각이 들어 개선하기로 마음먹었다.

 

해결

TypeORM의 QueryBuilder를 사용하여 Query문을 생성하고 실행하면

원하는 Query만 동작하지 않을까? 하는 생각에

 

// const { categories } = await this.users.findOneOrFail({
//   where: { id: user.id },
//   relations: {
//     categories: true,
//   },
// });
const { categories } = await this.users
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.categories', 'categories')
  .where('user.id = :id', { id: user.id })
  .getOneOrFail();

위와 같이 createQueryBuilder를 사용하여 코드를 수정해보았다.

 

 

그랬더니 이전과 달리 정말 원하는 Query 하나만 실행되는 것을 확인할 수 있었다.

 

트랜잭션을 관리하며 QueryRunnerManager를 사용하는 코드에도

const { categories } = await queryRunnerManager
  .createQueryBuilder(User, 'user')
  .leftJoinAndSelect('user.categories', 'categories')
  .where('user.id = :id', { id: user.id })
  .getOneOrFail();

위와 같이 적용가능하다!