Sequelize의 subQuery 옵션

문린이·2023년 8월 24일
0

sequelize에 있는 subquery 옵션에 대한 글입니다.

Subquery

model.d.ts 파일에 있는 내용

Use sub queries. This should only be used if you know for sure the query does not result in a cartesian product.

이 말의 의미는 서브쿼리를 사용할 때는 해당 쿼리가 카르테시안 곱을 발생시키지 않을 것이라는 것을 확실하게 알고 있어야 한다. 라고 할 수 있다.

서브쿼리(Sub-queries)

서브쿼리란 SQL 쿼리 안에 또 다른 쿼리를 중첩하는 것을 의미한다. 이 방식은 복잡한 데이터 추출이 필요할 때 유용하지만, 무분별하게 사용하면 성능 문제를 야기할 수 있다.

서브쿼리 예시

서브쿼리는 WHERE, FROM 및 SELECT 절을 포함하여 SQL 문의 다양한 부분에서 사용될 수 있다. 다음은 'WHERE' 절에 하위 쿼리를 사용하여 평균 급여보다 많은 급여를 받는 직원을 찾는 예

SELECT first_name, salary
FROM employees
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
);

카르테시안 곱(Cartesian Product)

카르테시안 곱은 두 개 이상의 테이블에서 가능한 모든 행의 조합을 생성한다. 카르테시안 곱이 발생하면, 반환되는 결과 세트의 크기가 상당히 커질 수 있으며 이는 성능에 큰 영향을 미칠 수 있다.

카르테시안 곱 예시

조건 없이 두 테이블을 조인하면 데카르트 곱이 생성된다. 예를 들어 제품과 고객이라는 두 개의 테이블이 있다고 가정한다. 이와 같은 조인 조건이 없는 쿼리는 데카르트 곱을 생성한다.

SELECT *
FROM products, customers;

여기서 products 테이블의 각 행은 customers 테이블의 각 행과 결합된다. products에 10개의 행이 있고 customers에 20개의 행이 있는 경우 결과 집합에는 10 x 20 = 200개의 행이 포함된다.

Sequelize에서의 subQuery 옵션

subQuery 예시

subQuery: true(default)

SELECT ... FROM (
    SELECT ... FROM "mainTable" 
    JOIN "associatedTable" ON ...
    WHERE ...
    GROUP BY ...
    ORDER BY ...
) AS "sub"
LIMIT ...

subQuery: false

SELECT ... FROM "mainTable"
JOIN "associatedTable" ON ...
WHERE ...
GROUP BY ...
ORDER BY ...
LIMIT ...

subQuery: false 옵션은 쿼리를 효과적으로 평면화하고 특정 사례, 데이터베이스 및 실제 쿼리 복잡성에 따라 더 높은 성능의 쿼리를 생성할 수 있다.

그러나 subQuery: false를 사용하면 특히 관련 테이블의 열을 기준으로 정렬하거나 필터링하는 경우 부작용이 발생할 수 있다. 집계를 사용할 때 결과에도 영향을 미칠 수 있다. 이는 생성된 특정 SQL과 쿼리 논리에 미치는 영향을 이해하면서 주의해서 사용해야 하는 옵션이다.

subQuery - limit

subQuery 옵션과 limit를 같이 쓰면 어떠한 일이 발생할까?

subQuery: true(default)

코드 예시

  const testOne = await TestOne.findAll({
    attributes: ['id', 'name'],
    include: [
      {
        model: TestTwo,
        as: 'two',
        attributes: ['id', 'name'],
        include: [
          {
            model: TestThree,
            as: 'three',
            attributes: ['id', 'name'],
          },
        ],
      },
    ],
    limit: 1,
  });

위에 코드를 sql로 확인해보면 다음과 같다.

SELECT "TestOne".*, 
"two"."id" AS "two.id", 
"two"."name" AS "two.name", 
"two->three"."id" AS "two.three.id", 
"two->three"."name" AS "two.three.name" 
FROM (SELECT "TestOne"."id", 
		"TestOne"."name" 
        FROM "TestOnes" AS "TestOne" LIMIT 1
	) AS "TestOne" 
LEFT OUTER JOIN "TestTwos" AS "two" ON "TestOne"."id" = "two"."oneId" 
LEFT OUTER JOIN "TestThrees" AS "two->three" ON "two"."id" = "two->three"."twoId";

결과는 다음과 같다.

[
    {
        "id": 1,
        "name": "1번 테이블 이름 1",
        "two": [
            {
                "id": 1,
                "name": "2번 테이블 이름 1",
                "three": [
                    {
                        "id": 1,
                        "name": "3번 테이블 이름 1"
                    },
                    {
                        "id": 2,
                        "name": "3번 테이블 이름 2"
                    }
                ]
            },
            {
                "id": 2,
                "name": "2번 테이블 이름 2",
                "three": [
                    {
                        "id": 3,
                        "name": "3번 테이블 이름 3"
                    },
                    {
                        "id": 4,
                        "name": "3번 테이블 이름 4"
                    }
                ]
            }
        ]
    }
]

subQuery: false

코드 예시

  const testOne = await TestOne.findAll({
    attributes: ['id', 'name'],
    include: [
      {
        model: TestTwo,
        as: 'two',
        attributes: ['id', 'name'],
        include: [
          {
            model: TestThree,
            as: 'three',
            attributes: ['id', 'name'],
          },
        ],
      },
    ],
    limit: 1,
   	subQuery: false, // 추가
  });

위에 코드를 sql로 확인해보면 다음과 같다.

SELECT "TestOne"."id", 
"TestOne"."name", 
"two"."id" AS "two.id", 
"two"."name" AS "two.name", 
"two->three"."id" AS "two.three.id", 
"two->three"."name" AS "two.three.name" 
FROM "TestOnes" AS "TestOne" 
LEFT OUTER JOIN "TestTwos" AS "two" ON "TestOne"."id" = "two"."oneId" 
LEFT OUTER JOIN "TestThrees" AS "two->three" ON "two"."id" = "two->three"."twoId" 
LIMIT 1;

결과는 다음과 같다.

[
    {
        "id": 1,
        "name": "1번 테이블 이름 1",
        "two": [
            {
                "id": 1,
                "name": "2번 테이블 이름 1",
                "three": [
                    {
                        "id": 1,
                        "name": "3번 테이블 이름 1"
                    }
                ]
            }
        ]
    }
]

비교

subQuery 옵션이 true 일 때랑 false 일 때 sql만 다시 보자

true

SELECT "TestOne".*, 
"two"."id" AS "two.id", 
"two"."name" AS "two.name", 
"two->three"."id" AS "two.three.id", 
"two->three"."name" AS "two.three.name" 
FROM (SELECT "TestOne"."id", 
		"TestOne"."name" 
        FROM "TestOnes" AS "TestOne" LIMIT 1
	) AS "TestOne" 
LEFT OUTER JOIN "TestTwos" AS "two" ON "TestOne"."id" = "two"."oneId" 
LEFT OUTER JOIN "TestThrees" AS "two->three" ON "two"."id" = "two->three"."twoId";

위에 쿼리는 먼저 "TestOne"에서 하나의 행을 선택한 다음 "TestTwo" 및 "TestThree"와 조인을 수행한다. 이를 통해 "TestTwo" 및 "TestThree"의 여러 행을 "TestOne"의 단일 행과 결합할 수 있으므로 보다 "확장된" 결과 집합을 얻을 수 있다.

false

SELECT "TestOne"."id", 
"TestOne"."name", 
"two"."id" AS "two.id", 
"two"."name" AS "two.name", 
"two->three"."id" AS "two.three.id", 
"two->three"."name" AS "two.three.name" 
FROM "TestOnes" AS "TestOne" 
LEFT OUTER JOIN "TestTwos" AS "two" ON "TestOne"."id" = "two"."oneId" 
LEFT OUTER JOIN "TestThrees" AS "two->three" ON "two"."id" = "two->three"."twoId" 
LIMIT 1;

위에 쿼리는 모든 조인을 먼저 수행한 다음 전체 결과 집합을 단 하나의 행으로 제한한다. 이렇게 하면 자연스럽게 "TestTwo" 및 "TestThree"의 관련 행이 제한되어 결과 집합이 더욱 "압축"된다.

첫 번째 쿼리의 'LIMIT'는 "TestOnes" 테이블에만 영향을 미친다. 두 번째 쿼리에서는 'LIMIT'가 조인된 결과 전체에 적용된다.

separate 옵션

Sequelize에서 separate 옵션은 hasMany 또는 belongsToMany 연관이 있고 하나의 큰 조인을 수행하는 대신 연관된 각 모델에 대해 별도의 SQL 쿼리를 실행하려는 경우에 특히 유용하다.

이는 종종 성능상의 이유로 유용하며 여러 일대다 관계 조인에서 발생할 수 있는 데카르트 곱 문제를 방지하는 데 도움이 될 수 있다.separate: true를 설정하면 Sequelize는 기본 모델과 각 관련 모델에 대해 별도의 SQL 쿼리를 실행한다.

코드 예시

  const testOne = await TestOne.findAll({
    attributes: ['id', 'name'],
    include: [
      {
        model: TestTwo,
        as: 'two',
        attributes: ['id', 'name'],
        include: [
          {
            model: TestThree,
            as: 'three',
            attributes: ['id', 'name'],
          },
        ],
        separate: true, // 추가
      },
    ],
    limit: 1,
    subQuery: false,
  });

위에 코드를 sql로 확인해보면 다음과 같다.

SELECT "TestOne"."id", 
"TestOne"."name" 
FROM "TestOnes" AS "TestOne" 
LIMIT 1;

SELECT "TestTwo"."id", 
"TestTwo"."name", 
"TestTwo"."oneId", 
"three"."id" AS "three.id", 
"three"."name" AS "three.name" 
FROM "TestTwos" AS "TestTwo" 
LEFT OUTER JOIN "TestThrees" AS "three" ON "TestTwo"."id" = "three"."twoId" 
WHERE "TestTwo"."oneId" IN (1);

결과는 다음과 같다.

[
    {
        "id": 1,
        "name": "1번 테이블 이름 1",
        "two": [
            {
                "id": 1,
                "name": "2번 테이블 이름 1",
                "three": [
                    {
                        "id": 1,
                        "name": "3번 테이블 이름 1"
                    },
                    {
                        "id": 2,
                        "name": "3번 테이블 이름 2"
                    }
                ]
            },
            {
                "id": 2,
                "name": "2번 테이블 이름 2",
                "three": [
                    {
                        "id": 3,
                        "name": "3번 테이블 이름 3"
                    },
                    {
                        "id": 4,
                        "name": "3번 테이블 이름 4"
                    }
                ]
            }
        ]
    }
]

결과가 아래와 동일함을 알 수 있다.

  const testOne = await TestOne.findAll({
    attributes: ['id', 'name'],
    include: [
      {
        model: TestTwo,
        as: 'two',
        attributes: ['id', 'name'],
        include: [
          {
            model: TestThree,
            as: 'three',
            attributes: ['id', 'name'],
          },
        ],
      },
    ],
    limit: 1,
  });

Sequelize에서 separate: true를 사용하면 관련 테이블에서 데이터를 가져오기 위해 별도의 쿼리를 실행하도록 Sequelize에 효과적으로 지시할 수 있다. 먼저 기본 테이블(이 경우 TestOnes)에 대한 쿼리를 실행한 다음 관련 테이블(TestTwos 및 TestThrees)에 대한 추가 쿼리를 실행하고 외래 키(oneId 및 twoId)를 기반으로 기본 테이블에 다시 연결한다.

profile
Software Developer

0개의 댓글