InfoGrab DocsInfoGrab Docs

NOT NULL 제약 조건

요약

NULL 값을 가져서는 안 되는 모든 속성은 데이터베이스에서 NOT NULL 칼럼으로 정의해야 합니다. 애플리케이션 로직에 따라, NOT NULL 칼럼에는 모델에 presence 유효성 검사를 정의하거나, 데이터베이스 정의의 일부로 기본값을 지정해야 합니다.

NULL 값을 가져서는 안 되는 모든 속성은 데이터베이스에서 NOT NULL 칼럼으로 정의해야 합니다.

애플리케이션 로직에 따라, NOT NULL 칼럼에는 모델에 presence 유효성 검사를 정의하거나, 데이터베이스 정의의 일부로 기본값을 지정해야 합니다. 예를 들어, 후자는 항상 non-NULL 값을 가져야 하지만 애플리케이션이 매번 강제할 필요 없는 잘 정의된 기본값이 있는 boolean 속성에 해당할 수 있습니다 (예: active=true).

belongs_to 연관 관계의 일부인 외래 키 칼럼의 경우, 별도의 presence: true 유효성 검사 대신 연관 관계에서 optional: false를 사용하는 것이 좋습니다. 이 방법이 의미적으로 더 올바르며 Rails의 내장 연관 관계 유효성 검사를 활용합니다. GitLab은 config/application.rbconfig.active_record.belongs_to_required_by_default = false가 설정되어 있으므로, belongs_to 연관 관계는 기본적으로 선택 사항이며 명시적으로 필수로 표시해야 합니다.

NOT NULL 칼럼이 있는 새 테이블 생성#

새 테이블을 추가할 때, 모든 NOT NULL 칼럼은 create_table 내부에서 직접 정의해야 합니다.

예를 들어, 두 개의 NOT NULL 칼럼이 있는 테이블을 생성하는 마이그레이션 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.bigint :guide, null: false
    end
  end
end

기존 테이블에 NOT NULL 칼럼 추가#

GitLab에서 PostgreSQL 11이 최소 버전이 된 이후로, NULL 및/또는 기본값이 있는 칼럼 추가가 훨씬 쉬워졌으며, 모든 경우에 표준 add_column 헬퍼를 사용해야 합니다.

예를 들어, db_guides 테이블에 새로운 NOT NULL 칼럼 active를 추가하는 마이그레이션 db/migrate/20200501000001_add_active_to_db_guides.rb를 고려해 보세요:

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]
  def change
    add_column :db_guides, :active, :boolean, default: true, null: false
  end
end

기존 칼럼에 NOT NULL 제약 조건 추가#

기존 데이터베이스 칼럼에 NOT NULL을 추가하려면 일반적으로 최소 두 개의 다른 릴리즈에 걸쳐 여러 단계가 필요합니다. 백그라운드 마이그레이션을 사용할 필요가 없을 만큼 테이블이 충분히 작다면, 이 모든 단계를 동일한 머지 리퀘스트에 포함할 수 있습니다. 트랜잭션 지속 시간을 줄이기 위해 별도의 마이그레이션을 사용하는 것을 권장합니다.

필요한 단계는 다음과 같습니다:

  • 릴리즈 N.M (현재 릴리즈)

    애플리케이션 레벨에서 $ATTRIBUTE 값이 설정되고 있는지 확인합니다.

    속성에 기본값이 있는 경우, 새 레코드에 기본값이 설정되도록 모델에 기본값을 추가합니다.

    • 새 레코드와 기존 레코드 모두에 대해 속성이 nil로 설정되는 코드의 모든 위치를 업데이트합니다 (해당하는 경우). before_savebefore_validation과 같은 ActiveRecord 콜백을 사용하는 경우에도, 일부 프로세스는 이러한 콜백을 건너뛸 수 있으므로 충분하지 않을 수 있습니다. update_column, update_columns, 그리고 insert_allupdate_all과 같은 대량 작업은 주의해야 할 메서드의 몇 가지 예시입니다.
  • 기존 레코드를 수정하기 위한 배포 후 마이그레이션을 추가합니다.

    테이블 크기에 따라, 다음 릴리즈에서 정리를 위한 백그라운드 마이그레이션이 필요할 수 있습니다. 자세한 내용은 대규모 테이블의 NOT NULL 제약 조건 섹션을 참조하세요.

  • 릴리즈 N.M+1 (다음 릴리즈)

    GitLab.com의 모든 기존 레코드에 속성이 설정되어 있는지 확인합니다. 그렇지 않다면, 릴리즈 N.M의 1단계로 돌아갑니다.

    • 1단계가 정상적이고 릴리즈 N.M의 백필이 배치 백그라운드 마이그레이션을 통해 완료된 경우, 백그라운드 마이그레이션을 완료하는 배포 후 마이그레이션을 추가합니다.

    • 이제 모든 기존 및 새 레코드가 유효해야 하므로, nil 속성을 가진 레코드를 방지하기 위해 모델에 속성에 대한 유효성 검사를 추가합니다.

    • NOT NULL 제약 조건을 추가하는 배포 후 마이그레이션을 추가합니다.

예시#

특정 릴리즈 마일스톤(예: 13.0)을 고려해 보세요.

프로덕션 데이터베이스를 확인한 결과, NULL 설명을 가진 epics가 있으므로 하나의 단계에서 제약 조건을 추가하고 유효성 검사를 할 수 없습니다.

NULL 설명을 가진 에픽이 없더라도, 다른 GitLab 인스턴스에는 그러한 레코드가 있을 수 있으므로 어느 경우에나 동일한 프로세스를 따릅니다.

새로운 유효하지 않은 레코드 방지 (현재 릴리즈)#

속성이 nil로 설정되는 코드 경로가 있다면 모두 업데이트하여 새 레코드와 기존 레코드 모두에 대해 속성을 non-nil 값으로 설정합니다.

Rails attributes API를 사용한 기본값이 있는 속성이 epic.rb에 추가되어 새 레코드에 기본값이 설정됩니다:

class Epic < ApplicationRecord
  attribute :description, default: 'No description'
end

기존 레코드 수정을 위한 데이터 마이그레이션 (현재 릴리즈)#

여기서의 접근 방식은 데이터 볼륨과 정리 전략에 따라 다릅니다. GitLab.com에서 수정해야 하는 레코드 수는 배포 후 마이그레이션을 사용할지 백그라운드 데이터 마이그레이션을 사용할지 결정하는 데 도움이 되는 좋은 지표입니다:

  • 데이터 볼륨이 1000개 레코드 미만이면, 배포 후 마이그레이션 내에서 데이터 마이그레이션을 실행할 수 있습니다.

  • 데이터 볼륨이 1000개 레코드보다 많으면, 백그라운드 마이그레이션을 생성하는 것이 권장됩니다.

어떤 옵션을 사용할지 확신이 없다면, 데이터베이스 팀에 조언을 구하세요.

다시 예시로 돌아가면, epics 테이블은 상당히 크지 않고 자주 액세스되지도 않으므로, 13.0 마일스톤(현재)에 대해 배포 후 마이그레이션을 추가합니다. db/post_migrate/20200501000002_cleanup_epics_with_null_description.rb:

class CleanupEpicsWithNullDescription < Gitlab::Database::Migration[2.1]
  # With BATCH_SIZE=1000 and epics.count=29500 on GitLab.com
  # - 30 iterations will be run
  # - each requires on average ~150ms
  # Expected total run time: ~5 seconds
  BATCH_SIZE = 1000

  disable_ddl_transaction!

  class Epic < MigrationRecord
    include EachBatch

    self.table_name = 'epics'
  end

  def up
    Epic.each_batch(of: BATCH_SIZE) do |relation|
      relation.
        where('description IS NULL').
        update_all(description: 'No description')
    end
  end

  def down
    # no-op : can't go back to `NULL` without first dropping the `NOT NULL` constraint
  end
end

모든 레코드가 수정되었는지 확인 (다음 릴리즈)#

postgres.ai를 사용하여 프로덕션 데이터베이스의 씬 클론을 생성하고 GitLab.com의 모든 레코드에 속성이 설정되어 있는지 확인합니다. 그렇지 않다면 새로운 유효하지 않은 레코드 방지 단계로 돌아가서 코드에서 속성이 명시적으로 nil로 설정되는 위치를 파악합니다. 코드 경로를 수정한 후 기존 레코드를 수정하기 위한 마이그레이션을 다시 예약하고 다음 단계를 수행할 다음 릴리즈를 기다립니다.

백그라운드 마이그레이션 완료 (다음 릴리즈)#

백그라운드 마이그레이션을 사용하여 마이그레이션이 완료된 경우 마이그레이션을 완료합니다.

모델에 유효성 검사 추가 (다음 릴리즈)#

이제 모든 기존 및 새 레코드가 유효해야 하므로, nil 속성을 가진 레코드를 방지하기 위해 모델에 속성에 대한 유효성 검사를 추가합니다.

belongs_to 연관 관계의 일부인 외래 키 칼럼의 경우, optional: false를 사용하는 것이 좋습니다:

class Epic < ApplicationRecord
  belongs_to :group, optional: false
end

다음보다 선호됩니다:

class Epic < ApplicationRecord
  belongs_to :group
  validates :group, presence: true
end

일반 속성의 경우:

class Epic < ApplicationRecord
  validates :description, presence: true
end

NOT NULL 제약 조건 추가 (다음 릴리즈)#

NOT NULL 제약 조건을 추가하면 전체 테이블을 스캔하여 각 레코드가 올바른지 확인합니다.

계속 예시에서, 13.1 마일스톤(다음)에 대해 최종 배포 후 마이그레이션에서 add_not_null_constraint 마이그레이션 헬퍼를 실행합니다:

class AddNotNullConstraintToEpicsDescription < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    # This will add the `NOT NULL` constraint and validate it
    add_not_null_constraint :epics, :description
  end

  def down
    # Down is required as `add_not_null_constraint` is not reversible
    remove_not_null_constraint :epics, :description
  end
end

대규모 테이블의 NOT NULL 제약 조건#

트래픽이 많은 테이블 (예: ci_buildsartifacts)에 대해 nullable 칼럼을 정리해야 하는 경우, 백그라운드 마이그레이션이 상당 시간 동안 실행될 수 있으며 데이터 마이그레이션 추가 후 다음 릴리즈에서 추가적인 배치 백그라운드 마이그레이션 정리가 필요합니다.

이 경우 릴리즈 수는 기존 레코드를 마이그레이션하는 데 필요한 시간에 따라 다릅니다. 정리는 백그라운드 마이그레이션이 완료된 후 예약되며, 이는 제약 조건이 추가된 후 여러 릴리즈가 지난 후일 수 있습니다.

  • 릴리즈 N.M:

    기존 레코드를 수정하기 위한 백그라운드 마이그레이션을 추가합니다:

# db/post_migrate/
class QueueBackfillMergeRequestDiffsProjectId < Gitlab::Database::Migration[2.2]
  milestone '16.7'
  restrict_gitlab_migration gitlab_schema: :gitlab_main_org

  MIGRATION = 'BackfillMergeRequestDiffsProjectId'
  DELAY_INTERVAL = 2.minutes

  def up
    queue_batched_background_migration(
      MIGRATION,
      :merge_request_diffs,
      :id
    )
  end

  def down
    delete_batched_background_migration(MIGRATION, :merge_request_diffs, :id, [])
  end
end
# db/post_migrate/
class FinalizeMergeRequestDiffsProjectIdBackfill < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'
  restrict_gitlab_migration gitlab_schema: :gitlab_main_org

  MIGRATION = 'BackfillMergeRequestDiffsProjectId'

  def up
    ensure_batched_background_migration_is_finished(
      job_class_name: MIGRATION,
      table_name: :merge_request_diffs,
      column_name: :id,
      job_arguments: [],
      finalize: true
    )
  end

  def down
    # no-op
  end
end
  • 그런 다음 완료 후, NOT NULL 제약 조건을 추가합니다:
# db/post_migrate/
class AddMergeRequestDiffsProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'

  def up
    add_not_null_constraint :merge_request_diffs, :project_id
  end

  def down
    remove_not_null_constraint :merge_request_diffs, :project_id
  end
end
  • 선택 사항. 매우 큰 테이블의 경우, 유효하지 않은 NOT NULL 제약 조건을 추가하고 비동기 유효성 검사를 예약합니다:
# db/post_migrate/
class AddMergeRequestDiffsProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'

  def up
    add_not_null_constraint :merge_request_diffs, :project_id, validate: false
  end

  def down
    remove_not_null_constraint :merge_request_diffs, :project_id
  end
end
# db/post_migrate/
class PrepareMergeRequestDiffsProjectIdNotNullValidation < Gitlab::Database::Migration[2.2]
  milestone '16.10'

  CONSTRAINT_NAME = 'check_11c5f029ad'

  def up
    prepare_async_check_constraint_validation :merge_request_diffs, name: CONSTRAINT_NAME
  end

  def down
    unprepare_async_check_constraint_validation :merge_request_diffs, name: CONSTRAINT_NAME
  end
end
  • 선택 사항. 파티션 테이블의 경우 다음을 사용합니다:
# db/post_migrate/

PARTITIONED_TABLE_NAME = :p_ci_builds
CONSTRAINT_NAME = 'check_9aa9432137'

# Partitioned check constraint to be validated in https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX
def up
  prepare_partitioned_async_check_constraint_validation PARTITIONED_TABLE_NAME, name: CONSTRAINT_NAME
end

def down
  unprepare_partitioned_async_check_constraint_validation PARTITIONED_TABLE_NAME, name: CONSTRAINT_NAME
end

prepare_partitioned_async_check_constraint_validation은 모든 파티션에 대해 기존의 NOT VALID 체크 제약 조건만 비동기적으로 유효성 검사합니다. 파티션 테이블에 대한 체크 제약 조건을 생성하거나 유효성 검사하지 않습니다.

  • 선택 사항. 제약 조건이 비동기적으로 유효성 검사된 경우, 유효성 검사가 완료되면 NOT NULL 제약 조건을 유효성 검사합니다:

    Database Lab을 사용하여 유효성 검사가 성공했는지 확인합니다. \d+ table_name 명령을 실행하고 체크 제약 조건 정의에서 NOT VALID가 제거되었는지 확인합니다.

  • NOT NULL 제약 조건을 유효성 검사하는 마이그레이션을 추가합니다:

# db/post_migrate/
class ValidateMergeRequestDiffsProjectIdNullConstraint < Gitlab::Database::Migration[2.2]
  milestone '16.10'

  def up
    validate_not_null_constraint :merge_request_diffs, :project_id
  end

  def down
    # no-op
  end
end

이러한 경우, 업데이트 주기 초반에 데이터베이스 팀과 상의하세요. NOT NULL 제약 조건이 필요하지 않을 수도 있고, 정말 크거나 자주 액세스되는 테이블에 영향을 주지 않는 다른 옵션이 있을 수도 있습니다.

여러 칼럼에 대한 NOT NULL 제약 조건#

때로는 특정 수의 NOT NULL 값을 포함하는 칼럼 집합을 보장하고 싶을 때가 있습니다. 일반적인 예시는 프로젝트나 그룹 중 하나에 속할 수 있는 테이블로, 이 경우 project_id 또는 group_id가 반드시 존재해야 합니다. 이를 적용하려면 위의 사용 사례에 맞는 단계를 따르되, add_multi_column_not_null_constraint 헬퍼를 사용합니다.

이 예시에서, labels는 프로젝트나 그룹 중 하나에 속해야 하지만 둘 다에 속할 수는 없습니다. 이를 적용하기 위해 체크 제약 조건을 추가할 수 있습니다:

class AddLabelsNullConstraint < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'

  def up
    add_multi_column_not_null_constraint(:labels, :group_id, :project_id)
  end

  def down
    remove_multi_column_not_null_constraint(:labels, :group_id, :project_id)
  end
end

이렇게 하면 labels에 다음 제약 조건이 추가됩니다:

CREATE TABLE labels (
    ...
    CONSTRAINT check_45e873b2a8 CHECK ((num_nonnulls(group_id, project_id) = 1))
);

num_nonnulls는 제공된 인수 중 non-null인 수를 반환합니다. 제약 조건에서 이 값이 1과 같은지 확인하면 행에서 group_idproject_id 중 하나만 non-null 값을 포함해야 하고 둘 다 포함해서는 안 된다는 의미입니다.

커스텀 한도 및 연산자#

필요한 non-null 수를 사용자 정의하려면 다른 limit 및/또는 operator를 사용할 수 있습니다:

class AddLabelsNullConstraint < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'

  def up
    add_multi_column_not_null_constraint(:labels, :group_id, :project_id, limit: 0, operator: '>')
  end

  def down
    remove_multi_column_not_null_constraint(:labels, :group_id, :project_id)
  end
end

이는 project_idgroup_id 모두 존재할 수 있도록 허용하는 제약 조건에 반영됩니다:

CREATE TABLE labels (
    ...
    CONSTRAINT check_45e873b2a8 CHECK ((num_nonnulls(group_id, project_id) > 0))
);

기존 테이블의 칼럼에서 NOT NULL 제약 조건 제거#

기존 데이터베이스 칼럼에서 NOT NULL 제약 조건을 제거하려면 다단계 마이그레이션 프로세스가 필요합니다:

  • NOT NULL 제약 조건을 삭제하는 스키마 마이그레이션.

  • 잠재적 롤백 후 데이터 무결성을 보장하기 위한 별도의 데이터 마이그레이션. 이 마이그레이션은 다음 중 하나를 수행할 수 있습니다:

    유효하지 않은 레코드를 삭제합니다.

    • 유효하지 않은 레코드를 기본값으로 업데이트합니다.

데이터 수정(DML)과 스키마 변경(DDL)을 하나의 마이그레이션에 결합하는 것은 허용되지 않으므로 여러 마이그레이션이 필요합니다.

칼럼에 체크 제약 조건이 있는 경우 NOT NULL 제약 조건 제거#

먼저, 칼럼에 제약 조건이 있는지 확인합니다. 여러 가지 방법으로 확인할 수 있습니다:

CREATE TABLE labels (
    ...
   CONSTRAINT check_061f6f1c91 CHECK ((project_view IS NOT NULL))
);

예시#

마일스톤 번호는 예시일 뿐입니다. 올바른 버전을 사용하세요.

# frozen_string_literal: true

class DropNotNullConstraintFromLabelsProjectView< Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.7'

  def up
    remove_not_null_constraint :labels, :project_view
  end

  def down
    add_not_null_constraint :labels, :project_view
  end
end
# frozen_string_literal: true

class CleanupRecordsWithNullProjectViewValuesFromLabels < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.7'

  BATCH_SIZE = 1000

  class Label < MigrationRecord
    include EachBatch

    self.table_name = 'labels'
  end

  def up
    # no-op - this migration is required to allow a rollback of `DropNotNullConstraintFromLabelsProjectView`
  end

  def down
    Label.each_batch(of: BATCH_SIZE) do |relation|
      relation.
        where('project_view IS NULL').
        delete_all
    end
  end
end

칼럼에 체크 제약 조건이 없는 경우 NOT NULL 제약 조건 제거#

NOT NULL이 체크 제약 조건 없이 칼럼에만 정의되어 있다면 change_column_null을 사용할 수 있습니다.

structure.sql의 예시:

CREATE TABLE labels (
    ...
   projects_limit integer NOT NULL
);

예시#

마일스톤 번호는 예시일 뿐입니다. 올바른 버전을 사용하세요.

# frozen_string_literal: true

class DropNotNullConstraintFromLabelsProjectsLimit < Gitlab::Database::Migration[2.2]
  milestone '16.7'

  def up
    change_column_null :labels, :projects_limit, true
  end

  def down
    change_column_null :labels, :projects_limit, false
  end
end
# frozen_string_literal: true

class CleanupRecordsWithNullProjectsLimitValuesFromLabels < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.7'

  BATCH_SIZE = 1000

  class Label < MigrationRecord
    include EachBatch

    self.table_name = 'labels'
  end

  def up
    # no-op - this migration is required to allow a rollback of `DropNotNullConstraintFromLabelsProjectsLimit`
  end

  def down
    Label.each_batch(of: BATCH_SIZE) do |relation|
      relation.
        where('projects_limit IS NULL').
        delete_all
    end
  end
end

파티션 테이블에서 NOT NULL 제약 조건 제거#

중요 참고 사항: 모든 파티션이 부모 테이블로부터 제약 조건을 상속받으므로, 부모 테이블에 제약 조건이 존재하는 경우 개별 파티션에서 NOT NULL 제약 조건을 삭제할 수 없습니다. 이러한 이유로, 모든 자식 파티션으로 연쇄되는 부모 테이블에서 제약 조건을 삭제해야 합니다.

NOT NULL 제약 조건

GitLab v19.1
원문 보기
요약

NULL 값을 가져서는 안 되는 모든 속성은 데이터베이스에서 NOT NULL 칼럼으로 정의해야 합니다. 애플리케이션 로직에 따라, NOT NULL 칼럼에는 모델에 presence 유효성 검사를 정의하거나, 데이터베이스 정의의 일부로 기본값을 지정해야 합니다.

NULL 값을 가져서는 안 되는 모든 속성은 데이터베이스에서 NOT NULL 칼럼으로 정의해야 합니다.

애플리케이션 로직에 따라, NOT NULL 칼럼에는 모델에 presence 유효성 검사를 정의하거나, 데이터베이스 정의의 일부로 기본값을 지정해야 합니다. 예를 들어, 후자는 항상 non-NULL 값을 가져야 하지만 애플리케이션이 매번 강제할 필요 없는 잘 정의된 기본값이 있는 boolean 속성에 해당할 수 있습니다 (예: active=true).

belongs_to 연관 관계의 일부인 외래 키 칼럼의 경우, 별도의 presence: true 유효성 검사 대신 연관 관계에서 optional: false를 사용하는 것이 좋습니다. 이 방법이 의미적으로 더 올바르며 Rails의 내장 연관 관계 유효성 검사를 활용합니다. GitLab은 config/application.rbconfig.active_record.belongs_to_required_by_default = false가 설정되어 있으므로, belongs_to 연관 관계는 기본적으로 선택 사항이며 명시적으로 필수로 표시해야 합니다.

NOT NULL 칼럼이 있는 새 테이블 생성#

새 테이블을 추가할 때, 모든 NOT NULL 칼럼은 create_table 내부에서 직접 정의해야 합니다.

예를 들어, 두 개의 NOT NULL 칼럼이 있는 테이블을 생성하는 마이그레이션 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.bigint :guide, null: false
    end
  end
end

기존 테이블에 NOT NULL 칼럼 추가#

GitLab에서 PostgreSQL 11이 최소 버전이 된 이후로, NULL 및/또는 기본값이 있는 칼럼 추가가 훨씬 쉬워졌으며, 모든 경우에 표준 add_column 헬퍼를 사용해야 합니다.

예를 들어, db_guides 테이블에 새로운 NOT NULL 칼럼 active를 추가하는 마이그레이션 db/migrate/20200501000001_add_active_to_db_guides.rb를 고려해 보세요:

class AddExtendedTitleToSprints < Gitlab::Database::Migration[2.1]
  def change
    add_column :db_guides, :active, :boolean, default: true, null: false
  end
end

기존 칼럼에 NOT NULL 제약 조건 추가#

기존 데이터베이스 칼럼에 NOT NULL을 추가하려면 일반적으로 최소 두 개의 다른 릴리즈에 걸쳐 여러 단계가 필요합니다. 백그라운드 마이그레이션을 사용할 필요가 없을 만큼 테이블이 충분히 작다면, 이 모든 단계를 동일한 머지 리퀘스트에 포함할 수 있습니다. 트랜잭션 지속 시간을 줄이기 위해 별도의 마이그레이션을 사용하는 것을 권장합니다.

필요한 단계는 다음과 같습니다:

  • 릴리즈 N.M (현재 릴리즈)

    애플리케이션 레벨에서 $ATTRIBUTE 값이 설정되고 있는지 확인합니다.

    속성에 기본값이 있는 경우, 새 레코드에 기본값이 설정되도록 모델에 기본값을 추가합니다.

    • 새 레코드와 기존 레코드 모두에 대해 속성이 nil로 설정되는 코드의 모든 위치를 업데이트합니다 (해당하는 경우). before_savebefore_validation과 같은 ActiveRecord 콜백을 사용하는 경우에도, 일부 프로세스는 이러한 콜백을 건너뛸 수 있으므로 충분하지 않을 수 있습니다. update_column, update_columns, 그리고 insert_allupdate_all과 같은 대량 작업은 주의해야 할 메서드의 몇 가지 예시입니다.
  • 기존 레코드를 수정하기 위한 배포 후 마이그레이션을 추가합니다.

    테이블 크기에 따라, 다음 릴리즈에서 정리를 위한 백그라운드 마이그레이션이 필요할 수 있습니다. 자세한 내용은 대규모 테이블의 NOT NULL 제약 조건 섹션을 참조하세요.

  • 릴리즈 N.M+1 (다음 릴리즈)

    GitLab.com의 모든 기존 레코드에 속성이 설정되어 있는지 확인합니다. 그렇지 않다면, 릴리즈 N.M의 1단계로 돌아갑니다.

    • 1단계가 정상적이고 릴리즈 N.M의 백필이 배치 백그라운드 마이그레이션을 통해 완료된 경우, 백그라운드 마이그레이션을 완료하는 배포 후 마이그레이션을 추가합니다.

    • 이제 모든 기존 및 새 레코드가 유효해야 하므로, nil 속성을 가진 레코드를 방지하기 위해 모델에 속성에 대한 유효성 검사를 추가합니다.

    • NOT NULL 제약 조건을 추가하는 배포 후 마이그레이션을 추가합니다.

예시#

특정 릴리즈 마일스톤(예: 13.0)을 고려해 보세요.

프로덕션 데이터베이스를 확인한 결과, NULL 설명을 가진 epics가 있으므로 하나의 단계에서 제약 조건을 추가하고 유효성 검사를 할 수 없습니다.

NULL 설명을 가진 에픽이 없더라도, 다른 GitLab 인스턴스에는 그러한 레코드가 있을 수 있으므로 어느 경우에나 동일한 프로세스를 따릅니다.

새로운 유효하지 않은 레코드 방지 (현재 릴리즈)#

속성이 nil로 설정되는 코드 경로가 있다면 모두 업데이트하여 새 레코드와 기존 레코드 모두에 대해 속성을 non-nil 값으로 설정합니다.

Rails attributes API를 사용한 기본값이 있는 속성이 epic.rb에 추가되어 새 레코드에 기본값이 설정됩니다:

class Epic < ApplicationRecord
  attribute :description, default: 'No description'
end

기존 레코드 수정을 위한 데이터 마이그레이션 (현재 릴리즈)#

여기서의 접근 방식은 데이터 볼륨과 정리 전략에 따라 다릅니다. GitLab.com에서 수정해야 하는 레코드 수는 배포 후 마이그레이션을 사용할지 백그라운드 데이터 마이그레이션을 사용할지 결정하는 데 도움이 되는 좋은 지표입니다:

  • 데이터 볼륨이 1000개 레코드 미만이면, 배포 후 마이그레이션 내에서 데이터 마이그레이션을 실행할 수 있습니다.

  • 데이터 볼륨이 1000개 레코드보다 많으면, 백그라운드 마이그레이션을 생성하는 것이 권장됩니다.

어떤 옵션을 사용할지 확신이 없다면, 데이터베이스 팀에 조언을 구하세요.

다시 예시로 돌아가면, epics 테이블은 상당히 크지 않고 자주 액세스되지도 않으므로, 13.0 마일스톤(현재)에 대해 배포 후 마이그레이션을 추가합니다. db/post_migrate/20200501000002_cleanup_epics_with_null_description.rb:

class CleanupEpicsWithNullDescription < Gitlab::Database::Migration[2.1]
  # With BATCH_SIZE=1000 and epics.count=29500 on GitLab.com
  # - 30 iterations will be run
  # - each requires on average ~150ms
  # Expected total run time: ~5 seconds
  BATCH_SIZE = 1000

  disable_ddl_transaction!

  class Epic < MigrationRecord
    include EachBatch

    self.table_name = 'epics'
  end

  def up
    Epic.each_batch(of: BATCH_SIZE) do |relation|
      relation.
        where('description IS NULL').
        update_all(description: 'No description')
    end
  end

  def down
    # no-op : can't go back to `NULL` without first dropping the `NOT NULL` constraint
  end
end

모든 레코드가 수정되었는지 확인 (다음 릴리즈)#

postgres.ai를 사용하여 프로덕션 데이터베이스의 씬 클론을 생성하고 GitLab.com의 모든 레코드에 속성이 설정되어 있는지 확인합니다. 그렇지 않다면 새로운 유효하지 않은 레코드 방지 단계로 돌아가서 코드에서 속성이 명시적으로 nil로 설정되는 위치를 파악합니다. 코드 경로를 수정한 후 기존 레코드를 수정하기 위한 마이그레이션을 다시 예약하고 다음 단계를 수행할 다음 릴리즈를 기다립니다.

백그라운드 마이그레이션 완료 (다음 릴리즈)#

백그라운드 마이그레이션을 사용하여 마이그레이션이 완료된 경우 마이그레이션을 완료합니다.

모델에 유효성 검사 추가 (다음 릴리즈)#

이제 모든 기존 및 새 레코드가 유효해야 하므로, nil 속성을 가진 레코드를 방지하기 위해 모델에 속성에 대한 유효성 검사를 추가합니다.

belongs_to 연관 관계의 일부인 외래 키 칼럼의 경우, optional: false를 사용하는 것이 좋습니다:

class Epic < ApplicationRecord
  belongs_to :group, optional: false
end

다음보다 선호됩니다:

class Epic < ApplicationRecord
  belongs_to :group
  validates :group, presence: true
end

일반 속성의 경우:

class Epic < ApplicationRecord
  validates :description, presence: true
end

NOT NULL 제약 조건 추가 (다음 릴리즈)#

NOT NULL 제약 조건을 추가하면 전체 테이블을 스캔하여 각 레코드가 올바른지 확인합니다.

계속 예시에서, 13.1 마일스톤(다음)에 대해 최종 배포 후 마이그레이션에서 add_not_null_constraint 마이그레이션 헬퍼를 실행합니다:

class AddNotNullConstraintToEpicsDescription < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    # This will add the `NOT NULL` constraint and validate it
    add_not_null_constraint :epics, :description
  end

  def down
    # Down is required as `add_not_null_constraint` is not reversible
    remove_not_null_constraint :epics, :description
  end
end

대규모 테이블의 NOT NULL 제약 조건#

트래픽이 많은 테이블 (예: ci_buildsartifacts)에 대해 nullable 칼럼을 정리해야 하는 경우, 백그라운드 마이그레이션이 상당 시간 동안 실행될 수 있으며 데이터 마이그레이션 추가 후 다음 릴리즈에서 추가적인 배치 백그라운드 마이그레이션 정리가 필요합니다.

이 경우 릴리즈 수는 기존 레코드를 마이그레이션하는 데 필요한 시간에 따라 다릅니다. 정리는 백그라운드 마이그레이션이 완료된 후 예약되며, 이는 제약 조건이 추가된 후 여러 릴리즈가 지난 후일 수 있습니다.

  • 릴리즈 N.M:

    기존 레코드를 수정하기 위한 백그라운드 마이그레이션을 추가합니다:

# db/post_migrate/
class QueueBackfillMergeRequestDiffsProjectId < Gitlab::Database::Migration[2.2]
  milestone '16.7'
  restrict_gitlab_migration gitlab_schema: :gitlab_main_org

  MIGRATION = 'BackfillMergeRequestDiffsProjectId'
  DELAY_INTERVAL = 2.minutes

  def up
    queue_batched_background_migration(
      MIGRATION,
      :merge_request_diffs,
      :id
    )
  end

  def down
    delete_batched_background_migration(MIGRATION, :merge_request_diffs, :id, [])
  end
end
# db/post_migrate/
class FinalizeMergeRequestDiffsProjectIdBackfill < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'
  restrict_gitlab_migration gitlab_schema: :gitlab_main_org

  MIGRATION = 'BackfillMergeRequestDiffsProjectId'

  def up
    ensure_batched_background_migration_is_finished(
      job_class_name: MIGRATION,
      table_name: :merge_request_diffs,
      column_name: :id,
      job_arguments: [],
      finalize: true
    )
  end

  def down
    # no-op
  end
end
  • 그런 다음 완료 후, NOT NULL 제약 조건을 추가합니다:
# db/post_migrate/
class AddMergeRequestDiffsProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'

  def up
    add_not_null_constraint :merge_request_diffs, :project_id
  end

  def down
    remove_not_null_constraint :merge_request_diffs, :project_id
  end
end
  • 선택 사항. 매우 큰 테이블의 경우, 유효하지 않은 NOT NULL 제약 조건을 추가하고 비동기 유효성 검사를 예약합니다:
# db/post_migrate/
class AddMergeRequestDiffsProjectIdNotNullConstraint < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'

  def up
    add_not_null_constraint :merge_request_diffs, :project_id, validate: false
  end

  def down
    remove_not_null_constraint :merge_request_diffs, :project_id
  end
end
# db/post_migrate/
class PrepareMergeRequestDiffsProjectIdNotNullValidation < Gitlab::Database::Migration[2.2]
  milestone '16.10'

  CONSTRAINT_NAME = 'check_11c5f029ad'

  def up
    prepare_async_check_constraint_validation :merge_request_diffs, name: CONSTRAINT_NAME
  end

  def down
    unprepare_async_check_constraint_validation :merge_request_diffs, name: CONSTRAINT_NAME
  end
end
  • 선택 사항. 파티션 테이블의 경우 다음을 사용합니다:
# db/post_migrate/

PARTITIONED_TABLE_NAME = :p_ci_builds
CONSTRAINT_NAME = 'check_9aa9432137'

# Partitioned check constraint to be validated in https://gitlab.com/gitlab-org/gitlab/-/issues/XXXXX
def up
  prepare_partitioned_async_check_constraint_validation PARTITIONED_TABLE_NAME, name: CONSTRAINT_NAME
end

def down
  unprepare_partitioned_async_check_constraint_validation PARTITIONED_TABLE_NAME, name: CONSTRAINT_NAME
end

prepare_partitioned_async_check_constraint_validation은 모든 파티션에 대해 기존의 NOT VALID 체크 제약 조건만 비동기적으로 유효성 검사합니다. 파티션 테이블에 대한 체크 제약 조건을 생성하거나 유효성 검사하지 않습니다.

  • 선택 사항. 제약 조건이 비동기적으로 유효성 검사된 경우, 유효성 검사가 완료되면 NOT NULL 제약 조건을 유효성 검사합니다:

    Database Lab을 사용하여 유효성 검사가 성공했는지 확인합니다. \d+ table_name 명령을 실행하고 체크 제약 조건 정의에서 NOT VALID가 제거되었는지 확인합니다.

  • NOT NULL 제약 조건을 유효성 검사하는 마이그레이션을 추가합니다:

# db/post_migrate/
class ValidateMergeRequestDiffsProjectIdNullConstraint < Gitlab::Database::Migration[2.2]
  milestone '16.10'

  def up
    validate_not_null_constraint :merge_request_diffs, :project_id
  end

  def down
    # no-op
  end
end

이러한 경우, 업데이트 주기 초반에 데이터베이스 팀과 상의하세요. NOT NULL 제약 조건이 필요하지 않을 수도 있고, 정말 크거나 자주 액세스되는 테이블에 영향을 주지 않는 다른 옵션이 있을 수도 있습니다.

여러 칼럼에 대한 NOT NULL 제약 조건#

때로는 특정 수의 NOT NULL 값을 포함하는 칼럼 집합을 보장하고 싶을 때가 있습니다. 일반적인 예시는 프로젝트나 그룹 중 하나에 속할 수 있는 테이블로, 이 경우 project_id 또는 group_id가 반드시 존재해야 합니다. 이를 적용하려면 위의 사용 사례에 맞는 단계를 따르되, add_multi_column_not_null_constraint 헬퍼를 사용합니다.

이 예시에서, labels는 프로젝트나 그룹 중 하나에 속해야 하지만 둘 다에 속할 수는 없습니다. 이를 적용하기 위해 체크 제약 조건을 추가할 수 있습니다:

class AddLabelsNullConstraint < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'

  def up
    add_multi_column_not_null_constraint(:labels, :group_id, :project_id)
  end

  def down
    remove_multi_column_not_null_constraint(:labels, :group_id, :project_id)
  end
end

이렇게 하면 labels에 다음 제약 조건이 추가됩니다:

CREATE TABLE labels (
    ...
    CONSTRAINT check_45e873b2a8 CHECK ((num_nonnulls(group_id, project_id) = 1))
);

num_nonnulls는 제공된 인수 중 non-null인 수를 반환합니다. 제약 조건에서 이 값이 1과 같은지 확인하면 행에서 group_idproject_id 중 하나만 non-null 값을 포함해야 하고 둘 다 포함해서는 안 된다는 의미입니다.

커스텀 한도 및 연산자#

필요한 non-null 수를 사용자 정의하려면 다른 limit 및/또는 operator를 사용할 수 있습니다:

class AddLabelsNullConstraint < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.10'

  def up
    add_multi_column_not_null_constraint(:labels, :group_id, :project_id, limit: 0, operator: '>')
  end

  def down
    remove_multi_column_not_null_constraint(:labels, :group_id, :project_id)
  end
end

이는 project_idgroup_id 모두 존재할 수 있도록 허용하는 제약 조건에 반영됩니다:

CREATE TABLE labels (
    ...
    CONSTRAINT check_45e873b2a8 CHECK ((num_nonnulls(group_id, project_id) > 0))
);

기존 테이블의 칼럼에서 NOT NULL 제약 조건 제거#

기존 데이터베이스 칼럼에서 NOT NULL 제약 조건을 제거하려면 다단계 마이그레이션 프로세스가 필요합니다:

  • NOT NULL 제약 조건을 삭제하는 스키마 마이그레이션.

  • 잠재적 롤백 후 데이터 무결성을 보장하기 위한 별도의 데이터 마이그레이션. 이 마이그레이션은 다음 중 하나를 수행할 수 있습니다:

    유효하지 않은 레코드를 삭제합니다.

    • 유효하지 않은 레코드를 기본값으로 업데이트합니다.

데이터 수정(DML)과 스키마 변경(DDL)을 하나의 마이그레이션에 결합하는 것은 허용되지 않으므로 여러 마이그레이션이 필요합니다.

칼럼에 체크 제약 조건이 있는 경우 NOT NULL 제약 조건 제거#

먼저, 칼럼에 제약 조건이 있는지 확인합니다. 여러 가지 방법으로 확인할 수 있습니다:

CREATE TABLE labels (
    ...
   CONSTRAINT check_061f6f1c91 CHECK ((project_view IS NOT NULL))
);

예시#

마일스톤 번호는 예시일 뿐입니다. 올바른 버전을 사용하세요.

# frozen_string_literal: true

class DropNotNullConstraintFromLabelsProjectView< Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.7'

  def up
    remove_not_null_constraint :labels, :project_view
  end

  def down
    add_not_null_constraint :labels, :project_view
  end
end
# frozen_string_literal: true

class CleanupRecordsWithNullProjectViewValuesFromLabels < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.7'

  BATCH_SIZE = 1000

  class Label < MigrationRecord
    include EachBatch

    self.table_name = 'labels'
  end

  def up
    # no-op - this migration is required to allow a rollback of `DropNotNullConstraintFromLabelsProjectView`
  end

  def down
    Label.each_batch(of: BATCH_SIZE) do |relation|
      relation.
        where('project_view IS NULL').
        delete_all
    end
  end
end

칼럼에 체크 제약 조건이 없는 경우 NOT NULL 제약 조건 제거#

NOT NULL이 체크 제약 조건 없이 칼럼에만 정의되어 있다면 change_column_null을 사용할 수 있습니다.

structure.sql의 예시:

CREATE TABLE labels (
    ...
   projects_limit integer NOT NULL
);

예시#

마일스톤 번호는 예시일 뿐입니다. 올바른 버전을 사용하세요.

# frozen_string_literal: true

class DropNotNullConstraintFromLabelsProjectsLimit < Gitlab::Database::Migration[2.2]
  milestone '16.7'

  def up
    change_column_null :labels, :projects_limit, true
  end

  def down
    change_column_null :labels, :projects_limit, false
  end
end
# frozen_string_literal: true

class CleanupRecordsWithNullProjectsLimitValuesFromLabels < Gitlab::Database::Migration[2.2]
  disable_ddl_transaction!
  milestone '16.7'

  BATCH_SIZE = 1000

  class Label < MigrationRecord
    include EachBatch

    self.table_name = 'labels'
  end

  def up
    # no-op - this migration is required to allow a rollback of `DropNotNullConstraintFromLabelsProjectsLimit`
  end

  def down
    Label.each_batch(of: BATCH_SIZE) do |relation|
      relation.
        where('projects_limit IS NULL').
        delete_all
    end
  end
end

파티션 테이블에서 NOT NULL 제약 조건 제거#

중요 참고 사항: 모든 파티션이 부모 테이블로부터 제약 조건을 상속받으므로, 부모 테이블에 제약 조건이 존재하는 경우 개별 파티션에서 NOT NULL 제약 조건을 삭제할 수 없습니다. 이러한 이유로, 모든 자식 파티션으로 연쇄되는 부모 테이블에서 제약 조건을 삭제해야 합니다.