InfoGrab DocsInfoGrab Docs

라벨로 필터링

요약

GitLab에는 이슈, 머지 리퀘스트, 에픽에 할당할 수 있는 라벨이 있습니다. 여러 라벨로 이러한 객체를 필터링하는 경우 — 예를 들어 '라벨 ~Plan과 라벨 ~backend가 모두 있는 열린 이슈' — GROUP BY 절을 포함하는 쿼리를 생성합니다.

소개#

GitLab에는 이슈, 머지 리퀘스트, 에픽에 할당할 수 있는 라벨이 있습니다. 이러한 객체의 라벨은 다형성 테이블 label_links를 통한 다대다(many-to-many) 관계입니다.

여러 라벨로 이러한 객체를 필터링하는 경우 — 예를 들어 '라벨 ~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

구체적으로:

  • GROUP BY issues.id는 결과를 이슈별로 그룹화합니다.

  • HAVING (COUNT(DISTINCT labels.title) = 2)는 일치하는 모든 이슈에 두 라벨이 모두 있도록 보장합니다.

이 방법은 이상적인 것보다 더 복잡합니다. 쿼리 구성 시 오류가 발생하기 쉬워집니다 (예: 이슈 #15557).

시도 A: WHERE EXISTS#

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

이슈 #37137과 관련 머지 리퀘스트에서 GROUP BYWHERE 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과 유사한 접근 방식을 따랐습니다. 이번에는 EXISTS 절에서 JOIN을 피하고 label_links.label_id로 직접 필터링할 수 있도록 필터에 사용된 라벨의 ID를 가져오는 별도의 쿼리를 실행했습니다. 또한 이 쿼리 속도를 높이기 위해 target_id, label_id, target_type 칼럼에 대한 새 인덱스를 label_links에 추가했습니다.

라벨 ID를 찾는 것은 단순하지 않았습니다. 단일 루트 네임스페이스 내에서 동일한 제목을 가진 라벨이 여러 개 있을 수 있기 때문입니다. 라벨 ID를 제목별로 그룹화한 다음 EXISTS 절에서 ID 배열을 사용하여 이 문제를 해결했습니다.

이로 인해 성능이 크게 향상되었습니다. 그러나 이 최적화는 프로젝트나 그룹 컨텍스트가 없는 대시보드 페이지에는 적용할 수 없었습니다. 여기서 라벨 ID를 검색하려면 사용자가 접근할 수 있는 모든 프로젝트와 그룹을 검색해야 하므로 쉽게 적용할 수 없었습니다.

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

이슈 #49651에서 라벨 ID와 제목의 두 가지 옵션으로 쿼리를 위한 label_links 테이블의 역정규화를 논의했습니다.

이 두 가지를 issues, merge_requests, epics의 배열 칼럼으로 생각할 수 있습니다: issues.label_ids는 라벨 ID의 배열 칼럼이고, issues.label_titles는 라벨 제목의 배열입니다.

이러한 배열 칼럼은 매칭을 개선하기 위해 GIN 인덱스로 보완할 수 있습니다.

시도 B1: 각 객체에 라벨 ID 저장#

이 방법은 제목에 비해 몇 가지 강력한 장점이 있습니다:

  • 라벨이 삭제되거나 프로젝트가 이동되지 않는 한, 역정규화된 칼럼을 일괄 업데이트할 필요가 없습니다.

  • 제목보다 저장 공간을 덜 사용합니다.

안타깝게도 애플리케이션 설계로 인해 이 방법이 어렵습니다. 라벨 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: 각 객체에 라벨 제목 저장#

객체 업데이트 관점에서 이것은 최악의 옵션입니다. 다음의 경우에 객체를 일괄 업데이트해야 합니다:

  • 객체가 한 프로젝트에서 다른 프로젝트로 이동될 때.

  • 프로젝트가 한 그룹에서 다른 그룹으로 이동될 때.

  • 라벨 이름이 변경될 때.

  • 라벨이 삭제될 때.

또한 저장 공간을 훨씬 더 많이 사용합니다. 그러나 쿼리는 간단합니다:

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

그리고 이슈 #49651의 테스트에서 이 방법이 빠를 수 있다는 것을 확인했습니다.

그러나 현재로서는 단점이 장점보다 큽니다.

결론#

역정규화가 필요 없으면서 쿼리 성능을 크게 향상시키는 방법 A2를 발견했습니다. 이 방법이 모든 경우에 적용되지는 않았지만, 나머지 경우에는 방법 A1을 적용하여 모든 시나리오에서 GROUP BYHAVING 절을 제거할 수 있었습니다.

이로 인해 쿼리가 단순화되고 가장 일반적인 경우의 성능이 향상되었습니다.

라벨로 필터링

GitLab v19.1
원문 보기
요약

GitLab에는 이슈, 머지 리퀘스트, 에픽에 할당할 수 있는 라벨이 있습니다. 여러 라벨로 이러한 객체를 필터링하는 경우 — 예를 들어 '라벨 ~Plan과 라벨 ~backend가 모두 있는 열린 이슈' — GROUP BY 절을 포함하는 쿼리를 생성합니다.

소개#

GitLab에는 이슈, 머지 리퀘스트, 에픽에 할당할 수 있는 라벨이 있습니다. 이러한 객체의 라벨은 다형성 테이블 label_links를 통한 다대다(many-to-many) 관계입니다.

여러 라벨로 이러한 객체를 필터링하는 경우 — 예를 들어 '라벨 ~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

구체적으로:

  • GROUP BY issues.id는 결과를 이슈별로 그룹화합니다.

  • HAVING (COUNT(DISTINCT labels.title) = 2)는 일치하는 모든 이슈에 두 라벨이 모두 있도록 보장합니다.

이 방법은 이상적인 것보다 더 복잡합니다. 쿼리 구성 시 오류가 발생하기 쉬워집니다 (예: 이슈 #15557).

시도 A: WHERE EXISTS#

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

이슈 #37137과 관련 머지 리퀘스트에서 GROUP BYWHERE 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과 유사한 접근 방식을 따랐습니다. 이번에는 EXISTS 절에서 JOIN을 피하고 label_links.label_id로 직접 필터링할 수 있도록 필터에 사용된 라벨의 ID를 가져오는 별도의 쿼리를 실행했습니다. 또한 이 쿼리 속도를 높이기 위해 target_id, label_id, target_type 칼럼에 대한 새 인덱스를 label_links에 추가했습니다.

라벨 ID를 찾는 것은 단순하지 않았습니다. 단일 루트 네임스페이스 내에서 동일한 제목을 가진 라벨이 여러 개 있을 수 있기 때문입니다. 라벨 ID를 제목별로 그룹화한 다음 EXISTS 절에서 ID 배열을 사용하여 이 문제를 해결했습니다.

이로 인해 성능이 크게 향상되었습니다. 그러나 이 최적화는 프로젝트나 그룹 컨텍스트가 없는 대시보드 페이지에는 적용할 수 없었습니다. 여기서 라벨 ID를 검색하려면 사용자가 접근할 수 있는 모든 프로젝트와 그룹을 검색해야 하므로 쉽게 적용할 수 없었습니다.

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

이슈 #49651에서 라벨 ID와 제목의 두 가지 옵션으로 쿼리를 위한 label_links 테이블의 역정규화를 논의했습니다.

이 두 가지를 issues, merge_requests, epics의 배열 칼럼으로 생각할 수 있습니다: issues.label_ids는 라벨 ID의 배열 칼럼이고, issues.label_titles는 라벨 제목의 배열입니다.

이러한 배열 칼럼은 매칭을 개선하기 위해 GIN 인덱스로 보완할 수 있습니다.

시도 B1: 각 객체에 라벨 ID 저장#

이 방법은 제목에 비해 몇 가지 강력한 장점이 있습니다:

  • 라벨이 삭제되거나 프로젝트가 이동되지 않는 한, 역정규화된 칼럼을 일괄 업데이트할 필요가 없습니다.

  • 제목보다 저장 공간을 덜 사용합니다.

안타깝게도 애플리케이션 설계로 인해 이 방법이 어렵습니다. 라벨 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: 각 객체에 라벨 제목 저장#

객체 업데이트 관점에서 이것은 최악의 옵션입니다. 다음의 경우에 객체를 일괄 업데이트해야 합니다:

  • 객체가 한 프로젝트에서 다른 프로젝트로 이동될 때.

  • 프로젝트가 한 그룹에서 다른 그룹으로 이동될 때.

  • 라벨 이름이 변경될 때.

  • 라벨이 삭제될 때.

또한 저장 공간을 훨씬 더 많이 사용합니다. 그러나 쿼리는 간단합니다:

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

그리고 이슈 #49651의 테스트에서 이 방법이 빠를 수 있다는 것을 확인했습니다.

그러나 현재로서는 단점이 장점보다 큽니다.

결론#

역정규화가 필요 없으면서 쿼리 성능을 크게 향상시키는 방법 A2를 발견했습니다. 이 방법이 모든 경우에 적용되지는 않았지만, 나머지 경우에는 방법 A1을 적용하여 모든 시나리오에서 GROUP BYHAVING 절을 제거할 수 있었습니다.

이로 인해 쿼리가 단순화되고 가장 일반적인 경우의 성능이 향상되었습니다.