데이터베이스 인덱스 추가
GitLab v19.4요약
인덱스는 데이터베이스 쿼리 속도를 높이는 데 사용할 수 있지만, 새 인덱스는 언제 추가해야 하는지가 관건입니다. 여기서는 user_id 칼럼으로 필터링하므로, 개발자가 이 칼럼에 인덱스를 추가하기로 결정할 수 있습니다.
인덱스는 데이터베이스 쿼리 속도를 높이는 데 사용할 수 있지만, 새 인덱스는 언제 추가해야 하는지가 관건입니다. 전통적으로 이 질문에 대한 답은 데이터를 필터링하거나 조인하는 데 사용되는 모든 칼럼에 인덱스를 추가하는 것이었습니다. 예를 들어, 다음 쿼리를 살펴보겠습니다.
SELECT *
FROM projects
WHERE user_id = 2;
여기서는 user_id 칼럼으로 필터링하므로, 개발자가 이 칼럼에
인덱스를 추가하기로 결정할 수 있습니다.
위의 방식으로 칼럼에 인덱스를 추가하는 것이 특정 경우에는 의미가 있을 수 있지만,
실제로는 부정적인 영향을 미칠 수 있습니다. 테이블에 데이터를 쓸 때마다 기존의 모든
인덱스도 업데이트되어야 합니다. 인덱스가 많을수록 이 과정이 잠재적으로 느려질 수
있습니다. 인덱스는 인덱싱된 데이터의 양과 인덱스 유형에 따라 상당한 디스크 공간을
차지할 수도 있습니다. 예를 들어, PostgreSQL은 일반 B-tree 인덱스로는 인덱싱할 수
없는 특정 데이터 유형을 인덱싱하는 데 사용할 수 있는 GIN 인덱스를 제공합니다.
그러나 이러한 인덱스는 일반적으로 B-tree 인덱스에 비해 더 많은 데이터를 차지하고
업데이트 속도가 느립니다.
이 모든 것을 고려할 때, 새 인덱스를 추가할 때는 다음 사항을 고려하는 것이 중요합니다:
- 새로운 쿼리가 기존 인덱스를 최대한 재사용하는지 확인합니다.
- 인덱스를 사용하는 것이 테이블의 행을 순회하는 것보다 빠를 만큼 충분한 데이터가 있는지 확인합니다.
- 인덱스를 유지하는 오버헤드가 쿼리 실행 시간 단축을 정당화할 만큼의 가치가 있는지 확인합니다.
어떤 상황에서는 인덱스가 필요하지 않을 수 있습니다:
- 테이블이 작고(
1,000개 미만의 레코드) 기하급수적으로 크기가 증가할 것으로 예상되지 않는 경우. - 기존 인덱스가 충분히 많은 행을 필터링하는 경우.
- 인덱스 추가 후 쿼리 실행 시간 단축이 크지 않은 경우.
또한 와이드 인덱스는 쿼리의 모든 필터 조건과 일치할 필요가 없습니다. 인덱스 조회의 선택도(selectivity)가 충분히 작을 만큼 칼럼을 포함하기만 하면 됩니다.
쿼리 재사용#
첫 번째 단계는 쿼리가 기존 인덱스를 최대한 재사용하도록 하는 것입니다. 예를 들어, 다음 쿼리를 살펴보겠습니다:
SELECT *
FROM todos
WHERE user_id = 123
AND state = 'open';
이제 user_id 칼럼에는 인덱스가 있지만 state 칼럼에는 인덱스가
없다고 가정해 봅니다. state에 인덱스가 없어 이 쿼리의 성능이
나쁠 것이라고 생각할 수 있습니다. 그러나 실제로는 user_id의
인덱스가 충분한 행을 필터링할 수 있으므로 쿼리가 충분히 잘 수행될 수 있습니다.
인덱스가 재사용되는지 확인하는 가장 좋은 방법은 EXPLAIN ANALYZE를
사용하여 쿼리를 실행하는 것입니다. 조인된 테이블과 필터링에 사용되는
칼럼에 따라 추가 인덱스가 별로 도움이 되지 않을 수도 있습니다.
요약하면 다음과 같습니다:
- 기존 인덱스를 최대한 재사용하는 방식으로 쿼리를 작성하도록 합니다.
EXPLAIN ANALYZE를 사용하여 쿼리를 실행하고 출력 결과를 분석해 가장 이상적인 쿼리를 찾습니다.
부분 인덱스#
부분 인덱스(Partial Index)는 일치하는 행의 일부로 제한하는 WHERE 절이 있는
인덱스입니다. 전체 인덱스에 비해 다음과 같은 여러 이점을 제공할 수 있습니다:
- 인덱스 크기 및 메모리 사용량 감소
- 쓰기 및 vacuum 오버헤드 감소
- 선택적 조건에 대한 쿼리 성능 향상
부분 인덱스는 항상 알려진 조건으로 필터링하고 특정 데이터 하위 집합을 타깃으로 하는 쿼리에 가장 적합합니다. 일반적인 사용 사례는 다음과 같습니다:
- nullable 칼럼:
WHERE column IS NOT NULL - 불리언 플래그:
WHERE feature_enabled = true - 소프트 삭제:
WHERE deleted_at IS NULL - 상태 필터:
WHERE status IN ('queued', 'running')
새로운 부분 인덱스를 생성하기 전에 먼저 재사용 또는 수정 가능성이 있는 기존 인덱스를 검토합니다. 각 인덱스는 유지 오버헤드를 발생시키므로, 새 인덱스를 추가하는 것보다 현재 인덱스를 활용하는 것을 우선시합니다.
예시#
새로운 카운트 쿼리를 도입하는 다음 애플리케이션 코드를 살펴봅니다:
def namespace_count
NamespaceSetting.where(duo_features_enabled: duo_settings_value).count
end
def duo_settings_value
params['duo_settings_value'] == 'default_on'
end
여기서 namespace_settings는 100만 개의 레코드가 있는 테이블이고,
duo_features_enabled는 nullable 불리언 칼럼입니다.
최근에 이 칼럼을 도입했고 백필(backfill)되지 않았다고 가정합니다.
이는 namespace_settings 테이블의 레코드 대부분이 duo_features_enabled에 대해
NULL 값을 가지고 있음을 의미합니다. 또한 duo_settings_value는
true 또는 false만 반환한다는 것을 알 수 있습니다.
모든 행을 인덱싱하는 것은 NULL 값이 대부분이므로 비효율적입니다. 대신,
관심 있는 데이터만 타깃으로 하는 부분 인덱스를 도입할 수 있습니다:
CREATE INDEX index_namespace_settings_on_duo_features_enabled_not_null
ON namespace_settings (duo_features_enabled)
WHERE duo_features_enabled IS NOT NULL;
이제 전체 인덱스 크기의 일부에 불과한 인덱스가 생성되었고, 쿼리 플래너는 수십만 개의 관련 없는 레코드를 효과적으로 건너뛸 수 있습니다.
데이터 크기#
데이터베이스는 일반 순차 스캔(모든 행 순회)이 더 빠른 경우, 특히 작은 테이블에서는 인덱스를 사용하지 않을 수 있습니다.
테이블이 증가할 것으로 예상되고 쿼리가 많은 행을 필터링해야 하는 경우
인덱스 추가를 고려합니다. 테이블 크기가 작거나(1,000개 미만의 레코드)
기존 인덱스가 이미 충분한 행을 필터링하는 경우에는 인덱스를 추가하지 않아도 됩니다.
유지 오버헤드#
인덱스는 모든 테이블 쓰기 시마다 업데이트되어야 합니다. PostgreSQL의 경우, 테이블에 데이터를 쓸 때마다 모든 기존 인덱스가 업데이트됩니다. 결과적으로 같은 테이블에 많은 인덱스가 있으면 쓰기 속도가 느려집니다. 따라서 쿼리 성능과 추가 인덱스를 유지하는 오버헤드를 균형 있게 고려하는 것이 중요합니다.
예를 들어, 인덱스를 추가하면 SELECT 실행 시간이 5밀리초 줄어들지만 INSERT/UPDATE/DELETE 실행 시간이 10밀리초 증가한다면, 새 인덱스는 그만한 가치가 없을 수 있습니다. 새 인덱스는 SELECT 실행 시간이 줄어들고 INSERT/UPDATE/DELETE 실행 시간이 영향을 받지 않을 때 더 가치가 있습니다.
인덱스 제한#
GitLab은 테이블당 인덱스 15개 제한을 적용합니다. 이 제한은 다음과 같은 효과가 있습니다:
- 최적의 데이터베이스 성능을 유지하는 데 도움이 됩니다
- 유지 오버헤드를 줄입니다
- 과도한 디스크 공간 사용을 방지합니다
이미 인덱스가 15개 있는 테이블에 인덱스를 추가해야 하는 경우 다음을 고려합니다:
- 사용되지 않는 인덱스 제거
- 기존 인덱스 결합
- 여러 쿼리 패턴을 지원할 수 있는 복합 인덱스 사용
일부 테이블은 더 이상 인덱스를 추가해서는 안 됩니다#
자주 접근되는 특정 테이블에는 추가 인덱스 생성을 방지하는 RuboCop 검사
(PreventIndexCreation)가 있습니다.
이는 LockManager LWLock 경합 때문입니다.
같은 이유로, 이러한 테이블에 새 칼럼을 추가하는 것을 방지하는 RuboCop 검사
(AddColumnsToWideTables)도 있습니다.
가능하면 인덱스 추가와 애플리케이션 코드 변경을 함께 수행합니다#
불필요한 인덱스 생성 위험을 최소화하려면, 가능한 경우 같은 머지 리퀘스트에서 다음을 함께 수행합니다:
- 애플리케이션 코드 변경
- 인덱스 생성 또는 제거
인덱스를 생성하는 마이그레이션은 보통 짧아서 머지 리퀘스트의 크기를 크게 늘리지 않습니다. 이렇게 하면 백엔드 및 데이터베이스 리뷰어가 머지 리퀘스트나 커밋 간에 컨텍스트를 전환하지 않고도 더 효율적으로 리뷰할 수 있습니다.
사용할 마이그레이션 유형#
권위 있는 가이드는 마이그레이션 스타일 가이드입니다. 확실하지 않은 경우 이 가이드를 참고합니다.
빠르게 참고할 수 있도록 권장 선택과 함께 일반적인 시나리오 몇 가지를 정리합니다.
기존 쿼리를 개선하기 위한 인덱스 추가#
배포 후 마이그레이션(post-deployment migration)을 사용합니다. 기존 쿼리는 추가된 인덱스 없이도 이미 작동하며, 애플리케이션 운영에 중요하지 않습니다.
인덱싱이 완료되는 데 오랜 시간이 걸리는 경우 (배포 후 마이그레이션의 동시 작업은 20분 미만이어야 합니다), 비동기적으로 인덱싱하는 것을 고려합니다.
새 쿼리 또는 업데이트된 쿼리를 지원하기 위한 인덱스 추가#
새 쿼리 또는 업데이트된 쿼리의 쿼리 플랜은 항상 검토합니다. 먼저 전용 인덱스 없이도 타임아웃이 발생하지 않는지, 권장 쿼리 실행 시간을 크게 초과하지 않는지 확인합니다.
쿼리가 타임아웃이 발생하지 않고 쿼리 실행 시간도 초과하지 않는 경우:
- 새 쿼리의 성능을 개선하기 위해 추가한 인덱스는 애플리케이션 운영에 중요하지 않습니다.
- 배포 후 마이그레이션을 사용하여 인덱스를 생성합니다.
- 같은 머지 리퀘스트에서 새 쿼리를 생성하고 사용하는 애플리케이션 코드 변경 사항을 함께 배포합니다.
타임아웃이 발생하거나 쿼리 실행 시간을 초과하는 쿼리는 GitLab.com에서만 그런지, 모든 GitLab 인스턴스에서 그런지에 따라 서로 다른 조치가 필요합니다. 대부분의 기능은 가장 큰 GitLab 설치 중 하나인 GitLab.com에서만 전용 인덱스가 필요합니다.
새 쿼리 또는 업데이트된 쿼리가 GitLab.com에서 느린 경우#
두 개의 MR을 사용하여 배포 후 마이그레이션에서 인덱스를 생성하고 애플리케이션 코드를 변경합니다:
- 첫 번째 MR은 배포 후 마이그레이션을 사용하여 인덱스를 생성합니다.
- 두 번째 MR은 애플리케이션 코드를 변경합니다. 첫 번째 MR의 배포 후 마이그레이션이 GitLab.com에서 실행된 후에만 머지해야 합니다.
기능 플래그를 사용할 수 있는 경우, 기능 플래그 뒤에 코드 변경을 포함하는 단일 MR을 사용할 수 있습니다. 배포 후 마이그레이션도 동시에 포함합니다. 배포 후 마이그레이션이 실행된 후 기능 플래그를 활성화할 수 있습니다.
GitLab.com의 경우, 지속적 통합을 통해 단일 릴리스 전체에 걸쳐 배포 후 마이그레이션을 실행합니다:
- 시간
t에 일군의 머지 리퀘스트가 머지되어 배포 준비가 됩니다. t+1에 해당 그룹의 일반 마이그레이션이 GitLab.com의 스테이징 및 프로덕션 데이터베이스에서 실행됩니다.t+2에 해당 그룹의 애플리케이션 코드 변경 사항이 롤링 방식으로 배포되기 시작합니다.
애플리케이션 코드 변경 사항이 완전히 배포된 후, 릴리스 매니저는 훨씬 나중 시점에 자신의 재량으로 배포 후 마이그레이션을 실행하도록 선택할 수 있습니다. 배포 후 마이그레이션은 GitLab.com의 가용성에 따라 하루에 한 번 실행됩니다. 이런 이유로, 두 번째 MR을 머지하기 전에 첫 번째 MR에 포함된 배포 후 마이그레이션이 실행되었는지 확인이 필요합니다.
새 쿼리 또는 업데이트된 쿼리가 대규모 GitLab 인스턴스에서 느릴 수 있는 경우#
GitLab Self-Managed 인스턴스에서는 쿼리 성능을 직접 확인할 수 없습니다. PostgreSQL은 데이터 분포를 기반으로 실행 계획을 생성하므로, 쿼리 성능을 추측하는 것은 어려운 작업입니다.
GitLab Self-Managed 인스턴스에서 쿼리 성능이 우려되어 GitLab Self-Managed 인스턴스에 인덱스가 필요하다고 판단한 경우, 다음 권장 사항을 따릅니다:
- 제로 다운타임 업그레이드를 따르는 GitLab Self-Managed 인스턴스의 경우, 애플리케이션 코드 배포 후 업그레이드를 수행할 때 배포 후 마이그레이션이 실행됩니다.
- 제로 다운타임 업그레이드를 따르지 않는 GitLab Self-Managed 인스턴스의 경우, 관리자가 일반 마이그레이션 실행 후 자신이 선택한 시점에 해당 릴리스의 배포 후 마이그레이션을 나중에 실행하도록 선택할 수 있습니다. 애플리케이션 코드는 업그레이드 시 배포됩니다.
이런 이유로, 애플리케이션은 배포 후 마이그레이션이 적용하는 데이터베이스 스키마가 같은 릴리스에서 배포되었다고 가정해서는 안 됩니다. 애플리케이션 코드는 같은 릴리스의 배포 후 마이그레이션에서 추가된 인덱스가 없어도 계속 작동해야 합니다.
인덱스 생성에 걸리는 시간에 따라 두 가지 옵션이 있습니다:
- 단일 릴리스: 일반 마이그레이션으로 필요한 인덱스를 매우 빠르게 생성할 수 있다면 (보통 테이블이 새로 만들어졌거나 매우 작은 경우) 일반 마이그레이션에서 인덱스를 생성하고, 같은 MR 및 마일스톤에서 애플리케이션 코드 변경 사항을 배포할 수 있습니다.
- 최소 두 개의 릴리스: 필요한 인덱스를 생성하는 데 시간이 걸리는 경우, 한 릴리스에서 PDM으로 인덱스를 생성한 다음 다음 릴리스까지 기다려 인덱스에 의존하는 애플리케이션 코드 변경 사항을 만들어야 합니다.
기존 테이블에 제약 조건으로 작용하는 고유 인덱스 추가#
PostgreSQL의 고유 인덱스는 제약 조건으로 작용합니다. 기존 테이블에 이를 추가하는 것은 까다로울 수 있습니다.
GitLab.com 및 GitLab Self-Managed 인스턴스에서 테이블이 항상 매우 작다고 보장되지 않는 한, 여러 릴리스에 걸친 여러 배포 후 마이그레이션을 사용하여 다음을 수행해야 합니다:
- 중복 레코드를 제거 및/또는 수정합니다.
- 기존 칼럼을 제한하는 고유 인덱스를 도입합니다.
다중 릴리스 접근 방식은 NOT NULL 제약 조건 추가 섹션을 참고합니다.
PostgreSQL의 고유 인덱스는 일반 제약 조건과 달리 비검증 상태로 도입할 수 없습니다. 제거 및 수정이 진행되는 동안 새 레코드와 업데이트된 레코드에 원하는 고유성을 적용하려면 PostgreSQL의 부분 고유 인덱스와 애플리케이션 유효성 검사를 함께 사용해야 합니다.
작업의 세부 사항은 상황에 따라 다르며 서로 다른 접근 방식이 필요할 수 있습니다. 작업을 계획할 때는 Database 팀, 리뷰어 또는 메인테이너에게 문의합니다.
모든 고유 인덱스는 범위가 지정되어야 합니다#
자세한 내용은 Cells의 고유 제약 조건을 참고합니다.
nullable 칼럼의 고유 인덱스#
기본적으로 PostgreSQL은 고유 인덱스에서 NULL 값을 서로 다른 값으로 처리합니다.
이는 (project_id, name)에 대한 고유 인덱스가 같은 project_id에 대해
name IS NULL인 여러 행을 허용한다는 것을 의미합니다.
PostgreSQL 15는 고유 인덱스에 NULLS NOT DISTINCT
절을 도입했습니다. 이를 활성화하면 PostgreSQL은 NULL 값을 동일하게 처리하므로,
인덱스는 고유 조합당 최대 하나의 NULL만 허용합니다.
NULL 값을 포함한 완전한 고유성을 적용해야 하는 경우
nulls_not_distinct: true를 사용합니다:
add_concurrent_index(
:vulnerability_finding_links,
%i[vulnerability_occurrence_id name url],
unique: true,
nulls_not_distinct: true,
name: "finding_link_occurrence_id_name_url_idx"
)
이는 두 개의 인덱스를 결합하던 이전 패턴을 대체합니다. null이 아닌 행을 위한
일반 고유 인덱스와 WHERE column IS NULL 조건이 있는 부분 고유 인덱스를 결합하는
방식이었으나, 단일 NULLS NOT DISTINCT 인덱스가 더 간단하고 디스크 공간도 덜 사용합니다.
사용되지 않는 인덱스 삭제#
사용되지 않는 인덱스는 유지 오버헤드를 증가시키고 디스크 공간을 소비하며, 성능상의 이점을 제공하지 않으면서 쿼리 계획 효율성을 저하시킬 수 있으므로 삭제해야 합니다. 그러나 여전히 사용 중인 인덱스를 삭제하면 쿼리 성능 저하나 타임아웃이 발생하여 인시던트로 이어질 수 있습니다. 제거하기 전에 GitLab.com과 GitLab Self-Managed 인스턴스 양쪽 모두에서 인덱스가 사용되지 않는지 확인하는 것이 중요합니다.
- 대규모 테이블의 경우 인덱스를 비동기적으로 삭제하는 것을 고려합니다.
- 파티션된 테이블의 경우 부모 인덱스만 삭제할 수 있습니다. PostgreSQL은 자식 인덱스 (즉, 해당 파티션에 있는 인덱스)가 독립적으로 제거되는 것을 허용하지 않습니다.
자동 감지 및 제거#
CleanupUnusedIndexes keep과
CleanupUnusedPartitionedIndexes keep은
gitlab-housekeeper를 통해 예약된 일정으로
실행되며, 이 프로세스의 첫 단계를 자동화합니다. CleanupUnusedIndexes는 일반적인
비고유 인덱스를 처리하고, CleanupUnusedPartitionedIndexes는 파티션된 부모 인덱스를 처리합니다.
두 keep은 다음을 수행합니다:
pg_stat_user_indexes_idx_scan메트릭을 기준으로, GitLab.com에서 스캔 기록이 없는 비고유 인덱스를 찾습니다. 파티션된 인덱스의 경우, 연결된 모든 자식 파티션 인덱스가 스캔 0회를 보이는 것을 의미합니다.- 외래 키를 지원하는 인덱스와 보존 목록에 있는 인덱스는 건너뜁니다.
- 인덱스를 제거하는 배포 후 마이그레이션과 함께
automation:cleanup-unused-indexes레이블이 지정된 머지 리퀘스트를 엽니다:db/docs의table_size가large또는over_limit이 아닌 테이블의 경우, 마이그레이션이 인덱스를 제거합니다.db/docs의table_size가large또는over_limit인 테이블의 경우, 마이그레이션은 대신prepare_async_index_removal로 제거를 예약하고, 해당 테이블을 소유한 팀이 후속 동기 제거를 처리합니다. 자세한 내용은 인덱스를 비동기적으로 삭제를 참고합니다.- 파티션된 인덱스의 경우, 마이그레이션이
remove_concurrent_partitioned_index_by_name으로 부모 인덱스를 제거하며, 이는 모든 파티션에 전파됩니다. 파티션된 인덱스에는 비동기 제거 경로가 없습니다.
- 영향을 받는 테이블의
db/docs딕셔너리 항목을 기준으로 해당 테이블을 소유한 팀의 구성원에게 머지 리퀘스트를 할당하며, 찾을 수 없는 경우 데이터베이스 팀 구성원으로 대체합니다.
이 자동화가 생성하는 머지 리퀘스트는 제거 제안일 뿐이며 최종 판단이 아닙니다. 스캔 0회 신호는 GitLab.com에 한정된 짧은 기간만 포함하므로, 인덱스가 사용되지 않는지 확인의 검사를 여전히 완료해야 합니다. 특히 GitLab Self-Managed 또는 GitLab Dedicated 인스턴스에서 해당 인덱스가 필요하지 않은지, 주기적인 cron job과 같이 드물게 실행되는 프로세스에서 사용되지 않는지 확인합니다. 각 머지 리퀘스트에는 이 체크리스트와 함께 지난 6개월간의 사용량을 확인할 수 있는 Grafana 링크가 포함됩니다.
이러한 머지 리퀘스트 중 하나를 받았는데 해당 인덱스를 유지해야 한다면, 인덱스를 보존 목록에 추가하고 머지 리퀘스트를 닫습니다. 두 keep 모두 해당 목록에 있는 인덱스의 제거를 다시 제안하지 않습니다.
사용 가능한 미사용 인덱스 찾기#
제거 후보인 인덱스를 확인하려면 다음 쿼리를 실행할 수 있습니다:
SELECT relname as table_name, indexrelname as index_name, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass))
FROM pg_stat_all_indexes
WHERE schemaname = 'public'
AND idx_scan = 0
AND idx_tup_read = 0
AND idx_tup_fetch = 0
ORDER BY pg_relation_size(indexrelname::regclass) desc;
이 쿼리는 통계가 마지막으로 재설정된 이후 사용되지 않은 모든 인덱스 목록을 출력하며, 인덱스 크기를 기준으로 내림차순으로 정렬합니다. 각 칼럼의 의미에 대한 자세한 내용은 https://www.postgresql.org/docs/16/monitoring-stats.html에서 확인할 수 있습니다.
GitLab.com의 경우, postgres.ai에서 최신 생성된 프로덕션 보고서를
확인하고 H002 Unused Indexes 파일을 검사할 수 있습니다.
이 보고서는 마지막 통계 재설정 이후 사용 기록이 없는 인덱스만 표시합니다. 인덱스가 절대 사용되지 않는다는 것을 보장하지는 않습니다.
인덱스가 사용되지 않는지 확인#
이 섹션에는 인덱스를 평가하고 제거가 안전한지 확인하는 데 도움이 되는 자료가 있습니다. 이는 제안된 가이드일 뿐이며 완전하지 않습니다. 궁극적인 목표는 인덱스 삭제를 정당화할 충분한 데이터를 수집하는 것입니다.
인덱스가 사용되지 않는다는 잘못된 인상을 줄 수 있는 요인에 주의해야 합니다. 예를 들면 다음과 같습니다:
- GitLab Self-Managed에서는 실행되지만 GitLab.com에서는 실행되지 않는 쿼리가 있을 수 있습니다.
- 인덱스가 주기적인 cron job과 같이 매우 드문 프로세스에 사용될 수 있습니다.
- 데이터가 거의 없는 테이블에서는 테이블이 충분히 커지기 전까지 PostgreSQL이 인덱스 스캔보다 순차 스캔을 먼저 선호할 수 있습니다.
인덱스 사용 조사#
-
인덱스에 사용 가능한 모든 메타데이터를 수집하는 것부터 시작하여 이름과 정의를 확인합니다.
- 개발 환경의 인덱스 이름은 프로덕션과 일치하지 않을 수 있습니다. 이름이 아니라 정의를
기준으로 인덱스를 연관 짓는 것이 중요합니다. 정의를 확인하려면 다음을 할 수 있습니다:
- db/structure.sql을 수동으로 검사합니다 (이 파일에는 동적으로 생성된 파티션에 대한 데이터가 포함되어 있지 않습니다.)
- Database Lab을 사용하여 인덱스의 상태를 확인합니다.
- 파티션된 테이블의 경우, 자식 인덱스는 부모 인덱스와 다르게 명명되는 경우가 많습니다.
모든 자식 인덱스를 나열하려면 다음을 할 수 있습니다:
-
Database Lab에서
\d+을 실행합니다. -
전체 부모-자식 인덱스 구조를 더 자세히 보려면 다음 쿼리를 실행합니다:
SELECT parent_idx.relname AS parent_index, child_tbl.relname AS child_table, child_idx.relname AS child_index, dep.deptype, pg_get_indexdef(child_idx.oid) AS child_index_def FROM pg_class parent_idx JOIN pg_depend dep ON dep.refobjid = parent_idx.oid JOIN pg_class child_idx ON child_idx.oid = dep.objid JOIN pg_index i ON i.indexrelid = child_idx.oid JOIN pg_class child_tbl ON i.indrelid = child_tbl.oid WHERE parent_idx.relname = '';
-
- 개발 환경의 인덱스 이름은 프로덕션과 일치하지 않을 수 있습니다. 이름이 아니라 정의를
기준으로 인덱스를 연관 짓는 것이 중요합니다. 정의를 확인하려면 다음을 할 수 있습니다:
-
GitLab.com의 경우 Grafana에서 인덱스 사용 데이터를 볼 수 있습니다.
-
관련 인덱스로 필터링한
pg_stat_user_indexes_idx_scan메트릭을 최소 지난 6개월 동안 쿼리합니다. 아래 쿼리는 모든 데이터베이스 인스턴스를 결합한 인덱스 사용률을 보여줍니다.sum by (indexrelname) (rate(pg_stat_user_indexes_idx_scan{env="gprd", relname=~"", indexrelname=~""}[30d])) -
파티션된 테이블의 경우, 부모를 삭제하기 전에 모든 자식 인덱스가 사용되지 않는지 확인해야 합니다.
-
데이터가 인덱스 사용이 0회이거나 미미함을 보인다면 제거의 강력한 후보입니다. 다만 이는 GitLab.com에서의 사용량에 한정된다는 점을 유의해야 합니다. GitLab Self-Managed 인스턴스에서 안전하게 제거할 수 있는지 확인하려면 관련된 모든 쿼리를 조사해야 합니다.
사용량이 낮은 인덱스도 다른 기존 인덱스가 해당 인덱스를 사용하는 쿼리를 충분히 지원할 수 있음을 확인할 수 있다면 여전히 삭제할 수 있습니다. PostgreSQL은 데이터 분포 통계를 기반으로 사용할 인덱스를 결정하므로, 두 인덱스가 모두 쿼리를 적절히 지원하더라도 특정 상황에서는 한쪽을 약간 더 선호할 수 있으며, 이것이 간헐적인 사용의 원인일 수 있습니다.
관련 쿼리 조사#
다음은 인덱스를 사용할 수 있는 모든 쿼리를 찾는 방법입니다. 쿼리가 실행되거나 실행될 수 있는 컨텍스트를 이해하는 것이 중요합니다. 이를 통해 인덱스가 다음 중 하나에 해당하는지 확인할 수 있습니다:
- GitLab.com이나 GitLab Self-Managed에 해당 인덱스에 의존하는 쿼리가 없는 경우.
- 다른 기존 인덱스로 충분히 지원될 수 있는 경우.
-
인덱스의 출처를 조사합니다.
- 인덱스를 도입한 커밋 이력, 관련 머지 리퀘스트, 이슈를 살펴봅니다.
- 다음과 같은 질문에 대한 답을 찾아봅니다:
- 인덱스가 처음에 왜 추가되었는지, 어떤 쿼리를 지원하기 위한 것이었는지 확인합니다.
- 해당 쿼리가 아직 존재하고 실행되는지 확인합니다.
- GitLab Self-Managed 인스턴스에만 해당되는지 확인합니다.
-
rspec:merge-auto-explain-logsCI job 실행에서 출력된 쿼리를 검토합니다.- 이 job은 테스트를 통해 실행된 쿼리를 수집하고 분석합니다. 출력은 아티팩트로 저장됩니다:
auto_explain/auto_explain.ndjson.gz - 항상 100% 테스트 커버리지를 확보하지는 않으므로, 이 job이 가능한 모든 쿼리와 변형을 캡처하지 못할 수 있습니다.
- 이 job은 테스트를 통해 실행된 쿼리를 수집하고 분석합니다. 출력은 아티팩트로 저장됩니다:
-
Kibana의 PostgreSQL 로그에 기록된 쿼리를 검토합니다.
-
일반적으로 인덱스 정의의 테이블 이름과 주요 칼럼을 포함하는
json.sql값으로 필터링할 수 있습니다. 예제 KQL:json.sql: AND json.sql: ** -
인덱스 사용에 영향을 미치는 요소는 많지만, 쿼리의 필터링 및 정렬 절이 가장 큰 영향을 미치는 경우가 많습니다. 일반적인 가이드라인은 조건이 인덱스 구조와 일치하는 쿼리를 찾는 것입니다. 예를 들어, PostgreSQL은 인덱스의 선행 칼럼을 필터링하고 부분 조건(있는 경우)을 만족하는 쿼리에 B-Tree 인덱스를 더 많이 활용합니다.
-
주의: 로그는 최근 7일만 보관하며, 이 데이터는 GitLab Self-Managed 사용에는 적용되지 않습니다.
-
-
GitLab 코드베이스를 수동으로 검색합니다.
- 이 과정은 번거로울 수 있지만, 이전 단계에서 놓친 다른 쿼리가 없는지 확인하는 가장 신뢰할 수 있는 방법입니다. 특히 드물게 실행되거나 GitLab Self-Managed 인스턴스에만 적용되는 쿼리가 그렇습니다.
- 인덱스가 처음 추가된 후 한참 지나서 도입된 쿼리가 있을 수 있으므로, 인덱스 출처에만 의존할 수는 없습니다. 코드베이스의 현재 상태도 함께 검사해야 합니다.
- 검색 방향을 잡으려면 테이블이 어떻게 사용되는지, 어떤 기능이 접근하는지에 대한 컨텍스트를 모읍니다. 인덱스 정의의 주요 칼럼이 포함된 쿼리, 특히 필터링이나 정렬 절에 속하는 칼럼을 찾습니다.
- 또 다른 방법은 모델/테이블 이름과 관련 칼럼으로 키워드 검색을 하는 것입니다. 다만 일부 쿼리는 여러 파일에 걸쳐 동적으로 컴파일될 수 있으므로 이 방법은 더 까다롭고 오래 걸릴 수 있습니다.
관련 쿼리를 수집한 후에는 EXPLAIN 플랜을 확인하여 쿼리가 해당 인덱스에 의존하는지 평가할 수 있습니다. 이 과정에는 인덱스가 쿼리를 어떻게 지원하는지, 데이터 분포 변화에 따라 사용이 어떻게 영향받는지에 대한 충분한 이해가 필요합니다. 평가에는 데이터베이스 도메인 전문가의 자문을 구하는 것을 권장합니다.
복합 인덱스 칼럼 순서#
인덱스를 삭제하거나 대체할 때, 개발자는 기존 복합 인덱스가 대체 역할을 할 수 있다고 가정하는 경우가 있습니다. 그러나 PostgreSQL B-tree 인덱스는 쿼리가 인덱스의 선행(맨 왼쪽) 칼럼을 필터링할 때 가장 효율적입니다. 복합 인덱스는 선행 칼럼이 아닌 칼럼만으로 필터링하는 쿼리를 효율적으로 지원할 수 없습니다.
예를 들어, 다음 인덱스가 있는 ssh_signatures 테이블을 살펴봅니다:
- 단일 칼럼 인덱스:
(commit_sha)에 대한index_ssh_signatures_on_commit_sha - 복합 인덱스:
(project_id, commit_sha)에 대한index_ssh_signatures_on_project_id_and_commit_sha
복합 인덱스 (project_id, commit_sha)는 다음을 효율적으로 지원할 수 있습니다:
-
project_id와commit_sha모두로 필터링하는 쿼리:SELECT * FROM ssh_signatures WHERE project_id = 1 AND commit_sha = 'abc123'; -
project_id(선행 칼럼)만으로 필터링하는 쿼리:SELECT * FROM ssh_signatures WHERE project_id = 1;
그러나 복합 인덱스는 commit_sha만으로 필터링하는 쿼리를 효율적으로 지원할 수 없습니다:
SELECT * FROM ssh_signatures WHERE commit_sha = 'abc123';
이 쿼리에는 단일 칼럼 인덱스 index_ssh_signatures_on_commit_sha가 필요합니다. 이를 삭제하면
쿼리 성능이 저하되어, 프로덕션에서 타임아웃이나 인시던트로 이어질 수 있습니다.
인덱스를 삭제하기 전에, 해당 인덱스를 사용하는 모든 쿼리가 다른 기존 인덱스로 효율적으로 지원될 수 있는지 확인해야 합니다. 복합 인덱스의 칼럼 순서에 특히 주의하고, 쿼리가 선행 칼럼을 필터링하는지 확인합니다. PostgreSQL이 복합 인덱스를 사용하는 방식에 대한 자세한 내용은 PostgreSQL의 다중 칼럼 인덱스 문서를 참고합니다.
사용되지 않는다고 생각하는 인덱스를 삭제하는 경우, 인덱스 사용 통계를 확인합니다.
인덱스 이름 요구 사항#
복잡한 정의를 가진 인덱스는 마이그레이션 메서드의 암묵적인 명명 동작에 의존하지 않고 명시적으로 이름을 지정해야 합니다. 즉, 다음 옵션 중 하나 이상을 사용하여 생성한 인덱스에는 반드시 명시적인 이름 인수를 제공해야 합니다:
whereusingorderlengthtypeopclass
인덱스 이름에 대한 고려 사항#
제약 조건 이름 규칙 페이지를 확인합니다.
명시적 이름이 필요한 이유#
Rails는 데이터베이스에 독립적이므로, 모든 인덱스의 필수 옵션인 테이블 이름과 칼럼 이름만으로 인덱스 이름을 생성합니다. 예를 들어, 마이그레이션에서 다음 두 인덱스가 생성된다고 가정해 봅니다:
def up
add_index :my_table, :my_column
add_index :my_table, :my_column, where: 'my_column IS NOT NULL'
end
두 번째 인덱스 생성은 실패합니다. Rails가 두 인덱스에 대해 동일한 이름을 생성하기 때문입니다.
이 명명 문제는 index_exists? 메서드의 동작으로 인해 더 복잡해집니다.
이 메서드는 비교할 때 인덱스의 테이블 이름, 칼럼 이름,
고유성 사양만 고려합니다. 다음을 살펴봅니다:
def up
unless index_exists?(:my_table, :my_column, where: 'my_column IS NOT NULL')
add_index :my_table, :my_column, where: 'my_column IS NOT NULL'
end
end
index_exists? 호출은 :my_table과 :my_column에 어떤
인덱스라도 존재하면 true를 반환하며, 인덱스 생성은 건너뛰어집니다.
add_concurrent_index 헬퍼는 데이터가 있는 테이블에 인덱스를 생성하기
위한 필수 요건입니다. 트랜잭션 마이그레이션 내부에서는 사용할 수 없으므로,
인덱스가 이미 존재하는지 감지하는 내장 검사가 있습니다. 일치하는 항목이
발견되면 인덱스 생성을 건너뜁니다.
명시적인 이름 인수가 없으면 Rails가 index_exists?에 대해
거짓 양성을 반환할 수 있어, 필요한 인덱스가 제대로 생성되지
않을 수 있습니다. 특정 유형의 인덱스에 항상 이름을 요구함으로써
오류 발생 가능성을 크게 줄일 수 있습니다.
인덱스 존재 여부 테스트#
이름으로 인덱스의 존재를 테스트하는 가장 쉬운 방법은 index_name_exists? 메서드를 사용하는 것이지만, index_exists? 메서드를 이름 옵션과 함께 사용할 수도 있습니다. 예를 들면 다음과 같습니다:
class MyMigration < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_name'
def up
# an index must be conditionally created due to schema inconsistency
unless index_exists?(:table_name, :column_name, name: INDEX_NAME)
add_index :table_name, :column_name, name: INDEX_NAME
end
end
def down
# no op
end
end
add_concurrent_index, remove_concurrent_index, remove_concurrent_index_by_name과 같은 동시 인덱스 헬퍼는 내부적으로 이미 존재 여부 검사를 수행한다는 점을 염두에 둡니다.
임시 인덱스#
인덱스가 일시적으로만 필요한 경우가 있습니다.
예를 들어, 마이그레이션에서 테이블의 칼럼이 조건부로 업데이트될 수 있습니다. 쿼리 성능 가이드라인에 따라 업데이트해야 하는 칼럼을 쿼리하려면, 다른 경우에는 사용되지 않을 인덱스가 필요합니다.
이러한 경우에는 임시 인덱스를 고려합니다. 임시 인덱스를 지정하려면 다음을 수행합니다:
- 인덱스 이름에
tmp_접두사를 붙이고 이름 규칙을 따릅니다. - 다음(또는 이후) 마일스톤에서 인덱스를 제거하기 위한 후속 이슈를 생성합니다.
- 마이그레이션에 제거 이슈를 언급하는 주석을 추가합니다.
임시 마이그레이션은 다음과 같은 형태입니다:
INDEX_NAME = 'tmp_index_projects_on_owner_where_emails_disabled'
def up
# Temporary index to be removed in 19.3 https://gitlab.com/gitlab-org/gitlab/-/issues/1234
add_concurrent_index :projects, :creator_id, where: 'emails_disabled = false', name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :projects, INDEX_NAME
end
일괄 처리 백그라운드 마이그레이션 전 새 인덱스 분석#
일괄 처리 백그라운드 마이그레이션을 지원하기 위해 인덱스를 추가해야 하는 경우가 있습니다. 일반적으로 두 개의 배포 후 마이그레이션을 생성하여 수행합니다:
- 새 인덱스를 추가합니다. 흔히 임시 인덱스입니다.
- 일괄 처리 백그라운드 마이그레이션 큐에 추가합니다.
대부분의 경우 추가 작업이 필요하지 않습니다. 새 인덱스가 생성되며, 일괄 처리 백그라운드 마이그레이션을 큐에 추가하고 실행할 때 예상대로 사용됩니다.
그러나 표현식 인덱스는
생성 시 새 인덱스에 대한 통계를 생성하지 않습니다. Autovacuum이
결국 ANALYZE를 실행하고 통계를 업데이트하여 새 인덱스가 사용되도록 합니다.
위에서 설명한 백그라운드 마이그레이션 시나리오처럼 인덱스가 생성된 직후
필요한 경우에만 명시적으로 ANALYZE를 실행합니다.
인덱스가 생성된 후 ANALYZE를 트리거하려면, 인덱스 생성 마이그레이션을 업데이트하여
테이블을 분석합니다:
# in db/post_migrate/
INDEX_NAME = 'tmp_index_projects_on_owner_and_lower_name_where_emails_disabled'
TABLE = :projects
disable_ddl_transaction!
def up
add_concurrent_index TABLE, '(creator_id, lower(name))', where: 'emails_disabled = false', name: INDEX_NAME
connection.execute("ANALYZE #{TABLE}")
end
ANALYZE는 배포 후 마이그레이션에서만 실행해야 하며,
대규모 테이블을 대상으로 해서는 안 됩니다.
더 큰 테이블에서 이 동작이 필요한 경우, #database Slack 채널에서 도움을 요청합니다.
파티션된 테이블의 인덱스#
파티션된 테이블에서는 인덱스를 동시에 생성할 수 없습니다.
그러나 인덱스를 비동시적으로 생성하면 인덱싱 대상 테이블에 쓰기 잠금이 걸립니다.
따라서 활성 시스템에서 서비스 중단을 피하려면 인덱스를 생성할 때
CONCURRENTLY를 사용해야 합니다.
이에 대한 해결 방법으로, Database 팀이 add_concurrent_partitioned_index를 제공했습니다.
이 헬퍼는 쓰기 잠금 없이 파티션된 테이블에 인덱스를 생성합니다.
내부적으로 add_concurrent_partitioned_index는 다음을 수행합니다:
CONCURRENTLY를 사용하여 각 파티션에 인덱스를 생성합니다.- 부모 테이블에 인덱스를 생성합니다.
Rails 마이그레이션 예시입니다:
# in db/post_migrate/
class AddIndexToPartitionedTable < Gitlab::Database::Migration[2.1]
include Gitlab::Database::PartitioningMigrationHelpers
disable_ddl_transaction!
TABLE_NAME = :table_name
COLUMN_NAMES = [:partition_id, :id]
INDEX_NAME = :index_name
def up
add_concurrent_partitioned_index(TABLE_NAME, COLUMN_NAMES, name: INDEX_NAME)
end
def down
remove_concurrent_partitioned_index_by_name(TABLE_NAME, INDEX_NAME)
end
end
비동기적으로 인덱스 생성#
매우 큰 테이블에서는 인덱스 생성 관리가 어려울 수 있습니다.
add_concurrent_index는 일반 트래픽을 차단하지 않는 방식으로 인덱스를
생성하지만, 인덱스 생성이 여러 시간 동안 실행되면 여전히 문제가
될 수 있습니다. autovacuum과 같은 필요한 데이터베이스 작업이 실행될 수 없으며,
GitLab.com에서는 인덱스 생성이 완료될 때까지 배포
프로세스가 차단됩니다.
GitLab.com에 대한 영향을 제한하기 위해, 주말 시간 동안 비동기적으로 인덱스를 생성하는 프로세스가 있습니다. 일반적으로 트래픽이 낮고 배포가 적기 때문에, 인덱스 생성을 더 낮은 위험 수준에서 진행할 수 있습니다.
영향이 낮은 시간에 인덱스 생성 예약#
생성할 인덱스 예약#
- 비동기 생성을 위해 인덱스를 준비하는 배포 후 마이그레이션을 포함하는 머지 리퀘스트를 생성합니다.
- 후속 이슈를 생성합니다. 인덱스를 동기적으로 생성하는 마이그레이션을 추가하는 이슈입니다.
- 비동기 인덱스를 준비하는 머지 리퀘스트에 후속 이슈를 언급하는 주석을 추가합니다.
비동기 인덱스 헬퍼를 사용하여 인덱스를 생성하는 예시는 아래
블록에서 볼 수 있습니다. 이 마이그레이션은 인덱스 이름과 정의를
postgres_async_indexes 테이블에 입력합니다. 주말에
실행되는 프로세스가 이 테이블에서 인덱스를 가져와
생성을 시도합니다.
# in db/post_migrate/
INDEX_NAME = 'index_ci_builds_on_some_column'
# TODO: Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX
def up
prepare_async_index :ci_builds, :some_column, name: INDEX_NAME
end
def down
unprepare_async_index :ci_builds, :some_column, name: INDEX_NAME
end
파티션된 테이블의 경우 다음을 사용합니다:
# in db/post_migrate/
include Gitlab::Database::PartitioningMigrationHelpers
PARTITIONED_INDEX_NAME = 'index_p_ci_builds_on_some_column'
# TODO: Partitioned index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX
def up
prepare_partitioned_async_index :p_ci_builds, :some_column, name: PARTITIONED_INDEX_NAME
end
def down
unprepare_partitioned_async_index :p_ci_builds, :some_column, name: PARTITIONED_INDEX_NAME
end
비동기 인덱스는 GitLab.com 환경에서만 지원되므로,
prepare_async_index와 prepare_partitioned_async_index는 다른 환경에서는 no-op입니다.
prepare_partitioned_async_index는 파티션에 대한 인덱스만 비동기적으로 생성합니다. 파티션 인덱스를 파티션된 테이블에 연결하지는 않습니다.
파티션된 테이블의 다음 단계에서 add_concurrent_partitioned_index는 인덱스를 동기적으로 추가할 뿐만 아니라 파티션 인덱스를 파티션된 테이블에 연결합니다.
MR이 배포되었고 프로덕션에 인덱스가 존재하는지 확인#
-
ChatOps에서
/chatops gitlab run auto_deploy status <merge_sha>를 사용하여 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인합니다. 출력이db/gprd를 반환하면, 배포 후 마이그레이션이 프로덕션 데이터베이스에서 실행된 것입니다. 자세한 내용은 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인하는 방법을 참고합니다. -
비동기적으로 생성된 인덱스의 경우, 인덱스가 주말에 생성될 수 있도록 다음 주까지 기다립니다.
-
비동기 인덱스는 주말(
12 * * * 0,6)에 12분마다 실행되도록 예약되어 있습니다. 설정은 chef-repo와 omnibus에 설정되어 있습니다. -
주말이 지나도 인덱스가 생성되지 않으면, 최근 DB thin clone에서 아래 쿼리를 실행하여 대기 중인 인덱스 작업 상태를 확인합니다.
SELECT definition, created_at, attempts, last_error FROM postgres_async_indexes WHERE definition ILIKE 'CREATE%' ORDER BY attempts ASC, id ASC;
-
-
Database Lab을 사용하여 생성이 성공했는지 확인합니다. 출력에 인덱스가
invalid라는 표시가 없는지 확인합니다.
인덱스를 동기적으로 생성하는 마이그레이션 추가#
프로덕션 데이터베이스에 인덱스가 존재하는 것을 확인한 후, 인덱스를
동기적으로 추가하는 두 번째 머지 리퀘스트를 생성합니다. 스키마 변경 사항은
이 두 번째 머지 리퀘스트에서 structure.sql에 업데이트하고 커밋해야 합니다.
동기 마이그레이션은 GitLab.com에서는 no-op이 되지만, 다른 설치본에서는
예상대로 마이그레이션을 추가해야 합니다. 아래 블록은
앞의 비동기 예시에 대한 두 번째 마이그레이션을
만드는 방법을 보여줍니다.
add_concurrent_index로 두 번째 마이그레이션을 머지하기 전에, 프로덕션에 인덱스가 존재하는지 확인합니다.
두 번째 마이그레이션이 인덱스가 생성되기 전에 배포되면,
두 번째 마이그레이션이 실행될 때 인덱스가 동기적으로 생성됩니다.
# in db/post_migrate/
INDEX_NAME = 'index_ci_builds_on_some_column'
disable_ddl_transaction!
def up
add_concurrent_index :ci_builds, :some_column, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :ci_builds, INDEX_NAME
end
파티션된 테이블에서 인덱스를 동기적으로 생성#
# in db/post_migrate/
include Gitlab::Database::PartitioningMigrationHelpers
PARTITIONED_INDEX_NAME = 'index_p_ci_builds_on_some_column'
disable_ddl_transaction!
def up
add_concurrent_partitioned_index :p_ci_builds, :some_column, name: PARTITIONED_INDEX_NAME
end
def down
remove_concurrent_partitioned_index_by_name :p_ci_builds, PARTITIONED_INDEX_NAME
end
로컬에서 데이터베이스 인덱스 변경 테스트#
머지 리퀘스트를 생성하기 전에 데이터베이스 인덱스 변경 사항을 로컬에서 테스트해야 합니다.
비동기적으로 생성된 인덱스 확인#
로컬 환경에서 비동기 인덱스 헬퍼를 사용하여 인덱스 생성 변경 사항을 테스트합니다:
- Rails 콘솔에서
Feature.enable(:database_async_index_creation)과Feature.enable(:database_reindexing)을 실행하여 기능 플래그를 활성화합니다. bundle exec rails db:migrate를 실행하여postgres_async_indexes테이블에 항목을 생성합니다.bundle exec rails gitlab:db:execute_async_index_operations:all을 실행하여 모든 데이터베이스에서 인덱스가 비동기적으로 생성되도록 합니다.- 인덱스를 확인하려면 GDK 명령
gdk psql로 PostgreSQL 콘솔을 열고\d <index_name>명령을 실행하여 새로 생성된 인덱스가 존재하는지 확인합니다.- 파티션에 생성된 인덱스의 경우, 해당 테이블에 고유한 이름이 자동 생성되었는지
\d gitlab_partitions_dynamic.<table_name>으로 확인합니다.
- 파티션에 생성된 인덱스의 경우, 해당 테이블에 고유한 이름이 자동 생성되었는지
비동기적으로 인덱스 삭제#
매우 큰 테이블에서는 인덱스 삭제 관리가 어려울 수 있습니다.
remove_concurrent_index는 일반 트래픽을 차단하지 않는 방식으로 인덱스를
제거하지만, 인덱스 삭제가 여러 시간 동안 실행되면 여전히 문제가
될 수 있습니다. autovacuum과 같은 필요한 데이터베이스 작업이 실행될 수 없으며,
GitLab.com에서는 인덱스 삭제가 완료될 때까지 배포
프로세스가 차단됩니다.
GitLab.com에 대한 영향을 제한하려면, 주말 시간 동안 다음 프로세스를 사용하여 인덱스를 비동기적으로 제거합니다. 일반적으로 트래픽이 낮고 배포가 적기 때문에, 인덱스 삭제를 더 낮은 위험 수준에서 진행할 수 있습니다.
제거할 인덱스 예약#
- 비동기 삭제를 위해 인덱스를 준비하는 배포 후 마이그레이션을 포함하는 머지 리퀘스트를 생성합니다.
- 후속 이슈를 생성합니다. 인덱스를 동기적으로 삭제하는 마이그레이션을 추가하는 이슈입니다.
- 비동기 인덱스 제거를 준비하는 머지 리퀘스트에 후속 이슈를 언급하는 주석을 추가합니다.
예를 들어, 비동기 인덱스 헬퍼를 사용하여 인덱스를 삭제하는 방법입니다:
# in db/post_migrate/
INDEX_NAME = 'index_ci_builds_on_some_column'
# TODO: Index to be destroyed synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX
def up
prepare_async_index_removal :ci_builds, :some_column, name: INDEX_NAME
end
def down
unprepare_async_index :ci_builds, :some_column, name: INDEX_NAME
end
이 마이그레이션은 인덱스 이름과 정의를 postgres_async_indexes
테이블에 입력합니다. 주말에 실행되는 프로세스가 이 테이블에서 인덱스를 가져와
제거를 시도합니다.
머지 리퀘스트를 생성하기 전에 데이터베이스 인덱스 변경 사항을 로컬에서 테스트해야 합니다. 테스트 결과를 머지 리퀘스트 설명에 포함합니다.
MR이 배포되었고 프로덕션에서 인덱스가 더 이상 존재하지 않는지 확인#
- ChatOps에서
/chatops gitlab run auto_deploy status <merge_sha>를 사용하여 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인합니다. 출력이db/gprd를 반환하면, 배포 후 마이그레이션이 프로덕션 데이터베이스에서 실행된 것입니다. 자세한 내용은 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인하는 방법을 참고합니다. - 비동기적으로 제거된 인덱스의 경우, 인덱스가 주말에 제거될 수 있도록 다음 주까지 기다립니다.
- Database Lab을 사용하여 제거가 성공했는지 확인합니다. Database Lab은 제거된 인덱스를 찾으려고 할 때 오류를 보고해야 합니다. 그렇지 않다면 인덱스가 아직 존재할 수 있습니다.
인덱스를 동기적으로 삭제하는 마이그레이션 추가#
프로덕션 데이터베이스에 인덱스가 더 이상 존재하지 않는 것을 확인한 후, 인덱스를
동기적으로 제거하는 두 번째 머지 리퀘스트를 생성합니다. 스키마 변경 사항은
이 두 번째 머지 리퀘스트에서 structure.sql에 업데이트하고 커밋해야 합니다.
동기 마이그레이션은 GitLab.com에서는 no-op이 되지만, 다른 설치본에서는
예상대로 마이그레이션을 추가해야 합니다. 예를 들어, 앞의 비동기
예시에 대한 두 번째 마이그레이션을 만드는 방법은 다음과 같습니다:
remove_concurrent_index_by_name으로 두 번째 마이그레이션을 머지하기 전에, 프로덕션에 인덱스가 더 이상 존재하지 않는지 확인합니다.
두 번째 마이그레이션이 인덱스가 삭제되기 전에 배포되면,
두 번째 마이그레이션이 실행될 때 인덱스가 동기적으로 삭제됩니다.
# in db/post_migrate/
INDEX_NAME = 'index_ci_builds_on_some_column'
disable_ddl_transaction!
def up
remove_concurrent_index_by_name :ci_builds, name: INDEX_NAME
end
def down
add_concurrent_index :ci_builds, :some_column, name: INDEX_NAME
end
비동기적으로 제거된 인덱스 확인#
인덱스 제거 변경 사항을 테스트하려면, 로컬 환경에서 비동기 인덱스 헬퍼를 사용합니다:
- Rails 콘솔에서
Feature.enable(:database_reindexing)을 실행하여 기능 플래그를 활성화합니다. bundle exec rails db:migrate를 실행하면postgres_async_indexes테이블에 항목이 생성됩니다.bundle exec rails gitlab:db:reindex를 실행하여 인덱스를 비동기적으로 삭제합니다.- 인덱스를 확인하려면 GDK를 사용하여 PostgreSQL 콘솔을 열고
gdk psql명령과\d <index_name>을 실행하여 삭제된 인덱스가 더 이상 존재하지 않는지 확인합니다.