문자열 및 텍스트 데이터 유형

문자열이나 기타 텍스트 정보를 저장하는 새 열을 추가할 때:

  1. 문자열 데이터 유형 대신에 항상 text 데이터 유형을 사용합니다.
  2. text 열은 항상 create_table에서 #text ... limit: 100 헬퍼(아래 참조)를 사용하여 테이블을 만들 때 또는 기존 테이블을 변경할 때 add_text_limit를 사용하여 꼭 제한을 설정해야 합니다.

기본 Rails text 열 유형은 제한을 정의할 수 없지만, 우리는 create_table을 확장하여 limit: 255 옵션을 추가합니다. create_table 외부에서는 add_text_limit를 사용하여 이미 존재하는 열에 체크 제약 조건을 추가할 수 있습니다.

배경 정보

우리가 항상 문자열 대신 text를 사용하고 싶어하는 이유는 문자열 열은 제한을 업데이트하기 원할 경우 ALTER TABLE ... 명령을 실행해야 한다는 단점이 있기 때문입니다.

제한이 추가되는 중 ALTER TABLE ... 명령은 테이블에 EXCLUSIVE LOCK을 요구하며, 이는 열을 업데이트하고 모든 기존 레코드를 유효성 검사하는 과정 동안 유지되어야 합니다. 이 프로세스는 대형 테이블의 경우 시간이 걸릴 수 있습니다.

반면에 PostgreSQL에서 text는 거의 문자열과 동등하면서, 기존 열에 제한을 추가하거나 제한을 업데이트하는 것은 매우 비용이 많이 드는 EXCLUSIVE LOCK이 필요하지 않다는 추가적인 이점이 있습니다. 우리는 유효한 옵션을 끈다면 제약을 업데이트하기 위해서는 EXCLUSIVE LOCK이 필요하지만, 이는 열의 선언을 업데이트하는 것에 대해서만 적용되고 있다. 그런 후 VALIDATE CONSTRAINT를 사용하여 나중에 유효성을 검증할 수 있습니다(SHARE UPDATE EXCLUSIVE LOCK만 필요하며 다른 유효성과 인덱스 생성과 충돌하지만 읽고 쓰기는 가능합니다).

note
attr_encrypted 속성에 대해 text 열을 사용하지 마세요. 대신 :binary을 사용하세요.

text 열을 가진 새로운 테이블 만들기

새로운 테이블을 추가할 때, text 열의 제한은 테이블 생성과 동일한 마이그레이션에 추가되어야 합니다. 이 열에 제한을 추가할 수 있도록 Rails의 #text 메서드에 limit: 속성을 추가합니다.

예를 들어, db/migrate/20200401000001_create_db_guides.rb에서 두 개의 text 열, titlenotes가 있는 테이블을 생성하는 마이그레이션을 고려해보세요:

class CreateDbGuides < Gitlab::Database::Migration[2.1]
  def change
    create_table :db_guides do |t|
      t.bigint :stars, default: 0, null: false
      t.text :title, limit: 128
      t.text :notes, limit: 1024
    end
  end
end

기존 테이블에 text 열 추가

기존 테이블에 열을 추가하려면 해당 테이블에 대해 독점적 잠금이 필요합니다. 이 잠금이 잠시동안 유지되지만, add_column이 실행되는 시간은 테이블에 얼마나 자주 액세스되는지에 따라 달라질 수 있습니다. 예를 들어, 매우 자주 액세스되는 테이블에 대해 독점적 잠금을 획들하는 것은 GitLab.com에서 몇 분이 걸릴 수 있으며 with_lock_retries를 사용해야 합니다.

text 제한을 추가할 때, 트랜잭션이 disable_ddl_transaction!로 비활성화되어야 합니다. 이는 마이그레이션이 나중에 실패해도 해당 열이 롤백되지 않는다는 것을 의미합니다. 마이그레이션을 다시 실행하려고 시도하면 이미 존재하는 열 때문에 오류가 발생합니다.

이러한 이유로 기존 테이블에 text 열을 추가하는 것은 다음 중 하나로 수행될 수 있습니다:

별도의 마이그레이션에 열과 제한 추가하기

테이블 sprints에 새로운 text 열 extended_title을 추가하는 마이그레이션을 고려해보세요, db/migrate/20200501000001_add_extended_title_to_sprints.rb:

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]
  
  # rubocop:disable Migration/AddLimitToTextColumns
  # limit is added in 20200501000002_add_text_limit_to_sprints_extended_title
  def change
    add_column :sprints, :extended_title, :text
  end
  # rubocop:enable Migration/AddLimitToTextColumns
end

두 번째 마이그레이션은 extended_title에 제한을 추가합니다, db/migrate/20200501000002_add_text_limit_to_sprints_extended_title.rb:

class AddTextLimitToSprintsExtendedTitle < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!
  
  def up
    add_text_limit :sprints, :extended_title, 512
  end
  
  def down
    # 다운이 필요한 이유는 `add_text_limit`가 되돌릴 수 없기 때문입니다.
    remove_text_limit :sprints, :extended_title
  end
end

열과 제한을 확인하여 하나의 마이그레이션에 열과 제한 추가하기

테이블 sprints에 새로운 text 열 extended_title을 추가하는 마이그레이션을 고려해보세요, db/migrate/20200501000001_add_extended_title_to_sprints.rb:

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!
  
  def up
    with_lock_retries do
      add_column :sprints, :extended_title, :text, if_not_exists: true
    end
    
    add_text_limit :sprints, :extended_title, 512
  end
  
  def down
    with_lock_retries do
      remove_column :sprints, :extended_title, if_exists: true
    end
  end
end

기존 열에 text 제한 제약 추가

기존 데이터베이스 열에 text 제한을 추가하는 것은 최소 두 가지 다른 릴리스로 나뉘어져 여러 단계로 진행해야 합니다:

  1. 릴리스 N.M(현재 릴리스)

    • validate: false를 사용하여 텍스트 열에 제한을 추가하는 사후 배포 마이그레이션 실행.
    • 기존 레코드를 수정하기 위한 사후 배포 마이그레이션 실행.

      note
      테이블의 크기에 따라 다음 릴리스에서 정리를 위한 백그라운드 마이그레이션이 필요할 수 있습니다. 자세한 내용은 대규모 테이블의 텍스트 제한 제약을 참조하십시오.
    • 텍스트 제한을 검증하기 위해 다음 마일스톤에 대한 이슈를 생성합니다.
  2. 릴리스 N.M+1(다음 릴리스)

    • 사후 배포 마이그레이션을 사용하여 텍스트 제한을 검증합니다.

예제

특정 릴리스 마일스톤(예: 13.0)에 issues.title_html1024 제한을 추가하려고 한다고 가정해 보겠습니다.

Issues는 2500만 개 이상의 레코드를 가진 크고 바쁜 테이블이므로 이를 실행하는 동안 이 테이블에 액세스하려는 다른 프로세스를 모두 잠그고 싶지 않습니다.

또한, 우리의 프로덕션 데이터베이스를 확인한 후에 issues에서 title의 문자 길이가 1024보다 큰 레코드가 있는 것을 알게 되어 전체적인 목적으로 이 제약을 하나의 단계로 추가하고 유효성을 검증할 수 없습니다.

note
제공된 제한보다 더 많은 문자가 포함된 제목과 같은 레코드가 없더라도, 다른 GitLab 인스턴스에는 그러한 레코드가 있을 수 있으므로 두 경우 모두 같은 프로세스를 따르게 됩니다.

새로운 유효하지 않은 레코드를 방지하기(현재 릴리스)

우리는 먼저 NOT VALID 체크 제약 조건으로 제한을 추가하여, 새 레코드가 추가되는 경우 또는 기존 레코드가 업데이트되는 경우에 일관성을 강제합니다.

위의 예시에서 1024 글자보다 더 많은 제목이 있는 기존 이슈는 영향을 받지 않고, 여전히 issues 테이블의 레코드를 업데이트할 수 있습니다. 그러나 title_html을 1024글자보다 많은 타이틀로 업데이트하려고 하면 제약이 데이터베이스 오류를 발생시킵니다.

기존 속성에 제약을 추가하거나 제거하려면 모든 애플리케이션 변경 사항이 먼저 배포되어야 하며, 그렇지 않으면 여전히 이전 버전의 애플리케이션 서버에서는 유효하지 않은 값으로 속성을 업데이트하려고 할 수 있습니다고 합니다. 이러한 이유로 add_text_limit는 사후 배포 마이그레이션에서 실행해야 합니다.

이전 예제에서 현재 릴리스인 13.0 마일스톤을 위해 모델 Issue에 다음과 같은 유효성 검사가 추가되었다고 가정해 봅시다:

validates :title_html, length: { maximum: 1024 }

또한 동일한 마일스톤에서 validate: false로 text 제한을 추가하는 사후 배포 마이그레이션을 통해 데이터베이스를 업데이트할 수 있습니다, db/post_migrate/20200501000001_add_text_limit_migration.rb:

class AddTextLimitMigration < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!
  
  def up
    # 이렇게 하면 유효성을 검증없이 제약을 추가합니다
    add_text_limit :issues, :title_html, 1024, validate: false
  end
  
  def down
    # 다운이 필요한 이유는 `add_text_limit`가 되돌릴 수 없기 때문입니다.
    remove_text_limit :issues, :title_html
  end
end

현재 릴리스의 기존 레코드를 수정하기 위한 데이터 이민

이 접근 방식은 데이터 양과 정리 전략에 따라 다릅니다. GitLab.com에서 수정해야 하는 레코드 수는 후속 이민을 사용할지 뒷받침 데이터 이민을 사용할지 결정하는 데 도움이 되는 좋은 지표입니다.

  • 데이터 양이 1,000 건 미만이면 데이터 이민은 후속 이민 내에서 실행될 수 있습니다.
  • 데이터 양이 1,000 건 이상이면 백그라운드 데이터 이민을 생성하는 것이 좋습니다.

어떤 옵션을 사용할지 확신이 안 들 때는 데이터베이스 팀에 문의하십시오.

예를 들어, 이슈 테이블은 상당히 크고 빈번히 액세스되므로 현재 13.0 마일스톤에 대해 백그라운드 이민을 추가할 것입니다. db/post_migrate/20200501000002_schedule_cap_title_length_on_issues.rb:

class ScheduleCapTitleLengthOnIssues < Gitlab::Database::Migration[2.1]
  # GitLab.com에서 영향을 받는 레코드 수 등에 대한 정보
  # 각 배치당 평균 실행 시간 등...
  BATCH_SIZE = 5000
  DELAY_INTERVAL = 2.minutes.to_i
  
  # 백그라운드 이민은 제목이 1024 글자 제한을 초과하는 이슈를 업데이트할 것입니다.
  ISSUES_BACKGROUND_MIGRATION = 'CapTitleLengthOnIssues'.freeze
  
  disable_ddl_transaction!
  
  def up
    queue_batched_background_migration(
      ISSUES_BACKGROUND_MIGRATION,
      :issues,
      :id,
      job_interval: DELAY_INTERVAL,
      batch_size: BATCH_SIZE
    )
  end
  
  def down
    delete_batched_background_migration(ISSUES_BACKGROUND_MIGRATION, :issues, :id, [])
  end
end

이 안내서를 간결하게 유지하기 위해 백그라운드 이민의 정의는 생략하고 후속 이민을 예약하는 데 사용되는 포스트-디플로이먼트 이민의 고수준 예제만 제공했습니다. 배치된 백그라운드 이민 가이드에서 자세한 정보를 찾을 수 있습니다.

텍스트 제한 검증 (다음 릴리스)

텍스트 제한 검증은 전체 테이블을 스캔하고 각 레코드가 올바른지 확인합니다.

여전히 예시로, 다음인 13.1 마일스톤에서 validate_text_limit 이민 헬퍼를 최종 포스트-디플로이먼트 이민에서 실행할 것입니다. db/post_migrate/20200601000001_validate_text_limit_migration.rb:

class ValidateTextLimitMigration < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!
  
  def up
    validate_text_limit :issues, :title_html
  end
  
  def down
    # no-op
  end
end

기존 열의 텍스트 제한 제약 조건 증가

기존 데이터베이스 열의 텍스트 제한을 높이는 것은 먼저 새로운 제한(다른 이름으로)을 추가한 후 이전 제한을 삭제함으로써 안전하게 수행할 수 있습니다.

class ChangeMaintainerNoteLimitInCiRunner < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!
  
  def up
    add_text_limit :ci_runners, :maintainer_note, 1024, constraint_name: check_constraint_name(:ci_runners, :maintainer_note, 'max_length_1K')
    remove_text_limit :ci_runners, :maintainer_note, constraint_name: check_constraint_name(:ci_runners, :maintainer_note, 'max_length')
  end
  
  def down
    # no-op: 레코드의 길이(maintainer_note)가 255보다 큰 경우 실패 가능성 있음
  end
end

대규모 테이블의 텍스트 제한 제약 조건

실제로 매우 크기가 큰 테이블(예: ci_buildsartifacts)의 텍스트 열을 정리해야 하는 경우, 백그라운드 이민이 길어지고 추가적인 배치된 백그라운드 이민 정리이 필요한 경우 데이터 이민을 추가한 후에 릴리스를 통해 이어지는 3 단계가 필요합니다:

  1. N.M 릴리스 - 텍스트 제한과 기존 레코드를 수정하기 위한 백그라운드 이민을 추가합니다.
  2. N.M+1 릴리스 - 백그라운드 이민을 정리합니다.
  3. N.M+2 릴리스 - 텍스트 제한을 검증합니다.