InfoGrab DocsInfoGrab Docs

데이터베이스 인덱스 추가

요약

인덱스는 데이터베이스 쿼리 속도를 높이는 데 사용할 수 있지만, 새 인덱스는 언제 추가해야 할까요? 여기서는 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_valuetrue 또는 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은 자식 인덱스 (즉, 해당 파티션의 해당 인덱스)가 독립적으로 제거되는 것을 허용하지 않습니다.

사용 가능한 미사용 인덱스 찾기#

제거 후보인 인덱스를 확인하려면 다음 쿼리를 실행할 수 있습니다:

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에서 \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에서 인덱스 사용 데이터를 볼 수 있습니다.

최소 지난 6개월 동안 관련 인덱스로 필터링된 메트릭 pg_stat_user_indexes_idx_scan을 쿼리하세요. 아래 쿼리는 결합된 모든 데이터베이스 인스턴스의 인덱스 사용률을 보여줍니다.

sum by (indexrelname) (rate(pg_stat_user_indexes_idx_scan{env="gprd", relname=~"", indexrelname=~""}[30d]))

파티션된 테이블의 경우, 부모를 삭제하기 전에 모든 자식 인덱스가 사용되지 않는지 확인해야 합니다.

데이터가 인덱스의 사용이 없거나 미미하다는 것을 보여주는 경우 제거의 강력한 후보입니다. 그러나 이는 GitLab.com의 사용으로만 제한된다는 점을 명심하세요. GitLab Self-Managed 인스턴스에서 안전하게 제거할 수 있도록 관련된 모든 쿼리를 조사해야 합니다.

사용량이 낮은 인덱스는 다른 기존 인덱스가 해당 인덱스를 사용하는 쿼리를 충분히 지원할 수 있음을 확인할 수 있다면 여전히 삭제할 수 있습니다. PostgreSQL은 데이터 분포 통계를 기반으로 사용할 인덱스를 결정하므로, 두 인덱스 모두 쿼리를 적절하게 지원하더라도 특정 상황에서 하나를 약간 선호할 수 있으며, 이것이 가끔 사용의 원인이 될 수 있습니다.

관련 쿼리 조사#

다음은 인덱스를 활용할 수 있는 모든 쿼리를 찾는 방법입니다. 쿼리가 실행되거나 실행될 수 있는 컨텍스트를 이해하여 인덱스가 다음 중 하나에 해당하는지 확인하는 것이 중요합니다:

  • GitLab.com이나 GitLab Self-Managed에 해당 인덱스에 의존하는 쿼리가 없는 경우.

  • 다른 기존 인덱스로 충분히 지원될 수 있는 경우.

인덱스의 출처를 조사하세요.

인덱스를 도입한 커밋 이력, 관련 머지 리퀘스트, 이슈를 살펴보세요.

  • 다음과 같은 질문에 대한 답을 찾으세요:

인덱스가 처음에 왜 추가되었나요? 어떤 쿼리를 지원하기 위한 것이었나요?

  • 해당 쿼리가 아직 존재하고 실행되는가?

  • GitLab Self-Managed 인스턴스에만 해당되는가?

rspec:merge-auto-explain-logs CI job 실행에서 출력된 쿼리를 검토하세요.

이 job은 테스트를 통해 실행된 쿼리를 수집하고 분석합니다. 출력은 아티팩트로 저장됩니다: auto_explain/auto_explain.ndjson.gz

  • 항상 100% 테스트 커버리지를 확보하지는 않으므로 이 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 테이블을 생각해 보세요:

  • 단일 칼럼 인덱스: index_ssh_signatures_on_commit_sha (commit_sha에 대해)

  • 복합 인덱스: index_ssh_signatures_on_project_id_and_commit_sha ((project_id, commit_sha)에 대해)

복합 인덱스 (project_id, commit_sha)는 다음을 효율적으로 지원할 수 있습니다:

project_idcommit_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의 다중 칼럼 인덱스 문서를 참조하세요.

사용되지 않는다고 생각하는 인덱스를 삭제하는 경우 인덱스 사용 통계를 확인하세요.

인덱스 이름 요구사항#

복잡한 정의를 가진 인덱스는 마이그레이션 메서드의 암묵적인 명명 동작에 의존하지 않고 명시적으로 이름을 지정해야 합니다. 즉, 다음 옵션 중 하나 이상으로 생성된 인덱스에 대해 반드시 명시적인 이름 인수를 제공해야 합니다:

  • where

  • using

  • order

  • length

  • type

  • opclass

인덱스 이름에 대한 고려 사항#

제약 조건 이름 규칙 페이지를 확인하세요.

명시적 이름이 필요한 이유#

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 13.9 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_indexprepare_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-repoomnibus에 설정되어 있습니다.

주말 이후에도 인덱스가 생성되지 않으면 최근 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이 배포되었고 프로덕션에 인덱스가 더 이상 존재하지 않는지 확인#

인덱스를 동기적으로 삭제하는 마이그레이션 추가#

프로덕션 데이터베이스에 인덱스가 더 이상 존재하지 않는 것이 확인된 후, 인덱스를 동기적으로 제거하는 두 번째 머지 리퀘스트를 생성하세요. 스키마 변경사항은 이 두 번째 머지 리퀘스트에서 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 명령 gdk psql을 사용하여 PostgreSQL 콘솔을 열고 \d <index_name>을 실행하여 삭제된 인덱스가 더 이상 존재하지 않는지 확인하세요.

데이터베이스 인덱스 추가

GitLab v19.1
원문 보기
요약

인덱스는 데이터베이스 쿼리 속도를 높이는 데 사용할 수 있지만, 새 인덱스는 언제 추가해야 할까요? 여기서는 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_valuetrue 또는 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은 자식 인덱스 (즉, 해당 파티션의 해당 인덱스)가 독립적으로 제거되는 것을 허용하지 않습니다.

사용 가능한 미사용 인덱스 찾기#

제거 후보인 인덱스를 확인하려면 다음 쿼리를 실행할 수 있습니다:

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에서 \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에서 인덱스 사용 데이터를 볼 수 있습니다.

최소 지난 6개월 동안 관련 인덱스로 필터링된 메트릭 pg_stat_user_indexes_idx_scan을 쿼리하세요. 아래 쿼리는 결합된 모든 데이터베이스 인스턴스의 인덱스 사용률을 보여줍니다.

sum by (indexrelname) (rate(pg_stat_user_indexes_idx_scan{env="gprd", relname=~"", indexrelname=~""}[30d]))

파티션된 테이블의 경우, 부모를 삭제하기 전에 모든 자식 인덱스가 사용되지 않는지 확인해야 합니다.

데이터가 인덱스의 사용이 없거나 미미하다는 것을 보여주는 경우 제거의 강력한 후보입니다. 그러나 이는 GitLab.com의 사용으로만 제한된다는 점을 명심하세요. GitLab Self-Managed 인스턴스에서 안전하게 제거할 수 있도록 관련된 모든 쿼리를 조사해야 합니다.

사용량이 낮은 인덱스는 다른 기존 인덱스가 해당 인덱스를 사용하는 쿼리를 충분히 지원할 수 있음을 확인할 수 있다면 여전히 삭제할 수 있습니다. PostgreSQL은 데이터 분포 통계를 기반으로 사용할 인덱스를 결정하므로, 두 인덱스 모두 쿼리를 적절하게 지원하더라도 특정 상황에서 하나를 약간 선호할 수 있으며, 이것이 가끔 사용의 원인이 될 수 있습니다.

관련 쿼리 조사#

다음은 인덱스를 활용할 수 있는 모든 쿼리를 찾는 방법입니다. 쿼리가 실행되거나 실행될 수 있는 컨텍스트를 이해하여 인덱스가 다음 중 하나에 해당하는지 확인하는 것이 중요합니다:

  • GitLab.com이나 GitLab Self-Managed에 해당 인덱스에 의존하는 쿼리가 없는 경우.

  • 다른 기존 인덱스로 충분히 지원될 수 있는 경우.

인덱스의 출처를 조사하세요.

인덱스를 도입한 커밋 이력, 관련 머지 리퀘스트, 이슈를 살펴보세요.

  • 다음과 같은 질문에 대한 답을 찾으세요:

인덱스가 처음에 왜 추가되었나요? 어떤 쿼리를 지원하기 위한 것이었나요?

  • 해당 쿼리가 아직 존재하고 실행되는가?

  • GitLab Self-Managed 인스턴스에만 해당되는가?

rspec:merge-auto-explain-logs CI job 실행에서 출력된 쿼리를 검토하세요.

이 job은 테스트를 통해 실행된 쿼리를 수집하고 분석합니다. 출력은 아티팩트로 저장됩니다: auto_explain/auto_explain.ndjson.gz

  • 항상 100% 테스트 커버리지를 확보하지는 않으므로 이 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 테이블을 생각해 보세요:

  • 단일 칼럼 인덱스: index_ssh_signatures_on_commit_sha (commit_sha에 대해)

  • 복합 인덱스: index_ssh_signatures_on_project_id_and_commit_sha ((project_id, commit_sha)에 대해)

복합 인덱스 (project_id, commit_sha)는 다음을 효율적으로 지원할 수 있습니다:

project_idcommit_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의 다중 칼럼 인덱스 문서를 참조하세요.

사용되지 않는다고 생각하는 인덱스를 삭제하는 경우 인덱스 사용 통계를 확인하세요.

인덱스 이름 요구사항#

복잡한 정의를 가진 인덱스는 마이그레이션 메서드의 암묵적인 명명 동작에 의존하지 않고 명시적으로 이름을 지정해야 합니다. 즉, 다음 옵션 중 하나 이상으로 생성된 인덱스에 대해 반드시 명시적인 이름 인수를 제공해야 합니다:

  • where

  • using

  • order

  • length

  • type

  • opclass

인덱스 이름에 대한 고려 사항#

제약 조건 이름 규칙 페이지를 확인하세요.

명시적 이름이 필요한 이유#

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 13.9 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_indexprepare_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-repoomnibus에 설정되어 있습니다.

주말 이후에도 인덱스가 생성되지 않으면 최근 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이 배포되었고 프로덕션에 인덱스가 더 이상 존재하지 않는지 확인#

인덱스를 동기적으로 삭제하는 마이그레이션 추가#

프로덕션 데이터베이스에 인덱스가 더 이상 존재하지 않는 것이 확인된 후, 인덱스를 동기적으로 제거하는 두 번째 머지 리퀘스트를 생성하세요. 스키마 변경사항은 이 두 번째 머지 리퀘스트에서 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 명령 gdk psql을 사용하여 PostgreSQL 콘솔을 열고 \d <index_name>을 실행하여 삭제된 인덱스가 더 이상 존재하지 않는지 확인하세요.