문자열과 텍스트 데이터 타입

문자열 또는 기타 텍스트 정보를 저장하기 위해 새로운 열을 추가할 때:

  1. 항상 string 데이터 타입 대신 text 데이터 타입을 사용합니다.
  2. text 열은 항상 제한을 설정해야 하며, 테이블을 생성할 때 create_table과 함께 #text ... limit: 100 헬퍼를 사용하거나, 기존 테이블을 변경할 때 add_text_limit을 사용합니다.

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

배경 정보

항상 string 대신 text를 사용하고자 하는 이유는 string 열은 제한을 업데이트하려면 ALTER TABLE ... 명령을 실행해야 하는 단점이 있기 때문입니다.

제한이 추가되는 동안 ALTER TABLE ... 명령은 테이블에 대한 EXCLUSIVE LOCK을 요구하며, 열을 업데이트하고 모든 기존 레코드를 검증하는 과정 동안 이 잠금은 유지됩니다. 이 과정은 대형 테이블의 경우 시간이 걸릴 수 있습니다.

반면에, 텍스트는 PostgreSQL에서 문자열과 거의 동등한 유형으로, 기존 열에 제한을 추가하거나 제한을 업데이트하는 것이 검증 단계에서 매우 비용이 많이 드는 EXCLUSIVE LOCK을 유지할 필요가 없다는 추가 이점을 가지고 있습니다.

제약 조건을 업데이트할 때 유효 옵션을 꺼서 시작할 수 있으며, 이는 열 선언을 업데이트하는 데만 EXCLUSIVE LOCK이 필요합니다. 이후에 VALIDATE CONSTRAINT를 사용하여 검증할 수 있으며, 이는 SHARE UPDATE EXCLUSIVE LOCK만 요구합니다(다른 검증 및 인덱스 생성과만 충돌하며 읽기 및 쓰기를 허용합니다).

참고: attr_encrypted 속성에 대해서는 텍스트 열을 사용하지 마세요. 대신 :binary을 사용하세요.

텍스트 열이 있는 새 테이블 생성

새 테이블을 추가할 때 모든 텍스트 열의 제한은 테이블 생성과 동일한 마이그레이션에서 추가되어야 합니다. 우리는 Rails의 #text 메소드에 limit: 속성을 추가하여 이 열에 대한 제한을 추가할 수 있도록 합니다.

예를 들어, 두 개의 텍스트 열을 가진 테이블을 생성하는 마이그레이션은 db/migrate/20200401000001_create_db_guides.rb에서 다음과 같습니다:

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

기존 테이블에 텍스트 열 추가

기존 테이블에 열을 추가하려면 해당 테이블에 대한 독점 잠금이 필요합니다. 비록 그 잠금이 짧은 시간 동안 유지되지만, add_column이 실행을 완료하는 데 걸리는 시간은 테이블에 대한 접근 빈도에 따라 달라질 수 있습니다. 예를 들어, 매우 자주 접근되는 테이블에 대해 독점 잠금을 획득하는 데는 GitLab.com에서 수분이 걸릴 수 있으며, with_lock_retries를 사용할 필요가 있습니다.

텍스트 제한을 추가할 때는 disable_ddl_transaction!로 트랜잭션을 비활성화해야 합니다. 이는 마이그레이션이 이후에 실패할 경우 열 추가가 롤백되지 않음을 의미합니다. 마이그레이션을 다시 실행하려고 하면 이미 존재하는 열 때문에 오류가 발생합니다.

따라서 기존 테이블에 텍스트 열을 추가하는 방법은 다음 중 하나입니다:

별도의 마이그레이션에서 열 및 제한 추가

sprints 테이블에 새로운 텍스트 열 extended_title을 추가하는 마이그레이션을 고려해보세요.

db/migrate/20200501000001_add_extended_title_to_sprints.rb:

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]

  # rubocop:disable Migration/AddLimitToTextColumns
  # 제한은 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 테이블에 새로운 텍스트 열 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

기존 열에 텍스트 제한 제약 조건 추가

기존 데이터베이스 열에 텍스트 제한을 추가하려면 여러 단계를 거쳐 최소 두 개의 릴리즈로 나누어야 합니다:

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

    • validate: false와 함께 텍스트 열에 제한을 추가하는 배포 후 마이그레이션을 추가합니다.
    • 기존 레코드를 수정하기 위한 배포 후 마이그레이션을 추가합니다.

      참고: 테이블의 크기에 따라 다음 릴리스에서 정리를 위한 백그라운드 마이그레이션이 필요할 수 있습니다. 자세한 내용은 대형 테이블의 텍스트 제한 제약 조건을 참조하세요.

    • 텍스트 제한을 검증하기 위한 다음 마일스톤에 대한 이슈를 만듭니다.
  2. 릴리스 N.M+1 (다음 릴리스)

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

예시

지정된 릴리스 마일스톤에 대해 issues.title_html1024 제한을 추가하고 싶다고 가정해 보겠습니다.

예를 들어 13.0과 같은 릴리스 마일스톤에 대해 말입니다.

Issues 테이블은 2,500만 개 이상의 행을 가진 매우 분주하고 큰 테이블이므로 업데이트를 실행하는 동안 다른 모든 프로세스를 잠그고 싶지 않습니다.

또한, 우리의 프로덕션 데이터베이스를 확인한 결과, 제목이 1024자 제한보다 많은 문자를 포함하는 issues가 있다는 것을 알고 있으므로 하나의 단계에서 제약 조건을 추가하고 검증할 수 없습니다.

참고: 주어진 제한보다 큰 제목을 가진 레코드가 없었다고 하더라도, 다른 GitLab 인스턴스가 그런 레코드를 가질 수 있으므로 우리는 어떤 경우든 동일한 프로세스를 따를 것입니다.

새로운 잘못된 레코드 방지 (현재 릴리스)

우리는 먼저 테이블에 NOT VALID 체크 제약 조건으로 제한을 추가하여 새로운 레코드가 삽입되거나 기존 레코드가 업데이트될 때 일관성을 보장합니다.

위의 예에서, 제목이 1024자가 넘는 기존 문제는 영향을 받지 않으며, 여전히 issues 테이블의 레코드를 업데이트할 수 있습니다. 그러나 1024자를 초과하는 제목으로 title_html을 업데이트하려고 하면, 제약 조건이 데이터베이스 오류를 발생시킵니다.

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

여전히 우리의 예에서 13.0 마일스톤(현재)에서, 다음과 같은 유효성 검사가 모델 Issue에 추가되었다고 가정합니다:

validates :title_html, length: { maximum: 1024 }

우리는 또한 13.0 마일스톤에서 validate: false를 사용하여 텍스트 제한을 추가함으로써 데이터베이스를 업데이트할 수 있습니다.

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: Danger of failing if there are records with length(maintainer_note) > 255
  end
end

대형 테이블의 텍스트 제한 제약

정말로 대형 테이블 (예: ci_buildsartifacts)에서 텍스트 열을 정리해야 하는 경우, 백그라운드 마이그레이션이 한동안 진행되고 데이터 마이그레이션을 추가한 이후 릴리스에서 추가적인 배치된 백그라운드 마이그레이션 정리가 필요합니다.

이러한 드문 경우에는 3개의 릴리스가 엔드 투 엔드로 필요합니다:

  1. 릴리스 N.M - 텍스트 제한 및 기존 레코드를 수정하기 위한 백그라운드 마이그레이션 추가.
  2. 릴리스 N.M+1 - 백그라운드 마이그레이션 정리.
  3. 릴리스 N.M+2 - 텍스트 제한 검증.