일괄로 테이블 반복하기

Rails에는 행을 일괄로 반복하는 데 사용할 수 있는 in_batches라는 메서드가 제공됩니다. 예를 들어:

User.in_batches(of: 10) do |relation|
  relation.update_all(updated_at: Time.now)
end

불행하게도, 이 메서드는 쿼리 및 메모리 사용 측면에서 효율적이지 않게 구현되어 있습니다.

이를 해결하기 위해 모델에 EachBatch 모듈을 포함한 후 each_batch 클래스 메서드를 사용할 수 있습니다. 예를 들어:

class User < ActiveRecord::Base
  include EachBatch
end

User.each_batch(of: 10) do |relation|
  relation.update_all(updated_at: Time.now)
end

이렇게 하면 다음과 같은 쿼리가 생성됩니다:

User Load (0.7ms)  SELECT  "users"."id" FROM "users" WHERE ("users"."id" >= 41654)  ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1000
  (0.7ms)  SELECT COUNT(*) FROM "users" WHERE ("users"."id" >= 41654) AND ("users"."id" < 42687)

이 메서드의 API는 in_batches와 유사하지만 in_batches가 지원하는 모든 인수를 지원하지는 않습니다. 따라서 특별한 경우가 아닌 이상 each_batch를 항상 사용해야 합니다.

고유하지 않은 열을 반복하는 경우

관련 컨텍스트에서 고유하지 않은 열로 each_batch 메서드를 사용해서는 안 됩니다. 또한 일관되지 않은 일괄 크기는 고유하지 않은 열을 반복할 때 성능 문제를 일으킵니다. 속성을 반복하는 경우 최대 일괄 크기를 적용하더라도 결과 일괄이 해당 크기를 초과하지 않는 것을 보장할 수 없습니다. 다음 스니펫은 이 시나리오를 보여줍니다. 1에서 10,000 사이의 id를 가진 사용자를 위해 Ci::Build 항목을 선택하려고 시도할 때 데이터베이스가 1,215,178개의 일치하는 행을 반환합니다.

[ gstg ] production> Ci::Build.where(user_id: (1..10_000)).size
=> 1215178

이는 다음 쿼리로 인해 발생합니다:

[ gstg ] production> puts Ci::Build.where(user_id: (1..10_000)).to_sql
SELECT "ci_builds".* FROM "ci_builds" WHERE "ci_builds"."type" = 'Ci::Build' AND "ci_builds"."user_id" BETWEEN 1 AND 10000
=> nil

범위 BETWEEN ? AND ?로 고유하지 않은 열을 필터링하는 And 쿼리는, 이전 예제에서 10,000으로 제한되었더라도 결과 데이터 집합의 크기에는 제한이 없습니다. 속성의 n가지 가능한 값에서 해당 속성을 포함하는 기록 수가 n보다 작을 것을 확신할 수 없기 때문에 이러한 일이 발생합니다.

distinct_each_batch로 느슨한 인덱스 스캔

고유하지 않은 열을 반복해야 하는 경우 distinct_each_batch 도우미 메서드를 사용하세요. 이 도우미는 데이터베이스 인덱스 내에서 중복된 값을 건너뛰는 느슨한 인덱스 스캔 기술(스킵 인덱스 스캔)을 사용합니다.

예: Issue 모델에서 고유한 author_id를 반복

Issue.distinct_each_batch(column: :author_id, of: 1000) do |relation|
  users = User.where(id: relation.select(:author_id)).to_a
end

이 기술은 데이터 분포에 관계없이 일괄 간에 안정적인 성능을 제공합니다. relation 객체는 주어진 column만 포함된 ActiveRecord 스코프를 반환합니다. 다른 열은 로드되지 않습니다.

기본 데이터베이스 쿼리는 재귀적 공통 테이블 표현식을 사용하며 추가 오버헤드를 추가합니다. 따라서 일반적인 each_batch 반복보다 작은 일괄 크기를 사용하는 것이 좋습니다.

열 정의

기본적으로 EachBatch는 모델의 기본 키를 반복에 사용합니다. 대부분의 경우에 동작하지만, 경우에 따라 반복에 다른 열을 사용하고 싶을 수 있습니다.

Project.distinct.each_batch(column: :creator_id, of: 10) do |relation|
  puts User.where(id: relation.select(:creator_id)).map(&:id)
end

위 쿼리는 프로젝트 생성자를 반복하고 중복을 제왕치 않고 출력합니다.

note
열이 고유하지 않은 경우(고유한 인덱스 정의 없음), 관련 문에 distinct 메서드를 호출하는 것이 필요합니다. distinct를 사용하지 않고 고유하지 않은 열을 사용하면 each_batch가 다음 문제에 빠질 수 있습니다.

데이터 마이그레이션에서의 EachBatch

대량의 데이터를 반복하는 경우 EachBatch를 사용하는 것이 좋습니다.

데이터 마이그레이션의 특별한 경우는 일괄 배경 마이그레이션으로, 실제 데이터 수정은 백그라운드 작업에서 실행됩니다. 데이터 범위(슬라이스)를 결정하고 백그라운드 작업을 예약하는 마이그레이션 코드는 each_batch를 사용합니다.

EachBatch의 효율적인 사용

EachBatch는 대형 테이블을 반복하는 데 도움이 됩니다. 그러나 EachBatch가 모든 반복 관련 성능 문제를 마법처럼 해결해 주지는 않으며, 특정 시나리오에서는 전혀 도움이 되지 않을 수도 있습니다. 데이터베이스 관점에서 올바르게 구성된 데이터베이스 인덱스가 EachBatch의 성능을 향상시키기 위해서도 필요합니다.

예시 1: 간단한 반복

예를 들어 users 테이블을 반복하고 User 레코드를 표준 출력에 출력하려고 합니다. users 테이블에는 수백만 개의 레코드가 있기 때문에 사용자를 가져오기 위한 단일 쿼리를 실행하면 시간 초과될 수 있습니다.

이 테이블은 간소화된 users 테이블로, 몇 개의 더 작은 간격이 있어 예제를 조금 더 현실적으로 만든 것입니다(id 컬럼의 몇 개의 레코드가 이미 삭제되었습니다). id 필드에 하나의 인덱스가 있습니다:

ID sign_in_count created_at
1 1 2020-01-01
2 4 2020-01-01
9 1 2020-01-03
300 5 2020-01-03
301 9 2020-01-03
302 8 2020-01-03
303 2 2020-01-03
350 1 2020-01-03
351 3 2020-01-04
352 0 2020-01-05
353 9 2020-01-11
354 3 2020-01-12

모든 사용자를 메모리로로드(회피):

users = User.all

users.each { |user| puts user.inspect }

each_batch 사용:

# 참고: 이 예제에서 일괄 크기로 5를 선택했지만 기본값은 1,000입니다
User.each_batch(of: 5) do |relation|
  relation.each { |user| puts user.inspect }
end

each_batch 작동 방식

첫 번째 단계는 다음과 같이 데이터베이스 쿼리를 실행하여 테이블에서 가장 낮은 id (시작 id)을 찾는 것입니다:

SELECT "users"."id" FROM "users" ORDER BY "users"."id" ASC LIMIT 1

시작 ID 값 읽기

쿼리에서 데이터의 읽기만 (인덱스만의 스캔(INDEX ONLY SCAN)) 하고 테이블에는 접근하지 않습니다. 데이터베이스 인덱스는 정렬되어 있기 때문에 첫 번째 항목을 가져오는 것은 매우 저렴한 작업입니다.

다음 단계는 일괄 크기 구성을 따르는 다음 id(끝 id)를 찾는 것입니다. 이 예에서는 크기를 5로 사용했습니다. EachBatchOFFSET 절을 사용하여 “이동된” id 값을 얻는 것입니다.

SELECT "users"."id" FROM "users" WHERE "users"."id" >= 1 ORDER BY "users"."id" ASC LIMIT 1 OFFSET 5

끝 ID 값 읽기

다시 인덱스에서만 쿼리합니다. OFFSET 5는 여섯 번째 id 값을 가져옵니다: 이 쿼리는 테이블 크기나 반복 횟수와는 상관없이 최대 여섯 항목만 인덱스에서 읽습니다.

이제 첫 번째 일괄의 id 범위를 알았습니다. 이제 relation 블록을 위한 쿼리를 작성하는 시간입니다.

SELECT "users".* FROM "users" WHERE "users"."id" >= 1 AND "users"."id" < 302

`users` 테이블에서 행 읽기

< 기호를 참고하세요. 이전에 인덱스에서 여섯 항목이 읽혔고, 이 쿼리에서 마지막 값은 “제외”됩니다. 쿼리는 테이블에서의 다섯 user 행의 위치를 얻기 위해 인덱스를 조사하고 행을 읽습니다. 반환된 배열이 루비에서 처리됩니다.

첫 반복이 완료되었습니다. 다음 반복에서는 마지막 id 값이 이전 반복에서 재사용되어 다음 끝 id 값을 알아내는 것입니다.

SELECT "users"."id" FROM "users" WHERE "users"."id" >= 302 ORDER BY "users"."id" ASC LIMIT 1 OFFSET 5

두 번째 끝 ID 값을 읽습니다

이제 두 번째 반복에 대한 users 쿼리를 손쉽게 작성할 수 있습니다.

SELECT "users".* FROM "users" WHERE "users"."id" >= 302 AND "users"."id" < 353

테이블에서 두 번째 반복을 위해 행 읽기

예제 2: 필터링된 반복

이전 예제를 기반으로, 우리는 0 회 로그인 한 사용자를 출력하려고 합니다. sign_in_count 열에서 로그인 횟수를 추적하기 때문에 다음과 같은 코드를 작성합니다.

users = User.where(sign_in_count: 0)

users.each_batch(of: 5) do |relation|
  relation.each { |user| puts user.inspect }
end

each_batch은 시작 id 값에 대한 다음 SQL 쿼리를 생성합니다.

SELECT "users"."id" FROM "users" WHERE "users"."sign_in_count" = 0 ORDER BY "users"."id" ASC LIMIT 1

id 열만 선택하고 id를 기준으로 정렬하기 때문에 데이터베이스는 id (기본 키 인덱스) 열의 인덱스를 사용하지만, sign_in_count 열에 추가 조건이 있습니다. 이 열은 인덱스의 일부가 아니기 때문에 데이터베이스는 첫 번째 일치하는 행을 찾기 위해 실제 테이블을 조회해야 합니다.

추가 필터와 함께 인덱스 읽기

note
조회된 행의 개수는 테이블 내 데이터 분포에 따라 다릅니다. - 최상의 경우: 첫 번째 사용자가 한 번도 로그인하지 않은 경우. 데이터베이스는 한 행만 읽습니다. - 최악의 경우: 모든 사용자가 한 번 이상 로그인한 경우. 데이터베이스는 모든 행을 읽습니다.

이 특정 예에서 데이터베이스는 첫 번째 id 값을 결정하기 위해 10개의 행을 읽어야 했습니다 (일관된 배치 크기 설정과 관계없이). “실제 세계” 응용 프로그램에서 필터링이 문제를 발생시키는 지 여부를 예측하기는 어렵지만, GitLab의 경우 프로덕션 레플리카에서 데이터를 확인하는 것은 좋은 시작점이지만, GitLab.com의 데이터 분포가 Self-Managed형 인스턴스와 다를 수 있다는 점을 명심하세요.

each_batch로 필터링 개선하기

특수 조건부 인덱스
CREATE INDEX index_on_users_never_logged_in ON users (id) WHERE sign_in_count = 0

테이블과 새롭게 생성된 인덱스는 다음과 같습니다.

특수 인덱스 읽기

이 인덱스 정의는 idsign_in_count 열의 조건을 커버하여 each_batch 쿼리를 매우 효과적으로 만듭니다 (단순한 반복 예제와 유사함).

사용자가 한 번도 로그인하지 않은 경우가 드물기 때문에 작은 인덱스 크기가 예상됩니다. 인덱스 정의에서 id 만 포함하는 것도 인덱스 크기를 작게 유지하는 데 도움이 됩니다.

열에 대한 인덱스

나중에 테이블을 반복하면서 다른 sign_in_count 값을 위해 필터링을 해야하는 경우, 이전에 제안된 조건부 인덱스를 사용할 수 없는 경우가 있습니다. 새로운 쿼리에 대한 WHERE 조건이 새 필터와 일치하지 않기 때문입니다.

이 문제를 해결하기 위해 두 가지 옵션이 있습니다:

  • 새로운 쿼리를 커버하기 위해 다른 조건부 인덱스를 생성합니다.
  • 보다 일반적인 구성으로 인덱스를 교체합니다.
note
같은 테이블 및 열에 대해 여러 개의 인덱스를 가지고 있는 것은 데이터를 쓸 때 성능 병목이 될 수 있습니다.

다음과 같은 인덱스(피함)를 고려해봅시다:

CREATE INDEX index_on_users_never_logged_in ON users (id, sign_in_count)

인덱스 정의는 id 열로 시작하여 데이터 선택성 관점에서 매우 비효율적인 인덱스를 만듭니다.

SELECT "users"."id" FROM "users" WHERE "users"."sign_in_count" = 0 ORDER BY "users"."id" ASC LIMIT 1

위의 쿼리를 실행하면 INDEX ONLY SCAN이 발생합니다. 그러나 쿼리는 여전히 인덱스의 알 수 없는 수의 항목을 반복하고, 그런 다음 sign_in_count0인 첫 번째 항목을 찾아야 합니다.

비효율적인 인덱스 읽기

우리는 인덱스 정의에서 열을 교환함으로써 쿼리를 크게 개선할 수 있습니다 (선호).

CREATE INDEX index_on_users_never_logged_in ON users (sign_in_count, id)

좋은 인덱스 읽기

다음과 같은 인덱스 정의는 each_batch와 잘 동작하지 않습니다 (피함).

CREATE INDEX index_on_users_never_logged_in ON users (sign_in_count)

each_batchid 열을 기반으로 범위 쿼리를 작성하기 때문에 이 인덱스를 효과적으로 사용할 수 없습니다. 데이터베이스는 테이블의 행을 읽거나 기본 키 인덱스도 읽는 바이너리 검색을 사용해야 합니다.

“느린” 반복

느린 반복이란 좋은 인덱스 구성을 사용하여 테이블을 반복하고 생성된 관계에 대해 필터링을 적용하는 것을 의미합니다.

User.each_batch(of: 5) do |relation|
  relation.where(sign_in_count: 0).each { |user| puts user.inspect }
end

반복은 기본 키 인덱스(열 id에 대해)를 사용하기 때문에 명령 시간 초과로부터 안전합니다. 필터(sign_in_count: 0)는 이미 제한된 상태의 relation에 적용됩니다(범위). 행의 수는 제한됩니다.

느린 반복은 일반적으로 완료하는 데 더 많은 시간이 소요됩니다. 반복 횟수는 더 많고 한 번의 반복이 배치 크기보다 적은 레코드를 얻을 수도 있습니다. 반복은 때로는 0개의 레코드를 얻을 수도 있습니다. 이것은 최적의 해결책이 아니지만(특히 큰 테이블을 다루는 경우) 몇 가지 경우에 유일한 선택지입니다.

서브쿼리 사용

each_batch 쿼리에서 서브쿼리를 사용하는 것은 대부분의 경우 잘 작동하지 않습니다. 다음 예를 고려해보세요.

projects = Project.where(creator_id: Issue.where(confidential: true).select(:author_id))

projects.each_batch do |relation|
  # 작업 수행
end

반복은 projects 테이블의 id 열을 사용합니다. 배치 설정은 서브쿼리에 영향을 주지 않습니다. 따라서 각 반복마다 서브쿼리가 데이터베이스에서 실행됩니다. 이는 쿼리에 일정한 “로드”를 추가하여 종종 명령 시간 초과로 이어집니다. 우리는 알 수 없는 수의 기밀 문제를 가지고 있으며, 실행 시간과 접근된 데이터베이스 행 수는 issues 테이블의 데이터 분포에 의존합니다.

note
서브쿼리를 사용하는 것은 서브쿼리가 일정 수의 행을 반환할 때에만 작동합니다.

서브쿼리 개선

서브쿼리를 다룰 때 느린 반복 접근 방식을 사용할 수 있습니다: creator_id에 대한 필터는 생성된 relation 객체의 일부가 될 수 있습니다.

projects = Project.all

projects.each_batch do |relation|
  relation.where(creator_id: Issue.where(confidential: true).select(:author_id))
end

만약 issues 테이블에 대한 쿼리가 충분히 효율적이지 않다면, 중첩 루프를 작성할 수 있습니다. 가능한 경우 이를 피하십시오.

projects = Project.all

projects.each_batch do |relation|
  issues = Issue.where(confidential: true)
  
  issues.each_batch do |issues_relation|
    relation.where(creator_id: issues_relation.select(:author_id))
  end
end

issues 테이블의 행이 projects보다 훨씬 많다는 것을 알고 있다면, issues 테이블을 먼저 나누는 것이 합리적일 것입니다.

JOINEXISTS 사용

JOIN을 사용하는 시점:

  • 테이블 간에 1:1 또는 1:N 관계가 있고 결합 레코드가 (거의) 항상 존재하는 경우. 이는 “확장 형식” 테이블에 잘 작동합니다.
    • projects - project_settings
    • users - user_details
    • users - user_statuses
  • 이 경우 LEFT JOIN이 잘 작동합니다. 결합된 테이블의 조건은 생성된 관계에 주입되어 결합된 테이블의 데이터 분포에 영향을 받지 않습니다.

예시:

users = User.joins("LEFT JOIN personal_access_tokens on personal_access_tokens.user_id = users.id")

users.each_batch do |relation|
  relation.where("personal_access_tokens.name = 'name'")
end

EXISTS 쿼리는 each_batch 쿼리의 내부 relation에만 추가되어야 합니다.

User.each_batch do |relation|
  relation.where("EXISTS (SELECT 1 FROM ...")
end

관계 객체에서 복잡한 쿼리

relation 객체에 여러 추가 조건이 있는 경우 실행 계획이 “불안정”해질 수 있습니다.

예시:

Issue.each_batch do |relation|
  relation
    .joins(:metrics)
    .joins(:merge_requests_closing_issues)
    .where("id IN (SELECT ...)")
    .where(confidential: true)
end

여기서는 relation 쿼리가 BATCH_SIZE의 사용자 레코드를 읽고 제공된 쿼리에 따라 결과를 필터링할 것으로 예상합니다. 계획자는 confidential 열에 대한 인덱스를 사용한 비트맵 인덱스 조회가 쿼리를 실행하는 더 나은 방법이라고 결정할 수 있습니다. 이로 인해 의도치 않게 많은 양의 행이 읽혀 시간이 초과될 수 있습니다.

문제: 우리는 관계가 최대 BATCH_SIZE의 레코드를 반환한다는 것을 정확히 알지만, 계획자는 이를 알지 못합니다.

공통 테이블 표현식(CTE) 트릭을 사용하여 범위 쿼리가 먼저 실행되도록 강제:

Issue.each_batch(of: 1000) do |relation|
  cte = Gitlab::SQL::CTE.new(:batched_relation, relation.limit(1000))
  
  scope = cte
    .apply_to(Issue.all)
    .joins(:metrics)
    .joins(:merge_requests_closing_issues)
    .where("id IN (SELECT ...)")
    .where(confidential: true)
  
  puts scope.to_a
end

레코드 카운팅

대량의 데이터가 있는 테이블의 경우 쿼리를 통한 레코드 카운팅은 시간 초과로 이어질 수 있습니다. EachBatch 모듈은 레코드를 반복적으로 카운팅하는 대안적인 방법을 제공합니다. each_batch 사용의 단점은 반환된 관계 객체에 대해 실행되는 추가 카운트 쿼리입니다.

each_batch_count 메서드는 추가 카운트 쿼리를 실행할 필요 없이 더 효율적인 접근 방법입니다. 이 메서드를 호출하여 반복 프로세스를 일시 중단하고 필요할 때 다시 시작할 수 있습니다. 이 기능은 카운팅 작업을 수행하는 동안 5분 후에 오류 예산 위반이 트리거되는 상황과 같이 유용합니다.

예를 들어, EachBatch를 사용하여 레코드를 카운팅하는 것은 다음과 같이 추가 카운트 쿼리를 호출하는 것을 포함합니다:

count = 0

Issue.each_batch do |relation|
  count += relation.count
end

puts count

반면에, each_batch_count 메서드는 추가 카운트 쿼리를 호출하지 않고 카운팅 프로세스를 더 효율적으로 수행합니다(카운팅이 반복 쿼리의 일부입니다):

count, _last_value = Issue.each_batch_count # 여기서 마지막 값은 무시할 수 있습니다

또한, each_batch_count 메서드는 카운팅 프로세스를 언제든 중지하고 다시 시작할 수 있습니다. 이 기능은 다음 코드 스니펫에서 나타납니다:

stop_at = Time.current + 3.minutes

count, last_value = Issue.each_batch_count do
  stop_at.past? # 카운팅을 중지하는 조건
end

# 나중에 카운팅 계속
stop_at = Time.current + 3.minutes

count, last_value = Issue.each_batch_count(last_count: count, last_value: last_value) do
  stop_at.past?
end

EachBatch vs BatchCount

Service Ping에 새 카운터를 추가할 때 레코드를 카운팅하는 우선적인 방법은 Gitlab::Database::BatchCount 클래스를 사용하는 것입니다. BatchCount에 구현된 반복 로직은 EachBatch와 유사한 성능 특성을 가지고 있습니다. BatchCount를 개선하기 위한 대부분의 팁과 제안은 BatchCount에도 적용됩니다.

키셋 페이지네이션으로 반복

EachBatch로 작업할 수 없는 특수한 경우 몇 가지 있습니다. EachBatch는 하나의 고유한 열(일반적으로 기본 키)을 필요로 하기 때문에 타임스탬프 열과 복합 기본 키를 가진 테이블에 대해 반복 작업이 불가능합니다.

EachBatch에서 작동하지 않는 경우, 키셋 페이지네이션을 사용하여 테이블이나 행 범위를 반복할 수 있습니다. 확장 및 성능 특성은 EachBatch와 매우 유사합니다.

예시:

  • 특정 순서(타임스탬프 열)로 테이블을 반복 작업하고 고유한 값을 포함하는 정렬 기준열이 정렬 열에 있는 경우 타이 브레이커와 결합합니다.
  • 복합 기본 키를 가진 테이블을 반복할 때.

프로젝트 내 이슈를 생성일별로 반복

테이블의 모든 열, 예를 들어 created_at DESC와 같이 특정 순서로 반복하려면 키셋 페이지네이션을 사용할 수 있습니다. created_at에 대해 반환된 레코드의 일관된 순서를 보장하려면 고유한 created_at 값이 동일한 경우 고유한 값을 가진 타이 브레이커 열을 사용합니다(예: id).

다음과 같이 issues 테이블에서 다음 인덱스가 있다고 가정해보십시오:

idx_issues_on_project_id_and_created_at_and_id" btree (project_id, created_at, id)

추가 처리용 레코드 검색

다음 스니펫은 특정 순서(created_at, id)를 사용하여 프로젝트 내 이슈 레코드를 반복합니다.

scope = Issue.where(project_id: 278964).order(:created_at, :id) # id는 타이 브레이커입니다

iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope)

iterator.each_batch(of: 100) do |records|
  puts records.map(&:id)
end

쿼리에 추가 필터를 추가할 수 있습니다. 다음 예제는 지난 30일 동안 생성된 이슈 ID만 나열합니다:

scope = Issue.where(project_id: 278964).where('created_at > ?', 30.days.ago).order(:created_at, :id) # id는 타이 브레이커입니다

iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope)

iterator.each_batch(of: 100) do |records|
  puts records.map(&:id)
end

배치 내 레코드 업데이트

복잡한 ActiveRecord 쿼리의 경우 .update_all 메서드는 잘 작동하지 않으므로 올바르지 않은 UPDATE 문을 생성합니다. 따라서 레코드를 배치로 업데이트하기 위해 원시 SQL을 사용할 수 있습니다:

scope = Issue.where(project_id: 278964).order(:created_at, :id) # id가 타이 브레이커입니다

iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope)

iterator.each_batch(of: 100) do |records|
  ApplicationRecord.connection.execute("UPDATE issues SET updated_at=NOW() WHERE issues.id in (#{records.dup.reselect(:id).to_sql})")
end
note
반복을 안정적이고 예측 가능하게 유지하려면 ORDER BY 절의 열을 업데이트하지 않도록 합니다.

merge_request_diff_commits 테이블 반복

merge_request_diff_commits 테이블은 merge_request_diff_id, relative_order의 복합 기본 키를 사용하므로 EachBatch를 효율적으로 사용하는 것이 불가능합니다.

merge_request_diff_commits 테이블을 페이징 처리하려면 다음 스니펫을 사용할 수 있습니다:

# 사용자 지정 오더 객체 구성:
order = Gitlab::Pagination::Keyset::Order.build([
  Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
    attribute_name: 'merge_request_diff_id',
    order_expression: MergeRequestDiffCommit.arel_table[:merge_request_diff_id].asc,
    nullable: :not_nullable
  ),
  Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
    attribute_name: 'relative_order',
    order_expression: MergeRequestDiffCommit.arel_table[:relative_order].asc,
    nullable: :not_nullable
  )
])
MergeRequestDiffCommit.include(FromUnion) # 키셋 페이지네이션은 UNION 쿼리를 생성합니다

scope = MergeRequestDiffCommit.order(order)

iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope)

iterator.each_batch(of: 100) do |records|
  puts records.map { |record| [record.merge_request_diff_id, record.relative_order] }.inspect
end

오더 객체 구성

간단한 ActiveRecord order 스코프와 잘 작동하는 키셋 페이지네이션입니다(첫번째 예시). 그러나 특별한 경우에는 ORDER BY 절의 열을 기술해야 합니다(두 번째 예시). 키셋 페이지네이션 라이브러리에서 ORDER BY 구성을 자동으로 결정할 수 없는 경우에는 오류가 발생합니다.

Gitlab::Pagination::Keyset::OrderGitlab::Pagination::Keyset::ColumnOrderDefinition 클래스의 코드 주석은 ORDER BY 절을 구성하는 데 사용할 수 있는 옵션에 대한 개요를 제공합니다. 또한 키셋 페이지네이션 문서에 몇 가지 코드 예제를 찾을 수 있습니다.