데이터베이스 인덱스 추가

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

SELECT *
FROM projects
WHERE user_id = 2;

여기서 ‘user_id’ 열을 필터링하고 있으므로 개발자는 이 열에 대해 인덱스를 만들기로 결정할 수 있습니다.

일부 경우에는 위와 같은 방식으로 열에 대한 인덱스를 만드는 것이 합리적일 수 있지만, 실제로는 부정적인 영향을 미칠 수도 있습니다. 테이블에 데이터를 쓸 때마다 기존의 인덱스도 업데이트해야 합니다. 인덱스가 많을수록 이 작업이 느려질 수 있습니다. 또한 인덱스는 색인화된 데이터 양과 인덱스 유형에 따라 디스크 공간을 상당히 차지할 수 있습니다. 예를 들어, PostgreSQL은 일반적인 B-트리 인덱스로 색인화할 수 없는 특정 데이터 유형에 대해 GIN 인덱스를 제공합니다. 그러나 이러한 인덱스는 일반적으로 더 많은 데이터를 차지하고 B-트리 인덱스에 비해 업데이트 속도가 느립니다.

이 모든 이유로 새로운 인덱스를 추가할 때 다음 사항을 고려하는 것이 중요합니다.

  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)도 있습니다.

가능한 경우 인덱스 추가 후 애플리케이션 코드 변경

불필요한 인덱스를 만드는 위험을 최소화하기 위해 가능한 경우 다음을 동일한 MR에서 수행하십시오.

  • 애플리케이션 코드 변경하기.
  • 인덱스 만들거나 제거하기.

인덱스를 생성하는 마이그레이션은 보통 짧고, MR의 크기를 크게 늘리지 않습니다. 이렇게 하면 백엔드 및 데이터베이스 리뷰어가 MR 또는 커밋 간의 컨텍스트를 전환하지 않고 보다 효율적으로 검토할 수 있습니다.

사용할 마이그레이션 유형

권위 있는 안내서는 마이그레이션 스타일 가이드입니다. 의문이 들 경우, 가이드를 참고하십시오.

다음은 간단한 참고용으로 일부 일반적인 시나리오와 권장되는 선택 사항입니다.

기존 쿼리 개선을 위한 인덱스 추가

배포 후 마이그레이션을 사용하십시오. 기존 쿼리가 추가된 인덱스 없이도 이미 작동되며 응용 프로그램 운영에 반드시 필요하지 않습니다.

인덱싱이 마침내 10분 미만으로 끝나는 데 시간이 오래 걸린다면, 비동기적으로 색인화를 고려하십시오.

새로운 또는 업데이트된 쿼리 지원을 위한 인덱스 추가

새로운 또는 업데이트된 쿼리의 쿼리 계획을 반드시 확인하십시오. 먼저, 해당 쿼리가 특정 인덱스 없이 권장된 쿼리 시간을 크게 초과하지 않거나 시간 초과되지 않는지 확인하십시오.

쿼리가 시간 초과되지 않거나 권장된 쿼리 시간을 크게 초과하지 않는 경우:

  • 새로운 쿼리의 성능을 향상시키기 위해 추가된 어떤 인덱스든 응용 프로그램 운영에 결정적이지 않습니다.
  • 인덱스를 생성하기 위해 배포 후 마이그레이션을 사용하십시오.
  • 동일한 MR에서 생성 및 사용되는 새로운 쿼리의 애플리케이션 코드 변경을 쉽게 배포하십시오.

쿼리가 시간 초과되거나 권장된 쿼리 시간을 초과하는 경우에는 GitLab.com만 해당되는지 또는 모든 GitLab 인스턴스에서 발생하는지에 따라 다른 조치가 필요합니다. 대부분의 기능은 큰 GitLab 설치 중 하나인 GitLab.com에서만 전용 인덱스가 필요합니다.

GitLab.com에서 새로운 또는 업데이트된 쿼리가 느리게 실행됩니다

해당하는 인덱스를 포함하는 후 배포 마이그레이션과 애플리케이션 코드 변경을 위한 두 개의 MR을 사용하십시오.

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

참고: 기능 플래그를 사용할 수 있다면 코드 변경을 피처 플래그 뒤에서 수행하는 단일 MR을 사용할 수 있습니다. 동시에 후 배포 마이그레이션을 포함하십시오. 후 배포 마이그레이션이 완전히 실행된 후 플래그를 활성화할 수 있습니다.

GitLab.com에서는 CI를 통해 단일 릴리스에서 후 배포 마이그레이션을 실행합니다.

  • 시간 t에는 커밋된 MR 그룹이 준비되어 배포할 준비가 됩니다.
  • t+1에는 해당 MR 그룹의 정규 마이그레이션이 GitLab.com의 스테이징 및 프로덕션 데이터베이스에서 실행됩니다.
  • t+2에는 해당 MR 그룹의 애플리케이션 코드 변경이 롤링 방식으로 배포됩니다.

애플리케이션 코드 변경이 완전히 배포된 후, 릴리스 매니저는 GitLab.com의 가용성을 기다린 후 하루에 한 번 후 배포 마이그레이션을 실행할 수 있습니다. 이유로 인해, 두 번째 MR을 병합하기 전에 첫 번째 MR에 포함된 후 배포 마이그레이션이 실행되었는지 확인해야 합니다.


위와 같이 과학적인 근거에 의한 데이터베이스 성능 최적화에 대한 가이드입니다. GitLab에서의 디스크 용량, 쿼리 시간, 인덱스 유지 관리 등을 고려하여 데이터베이스 성능을 향상시키는 데 유용한 정보가 담겨 있습니다.

대규모 GitLab 인스턴스에서는 새로 생성되거나 업데이트된 쿼리의 성능이 느릴 수 있습니다.

자체 관리형 인스턴스에서는 쿼리 성능을 직접 확인할 수 없습니다. PostgreSQL은 데이터 분포를 기반으로 실행 계획을 생성하기 때문에 쿼리 성능을 추정하는 것은 어려운 작업입니다.

자체 관리형 인스턴스의 쿼리 성능에 대해 걱정되고 자체 관리형 인스턴스에 인덱스를 추가해야 한다고 판단할 경우 다음 권장 사항을 따르세요.

  • 제로 다운타임 업그레이드를 지원하는 자체 관리형 인스턴스의 경우, 애플리케이션 코드 배포 후 업그레이드를 수행할 때 포스트-배포 마이그레이션이 실행됩니다.
  • 제로 다운타임 업그레이드를 지원하지 않는 자체 관리형 인스턴스의 경우, 관리자는 정기적인 마이그레이션을 실행한 후 원하는 시간에 릴리스 후 포스트-배포 마이그레이션을 실행할 수 있습니다. 애플리케이션 코드는 업그레이드 시 배포됩니다.

따라서 애플리케이션은 포스트-배포 마이그레이션에서 적용된 데이터베이스 스키마를 동일 릴리스에 적용된 것으로 가정해서는 안 됩니다. 애플리케이션 코드는 동일 릴리스에서 포스트-배포 마이그레이션에서 추가된 인덱스 없이 계속 작동해야 합니다.

인덱스를 생성하는 데 걸리는 시간에 따라 다음과 같이 두 가지 옵션이 있습니다: 인덱스 생성 시 걸리는 시간에 따른 옵션입니다.

  1. 단일 릴리스: 정기적인 마이그레이션이 필요한 인덱스를 아주 빨리 생성할 수 있는 경우 (일반적으로 테이블이 새로운 경우 또는 매우 작은 경우) 정기적인 마이그레이션에서 인덱스를 생성하고 동일한 MR(병합 요청) 및 마일스톤에 애플리케이션 코드 변경을 배포할 수 있습니다.

  2. 적어도 두 개의 릴리스: 필요한 인덱스를 생성하는 데 시간이 걸리는 경우 해당 인덱스를 한 릴리스에서 포스트-배포 마이그레이션에서 생성한 후 애플리케이션 코드 변경을 기다려야 합니다. 이 변경은 해당 인덱스에 의존하는 경우 다음 릴리스에서 수행됩니다.

기존 테이블에 제약 조건으로 작용하는 고유 인덱스 추가

PostgreSQL의 고유 인덱스는 제약 조건으로 작용합니다. 기존 테이블에 이를 추가하는 것은 까다로울 수 있습니다.

GitLab.com 및 자체 관리형 인스턴스에서 테이블이 절대적으로 작다고 보장할 수 없는 경우, 다음과 같은 여러 릴리스에 걸친 포스트-배포 마이그레이션을 사용해야 합니다.

  • 중복 레코드를 제거하거나 수정합니다.
  • 기존 열을 제약하는 고유 인덱스를 도입합니다.

기존 제약과는 달리 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 헬퍼는 populated 테이블에 인덱스를 생성하기 위한 요구사항입니다. 트랜잭션 마이그레이션 내부에서 사용할 수 없기 때문에, 이미 인덱스가 존재하는지 검사하는 내장된 확인을 갖고 있습니다. 일치하는 항목을 찾으면 인덱스 생성이 건너뛰어집니다. 명시적인 이름 인자가 없으면, 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
    # 작업 없음
  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를 명시적으로 실행합니다.

인덱스 생성 마이그레이션을 분석하도록 촉발하기 위해, 테이블을 분석하는 인덱스 생성 마이그레이션을 업데이트하세요:

# 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는 다음을 수행합니다:

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

Rails 마이그레이션 예제:

# 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를 사용하여 GitLab.com에서 게시 후 마이그레이션이 실행되었는지 확인하세요. 커맨드를 통해 /chatops run auto_deploy status <merge_sha>를 실행합니다. 결과가 db/gprd를 반환하면 게시 후 마이그레이션이 프로덕션 데이터베이스에서 실행된 것입니다. 자세한 정보는 GitLab.com에서 게시 후 마이그레이션이 실행되었는지 확인하는 방법을 참조하세요.
  2. 비동기적으로 생성된 인덱스의 경우, 인덱스가 다음 주말에 생성되도록 기다리세요.
  3. Database Lab을 사용하여 생성이 성공적으로 이루어졌는지 확인하세요. 결과에서 인덱스가 invalid로 나오지 않는지 확인하세요.

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

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

경고: 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

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

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

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

인덱스를 생성하는 변경 사항을 테스트하기 위해 로컬 환경에서 비동기식 인덱스 도우미를 사용해야합니다.

  1. 레일즈 콘솔에서 Feature.enable(:database_async_index_creation)Feature.enable(:database_reindexing)를 실행하여 기능 플래그를 활성화합니다.
  2. bundle exec rails db:migrate를 실행하여 postgres_async_indexes 테이블에 항목을 생성하세요.
  3. bundle exec rails gitlab:db:execute_async_index_operations:all를 실행하여 모든 데이터베이스에 대해 인덱스가 비동기식으로 생성되도록 처리합니다.
  4. 인덱스를 확인하려면 PostgreSQL 콘솔을 열고 GDK 명령 gdk psql을 실행한 후 명령 \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에서 배포 후 마이그레이션이 실행되었는지 ChatOps를 통해 확인하세요. 결과가 db/gprd로 반환되면 생산 데이터베이스에서 배포 후 마이그레이션이 실행되었습니다. 자세한 정보는 GitLab.com에서 배포 후 마이그레이션이 실행되었는지 확인하는 방법을 확인하세요.
  2. 인덱스가 비동기적으로 제거됨을 위한 경우 다음 주말까지 기다려서 인덱스를 제거합니다.
  3. 제거가 성공적으로 진행되었는지 확인하려면 Database Lab를 사용하세요. 제거된 인덱스를 찾으려고 시도할 때 Database Lab이 오류를 보고해야 합니다. 그렇지 않으면 인덱스가 아직 존재할 수 있습니다.

동기적으로 인덱스를 제거하기 위한 마이그레이션 추가

생산 데이터베이스에 인덱스가 더 이상 존재하지 않음을 확인한 후, 인덱스를 동기적으로 제거하는 두 번째 병합 요청을 만드세요. 스키마 변경 사항은 두 번째 병합 요청의 structure.sql에 업데이트되어야 합니다. 동기적 마이그레이션은 GitLab.com에서 noop 결과를 가져옵니다. 그러나 다른 설치에 대비하여 여전히 마이그레이션을 추가해야 합니다. 예를 들어, 이전 비동기 예제를 위한 두 번째 마이그레이션을 생성하려면:

경고: 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. 레일스 콘솔에서 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>을 실행하여 제거된 인덱스가 더 이상 존재하지 않는지 확인합니다.