InfoGrab DocsInfoGrab Docs

레이블로 필터링

요약

GitLab에는 이슈, 머지 리퀘스트, 에픽에 지정할 수 있는 레이블이 있습니다. 이 객체들을 여러 레이블로 필터링하려면(예: '~Plan 레이블과 ~backend 레이블이 모두 붙은 열린 이슈 전체') GROUP BY 절이 포함된 쿼리를 생성합니다.

개요#

GitLab에는 이슈, 머지 리퀘스트, 에픽에 지정할 수 있는 레이블이 있습니다. 이러한 객체의 레이블은 다형성 label_links 테이블을 통한 다대다 관계입니다.

이 객체들을 여러 레이블로 필터링하려면(예: '~Plan 레이블과 ~backend 레이블이 모두 붙은 열린 이슈 전체') GROUP BY 절이 포함된 쿼리를 생성합니다. 단순한 형태는 다음과 같습니다.

SELECT
    issues.*
FROM
    issues
    INNER JOIN label_links ON label_links.target_id = issues.id
        AND label_links.target_type = 'Issue'
    INNER JOIN labels ON labels.id = label_links.label_id
WHERE
    issues.project_id = 13083
    AND (issues.state IN ('opened'))
    AND labels.title IN ('Plan',
        'backend')
GROUP BY
    issues.id
HAVING (COUNT(DISTINCT labels.title) = 2)
ORDER BY
    issues.updated_at DESC,
    issues.id DESC
LIMIT 20 OFFSET 0

구체적으로는 다음과 같습니다.

  1. GROUP BY issues.id는 결과를 이슈 단위로 묶습니다.
  2. HAVING (COUNT(DISTINCT labels.title) = 2)는 일치한 모든 이슈가 두 레이블을 모두 가지도록 보장합니다.

이 방식은 이상적인 형태보다 복잡합니다. 그만큼 쿼리 구성에서 오류가 발생하기 쉽습니다(예: 이슈 #15557).

시도 A: WHERE EXISTS#

시도 A1: WHERE EXISTS를 사용한 다중 서브쿼리#

이슈 #37137과 연결된 머지 리퀘스트에서는 GROUP BY를 여러 개의 WHERE EXISTS로 대체해 보았습니다. 위 예시에 적용하면 다음과 같습니다.

WHERE (EXISTS (
        SELECT
            TRUE
        FROM
            label_links
            INNER JOIN labels ON labels.id = label_links.label_id
        WHERE
            labels.title = 'Plan'
            AND target_type = 'Issue'
            AND target_id = issues.id))
AND (EXISTS (
        SELECT
            TRUE
        FROM
            label_links
            INNER JOIN labels ON labels.id = label_links.label_id
        WHERE
            labels.title = 'backend'
            AND target_type = 'Issue'
            AND target_id = issues.id))

이 방식은 스키마 변경 없이 동작했고 가독성도 어느 정도 좋아졌지만, 쿼리 성능은 개선되지 않았습니다.

시도 A2: WHERE EXISTS 절에서 레이블 ID 사용#

머지 리퀘스트 #34503에서는 A1과 비슷한 방식을 따랐습니다. 다만 이번에는 필터에 사용된 레이블의 ID를 별도 쿼리로 가져와 EXISTS 절의 JOIN을 피하고 label_links.label_id로 직접 필터링했습니다. 이 쿼리를 빠르게 하기 위해 label_links에 target_id, label_id, target_type 칼럼을 사용하는 새 인덱스도 추가했습니다.

하나의 루트 네임스페이스 안에 제목이 같은 레이블이 여러 개 있을 수 있어 레이블 ID를 찾는 일은 간단하지 않았습니다. 이는 레이블 ID를 제목별로 묶은 다음 그 ID 배열을 EXISTS 절에 사용하는 방식으로 해결했습니다.

그 결과 성능이 크게 개선되었습니다. 다만 이 최적화는 프로젝트나 그룹 컨텍스트가 없는 대시보드 페이지에는 적용할 수 없었습니다. 여기서 레이블 ID를 찾으려면 사용자가 접근할 수 있는 모든 프로젝트와 그룹을 검색해야 하므로 간단히 처리할 수 없었습니다.

시도 B: 배열 칼럼을 사용한 역정규화#

이슈 #49651에서 쿼리를 위해 label_links 테이블을 역정규화하는 방안을 레이블 ID와 제목이라는 두 가지 선택지로 논의했습니다.

두 가지 모두 issues, merge_requests, epics의 배열 칼럼으로 생각할 수 있습니다. issues.label_ids는 레이블 ID의 배열 칼럼이 되고, issues.label_titles는 레이블 제목의 배열이 됩니다.

이러한 배열 칼럼에는 매칭 성능을 높이기 위해 GIN 인덱스를 함께 사용할 수 있습니다.

시도 B1: 각 객체에 레이블 ID 저장#

이 방식은 제목을 저장하는 방식보다 확실한 장점이 있습니다.

  1. 레이블이 삭제되거나 프로젝트가 이동하지 않는 한, 역정규화된 칼럼을 일괄 갱신할 필요가 없습니다.
  2. 제목보다 저장 공간을 적게 사용합니다.

다만 GitLab의 애플리케이션 설계 때문에 이 방식은 쉽지 않습니다. 레이블 ID만으로 쉽게 조회할 수 있었다면 이 문서 앞부분의 최초 쿼리에 INNER JOIN labels가 필요하지 않았을 것입니다. GitLab은 사용자가 프로젝트를 넘어, 나아가 그룹을 넘어 레이블 제목으로 필터링할 수 있게 하므로 ~Plan 레이블 필터에는 서로 다른 ID를 가진 레이블이 여러 개 포함될 수 있습니다.

사용자가 서로 다른 ID를 알아야 하는 상황은 원하지 않으며, 이는 다음 데이터 집합이 주어졌을 때를 뜻합니다.

프로젝트 ~Plan 레이블 ID ~backend 레이블 ID
A 11 12
B 21 22
C 31 32

다음과 같은 쿼리가 필요합니다.

WHERE
    label_ids @> ARRAY[11, 12]
    OR label_ids @> ARRAY[21, 22]
    OR label_ids @> ARRAY[31, 32]

경우에 따라 같은 객체에 적용될 수 있는, ID가 서로 다른 ~backend 레이블이 둘 있을 수도 있다는 점까지 고려하면 더 복잡해지며, 조합의 수는 더욱 늘어납니다.

시도 B2: 각 객체에 레이블 제목 저장#

객체를 갱신하는 관점에서는 이 방식이 가장 나쁜 선택지입니다. 다음 경우에 객체를 일괄 갱신해야 합니다.

  1. 객체가 한 프로젝트에서 다른 프로젝트로 이동할 때.
  2. 프로젝트가 한 그룹에서 다른 그룹으로 이동할 때.
  3. 레이블 이름이 바뀔 때.
  4. 레이블이 삭제될 때.

저장 공간도 훨씬 많이 사용합니다. 다만 쿼리는 단순합니다.

WHERE
    label_titles @> ARRAY['Plan', 'backend']

그리고 이슈 #49651의 테스트에서 이 방식이 빠를 수 있다는 결과를 얻었습니다.

다만 현재로서는 단점이 장점보다 큽니다.

결론#

역정규화가 필요 없으면서 쿼리 성능을 크게 개선하는 A2 방법을 찾았습니다. 이 방법을 모든 경우에 적용할 수는 없었지만, 나머지 경우에는 A1 방법을 적용해 모든 상황에서 GROUP BY와 HAVING 절을 제거할 수 있었습니다.

그 결과 쿼리가 단순해졌고 가장 흔한 경우의 성능이 개선되었습니다.

레이블로 필터링

GitLab v19.4
원문 보기

요약

GitLab에는 이슈, 머지 리퀘스트, 에픽에 지정할 수 있는 레이블이 있습니다. 이 객체들을 여러 레이블로 필터링하려면(예: '~Plan 레이블과 ~backend 레이블이 모두 붙은 열린 이슈 전체') GROUP BY 절이 포함된 쿼리를 생성합니다.

개요#

GitLab에는 이슈, 머지 리퀘스트, 에픽에 지정할 수 있는 레이블이 있습니다. 이러한 객체의 레이블은 다형성 label_links 테이블을 통한 다대다 관계입니다.

이 객체들을 여러 레이블로 필터링하려면(예: '~Plan 레이블과 ~backend 레이블이 모두 붙은 열린 이슈 전체') GROUP BY 절이 포함된 쿼리를 생성합니다. 단순한 형태는 다음과 같습니다.

SELECT
    issues.*
FROM
    issues
    INNER JOIN label_links ON label_links.target_id = issues.id
        AND label_links.target_type = 'Issue'
    INNER JOIN labels ON labels.id = label_links.label_id
WHERE
    issues.project_id = 13083
    AND (issues.state IN ('opened'))
    AND labels.title IN ('Plan',
        'backend')
GROUP BY
    issues.id
HAVING (COUNT(DISTINCT labels.title) = 2)
ORDER BY
    issues.updated_at DESC,
    issues.id DESC
LIMIT 20 OFFSET 0

구체적으로는 다음과 같습니다.

  1. GROUP BY issues.id는 결과를 이슈 단위로 묶습니다.
  2. HAVING (COUNT(DISTINCT labels.title) = 2)는 일치한 모든 이슈가 두 레이블을 모두 가지도록 보장합니다.

이 방식은 이상적인 형태보다 복잡합니다. 그만큼 쿼리 구성에서 오류가 발생하기 쉽습니다(예: 이슈 #15557).

시도 A: WHERE EXISTS#

시도 A1: WHERE EXISTS를 사용한 다중 서브쿼리#

이슈 #37137과 연결된 머지 리퀘스트에서는 GROUP BY를 여러 개의 WHERE EXISTS로 대체해 보았습니다. 위 예시에 적용하면 다음과 같습니다.

WHERE (EXISTS (
        SELECT
            TRUE
        FROM
            label_links
            INNER JOIN labels ON labels.id = label_links.label_id
        WHERE
            labels.title = 'Plan'
            AND target_type = 'Issue'
            AND target_id = issues.id))
AND (EXISTS (
        SELECT
            TRUE
        FROM
            label_links
            INNER JOIN labels ON labels.id = label_links.label_id
        WHERE
            labels.title = 'backend'
            AND target_type = 'Issue'
            AND target_id = issues.id))

이 방식은 스키마 변경 없이 동작했고 가독성도 어느 정도 좋아졌지만, 쿼리 성능은 개선되지 않았습니다.

시도 A2: WHERE EXISTS 절에서 레이블 ID 사용#

머지 리퀘스트 #34503에서는 A1과 비슷한 방식을 따랐습니다. 다만 이번에는 필터에 사용된 레이블의 ID를 별도 쿼리로 가져와 EXISTS 절의 JOIN을 피하고 label_links.label_id로 직접 필터링했습니다. 이 쿼리를 빠르게 하기 위해 label_links에 target_id, label_id, target_type 칼럼을 사용하는 새 인덱스도 추가했습니다.

하나의 루트 네임스페이스 안에 제목이 같은 레이블이 여러 개 있을 수 있어 레이블 ID를 찾는 일은 간단하지 않았습니다. 이는 레이블 ID를 제목별로 묶은 다음 그 ID 배열을 EXISTS 절에 사용하는 방식으로 해결했습니다.

그 결과 성능이 크게 개선되었습니다. 다만 이 최적화는 프로젝트나 그룹 컨텍스트가 없는 대시보드 페이지에는 적용할 수 없었습니다. 여기서 레이블 ID를 찾으려면 사용자가 접근할 수 있는 모든 프로젝트와 그룹을 검색해야 하므로 간단히 처리할 수 없었습니다.

시도 B: 배열 칼럼을 사용한 역정규화#

이슈 #49651에서 쿼리를 위해 label_links 테이블을 역정규화하는 방안을 레이블 ID와 제목이라는 두 가지 선택지로 논의했습니다.

두 가지 모두 issues, merge_requests, epics의 배열 칼럼으로 생각할 수 있습니다. issues.label_ids는 레이블 ID의 배열 칼럼이 되고, issues.label_titles는 레이블 제목의 배열이 됩니다.

이러한 배열 칼럼에는 매칭 성능을 높이기 위해 GIN 인덱스를 함께 사용할 수 있습니다.

시도 B1: 각 객체에 레이블 ID 저장#

이 방식은 제목을 저장하는 방식보다 확실한 장점이 있습니다.

  1. 레이블이 삭제되거나 프로젝트가 이동하지 않는 한, 역정규화된 칼럼을 일괄 갱신할 필요가 없습니다.
  2. 제목보다 저장 공간을 적게 사용합니다.

다만 GitLab의 애플리케이션 설계 때문에 이 방식은 쉽지 않습니다. 레이블 ID만으로 쉽게 조회할 수 있었다면 이 문서 앞부분의 최초 쿼리에 INNER JOIN labels가 필요하지 않았을 것입니다. GitLab은 사용자가 프로젝트를 넘어, 나아가 그룹을 넘어 레이블 제목으로 필터링할 수 있게 하므로 ~Plan 레이블 필터에는 서로 다른 ID를 가진 레이블이 여러 개 포함될 수 있습니다.

사용자가 서로 다른 ID를 알아야 하는 상황은 원하지 않으며, 이는 다음 데이터 집합이 주어졌을 때를 뜻합니다.

프로젝트 ~Plan 레이블 ID ~backend 레이블 ID
A 11 12
B 21 22
C 31 32

다음과 같은 쿼리가 필요합니다.

WHERE
    label_ids @> ARRAY[11, 12]
    OR label_ids @> ARRAY[21, 22]
    OR label_ids @> ARRAY[31, 32]

경우에 따라 같은 객체에 적용될 수 있는, ID가 서로 다른 ~backend 레이블이 둘 있을 수도 있다는 점까지 고려하면 더 복잡해지며, 조합의 수는 더욱 늘어납니다.

시도 B2: 각 객체에 레이블 제목 저장#

객체를 갱신하는 관점에서는 이 방식이 가장 나쁜 선택지입니다. 다음 경우에 객체를 일괄 갱신해야 합니다.

  1. 객체가 한 프로젝트에서 다른 프로젝트로 이동할 때.
  2. 프로젝트가 한 그룹에서 다른 그룹으로 이동할 때.
  3. 레이블 이름이 바뀔 때.
  4. 레이블이 삭제될 때.

저장 공간도 훨씬 많이 사용합니다. 다만 쿼리는 단순합니다.

WHERE
    label_titles @> ARRAY['Plan', 'backend']

그리고 이슈 #49651의 테스트에서 이 방식이 빠를 수 있다는 결과를 얻었습니다.

다만 현재로서는 단점이 장점보다 큽니다.

결론#

역정규화가 필요 없으면서 쿼리 성능을 크게 개선하는 A2 방법을 찾았습니다. 이 방법을 모든 경우에 적용할 수는 없었지만, 나머지 경우에는 A1 방법을 적용해 모든 상황에서 GROUP BY와 HAVING 절을 제거할 수 있었습니다.

그 결과 쿼리가 단순해졌고 가장 흔한 경우의 성능이 개선되었습니다.