데이터베이스 인덱스 추가하기

인덱스는 데이터베이스 쿼리를 가속화하는 데 사용될 수 있지만, 언제 새로운 인덱스를 추가해야 할까요? 전통적으로 이 질문에 대한 답은 데이터 필터링 또는 조인에 사용되는 모든 열에 대해 인덱스를 추가하는 것이었습니다. 예를 들어, 다음 쿼리를 고려하십시오:

SELECT *
FROM projects
WHERE user_id = 2;

여기서 우리는 user_id 열로 필터링하고 있으며, 개발자는 이 열에 인덱스를 생성하기로 결정할 수 있습니다.

특정 경우에는 위의 접근 방식으로 열에 인덱스를 추가하는 것이 의미가 있을 수 있으나, 실제로는 부정적인 영향을 미칠 수 있습니다. 테이블에 데이터를 작성할 때마다 기존 인덱스도 업데이트해야 합니다. 인덱스가 많을수록 이 과정이 느려질 수 있습니다. 인덱스는 인덱싱된 데이터 양과 인덱스 유형에 따라 상당한 디스크 공간을 차지할 수도 있습니다. 예를 들어, PostgreSQL은 일반 B-tree 인덱스로 인덱싱할 수 없는 특정 데이터 유형을 인덱싱하는 데 사용할 수 있는 GIN 인덱스를 제공합니다. 그러나 이러한 인덱스는 일반적으로 더 많은 데이터를 차지하며 B-tree 인덱스보다 업데이트하는 속도가 느립니다.

따라서 새로운 인덱스를 추가할 때 다음과 같은 고려 사항이 중요합니다:

  1. 새로운 쿼리가 가능한 한 많은 기존 인덱스를 재사용합니까?

  2. 인덱스를 사용하는 것이 테이블의 행을 반복하는 것보다 빠른 만큼의 데이터가 충분합니까?

  3. 인덱스를 유지하는 오버헤드가 쿼리 시간 단축의 가치가 있습니까?

어떤 상황에서는 인덱스가 필요하지 않을 수 있습니다:

  • 테이블이 작고(1,000개 이하의 레코드) 크기가 기하급수적으로 성장할 것으로 예상되지 않을 때.

  • 기존 인덱스가 충분한 행을 필터링할 때.

  • 인덱스 추가 후 쿼리 시간 단축이 유의미하지 않을 때.

또한, 넓은 인덱스는 쿼리의 모든 필터 기준에 맞출 필요가 없습니다. 우리는 인덱스 조회가 충분히 낮은 선택성을 갖도록 충분한 열을 커버하기만 하면 됩니다.

쿼리 재사용하기

첫 번째 단계는 쿼리가 가능한 한 많은 기존 인덱스를 재사용하도록 하는 것입니다. 예를 들어, 다음 쿼리를 고려하십시오:

SELECT *
FROM todos
WHERE user_id = 123
AND state = 'open';

이제 user_id 열에는 인덱스가 있지만 state 열에는 인덱스가 없다고 가정해 보겠습니다. 한 사람은 state가 인덱스가 없기 때문에 이 쿼리가 성능이 나쁘다고 생각할 수 있습니다. 그러나 실제로는 user_id에 대한 인덱스가 충분한 행을 필터링할 수 있기 때문에 쿼리가 잘 수행될 수도 있습니다.

인덱스가 재사용되는지 확인하는 가장 좋은 방법은 EXPLAIN ANALYZE를 사용하여 쿼리를 실행하는 것입니다. 조인된 테이블과 필터링에 사용되는 열에 따라 추가 인덱스가 큰 차이를 만들지 않을 수 있습니다.

요약하자면:

  1. 가능한 한 많은 기존 인덱스를 재사용하도록 쿼리를 작성해 보세요.

  2. EXPLAIN ANALYZE를 사용하여 쿼리를 실행하고 출력 결과를 연구하여 가장 이상적인 쿼리를 파악하세요.

데이터 크기

데이터베이스는 일반 시퀀스 스캔(모든 행 반복)이 더 빠른 경우에도 인덱스를 사용하지 않을 수 있습니다, 특히 작은 테이블의 경우.

테이블이 성장할 것으로 예상된다면 인덱스를 추가하는 것을 고려하세요, 그리고 쿼리가 많은 행을 필터링해야 할 경우 인덱스를 사용하는 것이 좋습니다.

테이블 크기가 작거나(<1,000 레코드) 기존 인덱스가 이미 충분한 행을 필터링하고 있다면 인덱스를 추가하고 싶지 않을 수 있습니다.

유지 관리 오버헤드

인덱스는 모든 테이블 쓰기 시 업데이트되어야 합니다. PostgreSQL의 경우, 데이터를 테이블에 작성할 때 모든 기존 인덱스가 업데이트됩니다. 결과적으로, 동일한 테이블에 많은 인덱스가 있을 경우 쓰기가 느려집니다. 따라서 쿼리 성능과 추가 인덱스를 유지하는 오버헤드 간의 균형을 맞추는 것이 중요합니다.

인덱스를 추가하는 것이 SELECT 시간을 5밀리초 단축시키지만 INSERT/UPDATE/DELETE 시간을 10밀리초 증가시킨다면, 이 경우 새로운 인덱스는 그리 가치가 없을 수 있습니다. 새로운 인덱스는 SELECT 시간이 감소하고 INSERT/UPDATE/DELETE 시간이 영향을 받지 않을 때 더 가치가 있습니다.

일부 테이블에 더 이상 인덱스가 없어야 합니다

우리는 자주 접근되는 선택된 테이블에 대한 추가 인덱스 생성을 방지하기 위해 RuboCop 체크(PreventIndexCreation)를 수행합니다.
이는 LockManager LWLock contention 때문입니다.

같은 이유로, 이러한 테이블에 새로운 열을 추가하는 것에 대해서도 RuboCop 체크(AddColumnsToWideTables)가 수행됩니다.

가능하면 인덱스를 추가하고 애플리케이션 코드 변경을 함께 수행하세요

불필요한 인덱스 생성을 최소화하기 위해 가능하면 같은 병합 요청에서 다음을 수행하세요:

  • 애플리케이션 코드 변경을 수행합니다.
  • 인덱스를 생성하거나 제거합니다.

인덱스를 생성하는 마이그레이션은 일반적으로 짧고, 병합 요청의 크기를 크게 증가시키지 않습니다.
이렇게 하면 백엔드 및 데이터베이스 리뷰어가 병합 요청이나 커밋 간의 컨텍스트 전환 없이 더 효율적으로 검토할 수 있습니다.

사용할 마이그레이션 유형

권위 있는 가이드는 마이그레이션 스타일 가이드입니다.
의문이 있을 경우, 가이드를 참조하세요.

여기에서 몇 가지 일반적인 시나리오에 대한 추천 선택을 빠른 참조로 제공합니다.

기존 쿼리를 개선하기 위해 인덱스를 추가합니다

배포 후 마이그레이션을 사용하세요.
기존 쿼리는 이미 추가 인덱스 없이 작동하며, 애플리케이션 운영에 치명적이지 않습니다.

인덱스를 생성하는 데 시간이 오래 걸리는 경우
(배포 후 마이그레이션은 10분 이내에 끝나야 합니다),
비동기적으로 인덱싱하는 것을 고려하세요.

새로운 쿼리 또는 업데이트된 쿼리를 지원하기 위해 인덱스를 추가합니다

새로운 쿼리 또는 업데이트된 쿼리에 대한 쿼리 계획을 항상 검토하세요. 먼저, 전용 인덱스 없이
타임아웃이 발생하지 않거나 권장 쿼리 시간을 크게 초과하지 않는지 확인합니다.

쿼리가 타임아웃이 발생하지 않거나 쿼리 시간을 초과하지 않는 경우:

  • 새로운 쿼리의 성능을 개선하기 위해 추가된 인덱스는 애플리케이션 운영에 치명적이지 않습니다.
  • 배포 후 마이그레이션을 사용하여 인덱스를 생성합니다.
  • 같은 병합 요청에서 새로운 쿼리를 생성하고 사용하는 애플리케이션 코드 변경을 함께 배포합니다.

쿼리가 타임아웃되거나 쿼리 시간을 초과하는 경우에는,
그것이 GitLab.com에서만 발생하는지 아니면 모든 GitLab 인스턴스에서 발생하는지에 따라
다른 조치가 필요합니다. 대부분의 기능은 GitLab.com에 대해서만 전용 인덱스가 필요하며, 이는 가장 큰 GitLab 설치 중 하나입니다.

GitLab.com에서 새로운 쿼리 또는 업데이트된 쿼리가 느리게 수행됨

배포 후 마이그레이션에서 인덱스를 생성하고 애플리케이션 코드 변경을 수행하기 위해 두 개의 MR을 사용합니다:

  • 첫 번째 MR은 배포 후 마이그레이션을 사용하여 인덱스를 생성합니다.
  • 두 번째 MR은 애플리케이션 코드 변경을 수행합니다. 이는 첫 번째 MR의
    배포 후 마이그레이션이 GitLab.com에서 실행된 후에만 머지되어야 합니다.

참고:
기능 플래그를 사용할 수 있는 경우, 기능 플래그 뒤에서 코드 변경을 수행하는 단일 MR을 사용할 수 있습니다.
배포 후 마이그레이션을 동시에 포함하세요. 배포 후 마이그레이션이 실행된 후, 기능 플래그를 활성화할 수 있습니다.

GitLab.com에서는 지속적인 통합을 통해 단일 릴리스 전반에 걸쳐 배포 후 마이그레이션을 실행합니다:

  • 특정 시간 t에, 병합 요청 그룹이 병합되고 배포 준비가 완료됩니다.
  • t+1에, 그룹의 일반 마이그레이션이 GitLab.com의 스테이징 및 프로덕션 데이터베이스에서 실행됩니다.
  • t+2에, 그룹의 애플리케이션 코드 변경이 롤링 방식으로 배포되기 시작합니다.

애플리케이션 코드 변경이 완전히 배포된 후,
릴리즈 관리자는 나중에 자신의 재량에 따라 배포 후 마이그레이션을 실행할 수 있습니다.
배포 후 마이그레이션은 GitLab.com의 가용성을 고려하여 매일 한 번 실행됩니다.
이 때문에 첫 번째 MR에 포함된 배포 후 마이그레이션이 실행되었는지에 대한 확인이 필요하며,
두 번째 MR을 머지하기 전에 실행되어야 합니다.

대규모 GitLab 인스턴스에서의 새 또는 업데이트된 쿼리는 느릴 수 있습니다

자체 관리 인스턴스에서 쿼리 성능을 직접 확인하는 것은 불가능합니다.

PostgreSQL은 데이터 분포에 따라 실행 계획을 생성하므로

쿼리 성능을 추정하는 것은 어려운 작업입니다.

자체 관리 인스턴스에서 쿼리 성능이 우려되는 경우

자체 관리 인스턴스에 인덱스가 있어야 한다고 결정했다면

다음 권장 사항을 따르세요:

  • 제로 다운타임 업그레이드를 따르는 자체 관리 인스턴스의 경우,

    애플리케이션 코드 배포 후 업그레이드 작업을 수행할 때

    배포 후 마이그레이션이 실행됩니다.

  • 제로 다운타임 업그레이드를 따르지 않는 자체 관리 인스턴스의 경우,

    관리자가 일반 마이그레이션이 실행된 후에

    선택한 시간에 릴리스를 위한 배포 후 마이그레이션을 실행하도록 선택할 수 있습니다.

    그들이 업그레이드할 때 애플리케이션 코드가 배포됩니다.

이러한 이유로 인해 애플리케이션은

배포 후 마이그레이션에 의해 적용된 데이터베이스 스키마가

동일한 릴리스에 포함되었다고 가정해서는 안 됩니다.

애플리케이션 코드는

동일한 릴리스에서 배포 후 마이그레이션에서 추가된 인덱스 없이도

계속 작동해야 합니다.

인덱스를 생성하는 데 소요되는 시간에 따라

두 가지 옵션이 있습니다:

  1. 단일 릴리스: 일반 마이그레이션이 필요한 인덱스를 매우 빠르게 생성할 수 있다면

    (대개 테이블이 새롭거나 매우 작기 때문에)

    일반 마이그레이션에서 인덱스를 생성하고

    동일한 MR 및 마일스톤에서 애플리케이션 코드 변경을 배포할 수 있습니다.

  2. 최소 두 개의 릴리스: 필요한 인덱스 생성에 시간이 걸리는 경우

    하나의 릴리스에서 PDM에서 인덱스를 생성한 다음

    다음 릴리스에서 인덱스에 의존하는 애플리케이션 코드 변경을 수행해야 합니다.

기존 테이블에 제약 조건 역할을 하는 고유 인덱스 추가

PostgreSQL의 고유 인덱스는 제약 조건 역할을 합니다.

기존 테이블에 이를 추가하는 것은 까다로울 수 있습니다.

GitLab.com 및 자체 관리 인스턴스에서

테이블이 절대적으로 작다고 보장되지 않는 한

여러 릴리스에 걸쳐 여러 배포 후 마이그레이션을 사용해야 합니다:

  • 중복 레코드를 제거하고(또는) 수정합니다.

  • 기존 열을 제약하는 고유 인덱스를 도입합니다.

기존 열에 NOT NULL 제약 조건을 추가하는 섹션에 설명된

다단계 접근 방식을 참조하세요.

PostgreSQL의 고유 인덱스는 일반 제약 조건과 달리

비검증 상태로 도입될 수 없습니다.

원하는 고유성을 보장하기 위해

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/current/monitoring-stats.html에서 확인할 수 있습니다.

프로덕션에서 인덱스가 여전히 사용되고 있는지 확인하려면

Grafana:

sum by (type)(rate(pg_stat_user_indexes_idx_scan{env="gprd", indexrelname="INSERT INDEX NAME HERE"}[30d]))

쿼리 출력은 데이터베이스의 실제 사용에 따라 달라지므로

다음과 같은 요인에 영향을 받을 수 있습니다:

  • 특정 쿼리가 실행되지 않아 특정 인덱스를 사용할 수 없는 경우.

  • 특정 테이블에 데이터가 적어 PostgreSQL이 인덱스 스캔 대신 시퀀스 스캔을 사용하는 경우.

이 데이터는

자주 사용되는 데이터베이스에 대해 신뢰할 수 있으며

가능한 한 많은 GitLab 기능을 사용하는 경우에만 유효합니다.

인덱스 이름 지정 요구사항

복잡한 정의를 가진 인덱스는 마이그레이션 메서드의 암시적 이름 지정 방식에 의존하지 않고 명시적으로 이름을 지정해야 합니다. 간단히 말해, 다음 옵션 중 하나 이상을 사용하여 생성된 인덱스에 대해 명시적 이름 인수를 제공해야 합니다:

  • 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
    # 스키마 불일치로 인해 인덱스는 조건부로 생성되어야 합니다.
    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와 같은 동시 인덱스 헬퍼는 이미 내부적으로 존재 여부 검사를 수행합니다.

임시 인덱스

인덱스가 일시적으로만 필요한 경우가 있을 수 있습니다.

예를 들어, 마이그레이션에서 테이블의 열이 조건부로 업데이트될 수 있습니다. 쿼리 성능 지침에서 업데이트해야 할 열을 쿼리하기 위해 그렇지 않으면 사용되지 않을 인덱스가 필요합니다.

이러한 경우, 임시 인덱스를 고려하세요. 임시 인덱스를 지정하려면:

  1. 인덱스 이름을 tmp_로 접두사하고 이름 지정 규칙을 준수하세요.
  2. 다음(또는 미래의) 마일스톤에서 인덱스를 제거하기 위한 후속 이슈를 생성하세요.
  3. 제거 이슈를 언급하는 주석을 마이그레이션에 추가하세요.

임시 마이그레이션은 다음과 같을 것입니다:

INDEX_NAME = 'tmp_index_projects_on_owner_where_emails_disabled'

def up
  # 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

배치 백그라운드 마이그레이션 전에 새로운 인덱스 분석하기

때때로 배치 백그라운드 마이그레이션을 지원하기 위해 인덱스를 추가해야 할 필요가 있습니다.

이는 일반적으로 두 개의 배포 후 마이그레이션을 생성하여 수행됩니다:

  1. 새로운 인덱스를 추가합니다. 일반적으로 임시 인덱스입니다.
  2. 배치 백그라운드 마이그레이션을 큐에 추가합니다.

대부분의 경우 추가 작업이 필요하지 않습니다. 새로운 인덱스는 생성되고 배치 백그라운드 마이그레이션을 큐에 추가하고 실행할 때 기대한 대로 사용됩니다.

그러나 표현식 인덱스는 생성할 때 새로운 인덱스에 대한 통계를 생성하지 않습니다. Autovacuum이 결국 ANALYZE를 실행하고, 통계를 업데이트하여 새로운 인덱스가 사용될 수 있도록 합니다.

인덱스가 생성된 직후, 위에서 설명한 배경 마이그레이션 시나리오처럼 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 슬랙 채널에서 도움을 요청하세요.

파티션 테이블에 대한 인덱스

파티션 테이블에서는 동시 인덱스를 생성할 수 없습니다.

그러나 비 동시 인덱스를 생성하면 인덱스가 생성되는 테이블에 대해 쓰기 잠금을 유지합니다.

따라서 핫 시스템에서 서비스 중단을 피하기 위해 인덱스를 생성할 때 CONCURRENTLY를 사용해야 합니다.

우회 방법으로, 데이터베이스 팀은 add_concurrent_partitioned_index를 제공했습니다.

이 도우미는 쓰기 잠금을 유지하지 않고 파티션 테이블에 인덱스를 생성합니다.

내부적으로 add_concurrent_partitioned_index는:

  1. 각 파티션에 대해 CONCURRENTLY를 사용하여 인덱스를 생성합니다.
  2. 부모 테이블에 대한 인덱스를 생성합니다.

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에 미치는 영향을 제한하기 위해, 주말 시간 동안 비동기적으로 인덱스를 생성하는 프로세스가 존재합니다.

트래픽이 일반적으로 낮고 배포가 적은 시간대에 인덱스 생성을 진행할 수 있으므로 위험 수준이 낮습니다.

낮은 영향 시간에 인덱스 생성을 예약하기

  1. 인덱스를 생성하도록 예약하기.
  2. MR이 배포되었는지 확인하고 인덱스가 프로덕션에 존재하는지 확인하기.
  3. 인덱스를 동기적으로 생성하기 위한 마이그레이션 추가하기.

인덱스를 생성하도록 예약하기

  1. 비동기 생성 준비를 위한 배포 후 마이그레이션을 포함한 병합 요청을 만듭니다.

  2. 동기적으로 인덱스를 생성하기 위한 마이그레이션 추가하는 후속 이슈를 만듭니다.

  3. 비동기 인덱스를 준비하는 병합 요청에서 후속 이슈를 언급하는 댓글을 추가합니다.

아래 블록에서 비동기 인덱스 헬퍼를 사용하여 인덱스를 만드는 예제를 볼 수 있습니다. 이 마이그레이션은 인덱스 이름과 정의를 postgres_async_indexes 테이블에 입력합니다. 주말에 실행되는 프로세스는 이 테이블에서 인덱스를 가져오고 생성하려고 시도합니다.

# in db/post_migrate/

INDEX_NAME = 'index_ci_builds_on_some_column'

# TODO: 동기적으로 생성할 인덱스는 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: 동기적으로 생성할 파티셔닝된 인덱스는 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

참고: prepare_partitioned_async_index는 파티션에 대해 비동기적으로 인덱스를 생성합니다. 파티셔닝된 테이블에 파티션 인덱스를 연결하지는 않습니다. 다음 단계인 파티셔닝된 테이블에 대해 동기적으로 인덱스를 생성하기에서는 add_concurrent_partitioned_index가 인덱스를 동기적으로 추가할 뿐만 아니라 파티션 인덱스를 파티셔닝된 테이블에 연결합니다.

MR이 배포되었는지 확인하고 인덱스가 프로덕션에 존재하는지 확인하기

  1. /chatops run auto_deploy status <merge_sha>를 사용하여 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인합니다. 출력이 db/gprd를 반환하면, 배포 후 마이그레이션이 프로덕션 데이터베이스에서 실행된 것입니다. 더 많은 정보는 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인하는 방법을 참조하세요.

  2. 비동기적으로 생성된 인덱스의 경우, 인덱스가 주말 동안 생성될 수 있도록 다음 주까지 기다립니다.

  3. Database Lab를 사용하여 생성이 성공했는지 확인하기를 확인합니다. 출력에 인덱스가 invalid로 표시되지 않도록 합니다.

인덱스를 동기적으로 생성하는 마이그레이션 추가

생산 데이터베이스에서 인덱스가 존재하는 것을 확인한 후, 두 번째 병합 요청을 만들어 인덱스를 동기적으로 추가합니다. 이 두 번째 병합 요청에서는 스키마 변경 사항이 structure.sql에 업데이트되고 커밋되어야 합니다. 동기 마이그레이션은 GitLab.com에서 아무 작업도 수행하지 않지만, 다른 설치에 대한 예상대로 마이그레이션을 추가해야 합니다. 아래 블록에서는 이전 비동기 예제의 두 번째 마이그레이션을 만드는 방법을 보여줍니다.

경고:

인덱스가 생성되기 전에 add_concurrent_index를 사용하여 두 번째 마이그레이션을 병합하지 마세요. 두 번째 마이그레이션이 인덱스가 생성되기 전에 배포되면, 두 번째 마이그레이션이 실행될 때 인덱스가 동기적으로 생성됩니다.

# 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

파티션 테이블에 대해 인덱스를 동기적으로 생성

# 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

데이터베이스 인덱스 변경 사항을 로컬에서 테스트

병합 요청을 만들기 전에 데이터베이스 인덱스 변경 사항을 로컬에서 테스트해야 합니다.

비동기적으로 생성된 인덱스 확인

로컬 환경에서 비동기 인덱스 헬퍼를 사용하여 인덱스를 생성하기 위한 변경 사항을 테스트하세요:

  1. Rails 콘솔에서 Feature.enable(:database_async_index_creation)Feature.enable(:database_reindexing)를 실행하여 기능 플래그를 활성화합니다.

  2. bundle exec rails db:migrate를 실행하여 postgres_async_indexes 테이블에 항목을 생성합니다.

  1. bundle exec rails gitlab:db:execute_async_index_operations:all를 실행하여 모든 데이터베이스에서 비동기적으로 인덱스를 생성합니다.
  1. 인덱스를 확인하려면, GDK 명령어 gdk psql을 사용하여 PostgreSQL 콘솔을 열고, \d <index_name> 명령어를 실행하여 새로 생성된 인덱스가 존재하는지 확인합니다.

비동기적으로 인덱스 삭제

매우 큰 테이블의 경우, 인덱스 삭제를 관리하는 것이 어려울 수 있습니다. remove_concurrent_index는 일반적인 트래픽을 차단하지 않고 인덱스를 삭제하지만, 인덱스 삭제가 여러 시간 동안 지속되는 경우 여전히 문제가 될 수 있습니다. autovacuum과 같은 필요한 데이터베이스 작업을 실행할 수 없으며, GitLab.com에서 인덱스 삭제가 완료될 때까지 배포 프로세스가 차단됩니다.

GitLab.com에 미치는 영향을 줄이기 위해, 주말 시간 동안 비동기적으로 인덱스를 제거하는 다음 프로세스를 사용하세요. 일반적으로 트래픽이 적고 배포가 적기 때문에 인덱스 삭제는 낮은 위험 수준에서 진행될 수 있습니다.

  1. 인덱스를 제거하도록 예약.
  2. MR이 배포되었고 인덱스가 생산에 존재하는지 확인.
  3. 인덱스를 동기적으로 삭제하는 마이그레이션 추가.

인덱스 제거 예약

  1. 비동기 제거를 위한 인덱스를 준비하는 배포 후 마이그레이션을 포함하는 병합 요청을 생성합니다.
  2. 인덱스를 동기식으로 제거하는 마이그레이션을 추가하는 후속 이슈 생성합니다.
  3. 비동기 인덱스 제거를 준비하는 병합 요청에 후속 이슈를 언급하는 댓글을 추가합니다.

예를 들어, 비동기 인덱스 도우미를 사용하여 인덱스를 제거하려면:

# in db/post_migrate/

INDEX_NAME = 'index_ci_builds_on_some_column'

# TODO: 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이 배포되었고 인덱스가 프로덕션에서 더 이상 존재하지 않음을 확인하십시오

  1. /chatops run auto_deploy status <merge_sha>를 사용하여 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인합니다.

    결과가 db/gprd를 반환하면, 배포 후 마이그레이션이 프로덕션 데이터베이스에서 실행된 것입니다. 더 많은 정보는 GitLab.com에서 배포 후 마이그레이션 실행 여부 확인 방법을 참조하세요.

  2. 비동기적으로 제거된 인덱스의 경우,

    다음 주까지 기다려서 주말 동안 인덱스가 제거될 수 있게 합니다.

  3. 데이터베이스 랩 을 사용하여 제거가 성공했는지 확인합니다.

    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

인덱스 비동기 제거 확인

인덱스를 제거하는 변경사항을 테스트하기 위해 로컬 환경에서 비동기 인덱스 헬퍼를 사용하세요:

  1. Rails 콘솔에서 Feature.enable(:database_reindexing)를 실행하여 기능 플래그를 활성화합니다.
  2. bundle exec rails db:migrate를 실행하여 postgres_async_indexes 테이블에 항목을 생성합니다.
  3. bundle exec rails gitlab:db:reindex를 실행하여 인덱스를 비동기적으로 제거합니다.
  4. 인덱스를 확인하려면 GDK 명령어 gdk psql을 사용하여 PostgreSQL 콘솔을 열고 \d <index_name>을 실행하여 제거된 인덱스가 더 이상 존재하지 않음을 확인합니다.