라벨로 필터링
GitLab v19.1GitLab에는 이슈, 머지 리퀘스트, 에픽에 할당할 수 있는 라벨이 있습니다. 여러 라벨로 이러한 객체를 필터링하는 경우 — 예를 들어 '라벨 ~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 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과 유사한 접근 방식을 따랐습니다. 이번에는 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 BY와 HAVING 절을 제거할 수 있었습니다.
이로 인해 쿼리가 단순화되고 가장 일반적인 경우의 성능이 향상되었습니다.