일괄 처리 테이블 반복
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
메서드를 사용해서는 안됩니다. 이는
무한 루프에 빠질 수 있습니다.
또한 일관되지 않은 일괄 크기는 고유하지 않은 열을 반복할 때 성능 문제를 일으킵니다. 특성에 대한 최대 일괄 크기를 적용해도 결과적인 일괄이 해당 크기를 초과하지 않을 것이 보장되지 않습니다. 다음 스니펫은 이 상황을 보여줍니다.
예를 들어, id
가 1
에서10,000
사이인 사용자의
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
비고 여부와 상관없이 고유하지 않은 열을 범위로 필터링하는 AND
쿼리는 "ci_builds"."user_id" BETWEEN ? 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 스코프를 반환합니다. 다른 열은 로드되지 않습니다.
기본적으로 EachBatch
는 모델의 기본 키를 반복에 사용합니다. 이는 대부분의 경우에 작동하지만 일부 경우에는 반복에 다른 열을 사용하고 싶을 수 있습니다.
Project.distinct.each_batch(column: :creator_id, of: 10) do |relation|
puts User.where(id: relation.select(:creator_id)).map(&:id)
end
위의 쿼리는 프로젝트 생성자를 반복하여 중복을 제외하고 출력합니다.
비고:
열이 고유하지 않은 경우 (고유한 인덱스 정의 없음) 관련성에distinct
메서드를 호출하는 것이 필요합니다. 고유하지 않은 열을 distinct
없이 사용하면 다음에 설명된 무한 루프에 빠질 수 있습니다.
데이터 마이그레이션에서의 EachBatch
대량의 데이터를 처리할 때 선호되는 방법은 EachBatch
를 사용하여 데이터를 반복하는 것입니다.
데이터 마이그레이션의 특수한 경우는 일괄 배경 마이그레이션으로, 실제 데이터 수정이 백그라운드 작업에서 실행됩니다. 데이터 범위(슬라이스)를 결정하고 백그라운드 작업을 예약하는 마이그레이션 코드가 each_batch
를 사용합니다.
each_batch
의 효율적인 사용
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
(끝 id
)를 찾는 것입니다. 이 예제에서는 배치 크기를 5로 사용했습니다. EachBatch
는 OFFSET
절을 사용하여 “이동된” id
값을 얻습니다.
SELECT "users"."id" FROM "users" WHERE "users"."id" >= 1 ORDER BY "users"."id" ASC LIMIT 1 OFFSET 5
다시, 쿼리는 인덱스만을 참조합니다. OFFSET 5
는 여섯 번째 id
값을 가져옵니다. 이 쿼리는 테이블 크기나 반복 횟수에 관계없이 최대 여섯 개의 항목을 인덱스에서 읽습니다.
이 시점에서 첫 번째 배치의 id
범위를 알게 되었습니다. 이제 relation
블록을 위한 쿼리를 작성할 시간입니다.
SELECT "users".* FROM "users" WHERE "users"."id" >= 1 AND "users"."id" < 302
<
기호를 주목하세요. 이전에 인덱스에서 여섯 개의 항목이 읽혔고, 이 쿼리에서는 “제외된” 마지막 값입니다. 쿼리는 인덱스를 참조하여 디스크의 다섯 개의 user
행의 위치를 가져오고 테이블에서 행을 읽습니다. 반환된 배열은 Ruby에서 처리됩니다.
첫 번째 반복이 완료되었습니다. 다음 반복에서는 이전 반복에서 마지막 id
값을 재사용하여 다음 끝 id
값을 찾습니다.
SELECT "users"."id" FROM "users" WHERE "users"."id" >= 302 ORDER BY "users"."id" ASC LIMIT 1 OFFSET 5
이제 우리는 두 번째 반복을 위한 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
열에 추가 조건이 있는 것으로 인해 실제 테이블을 조회해야 합니다.
참고: 조회된 행의 수는 테이블 내 데이터 분포에 따라 다릅니다.
- 최상의 시나리오: 첫 번째 사용자가 한 번도 로그인하지 않았습니다. 데이터베이스는 하나의 행만 읽습니다.
- 최악의 시나리오: 모든 사용자가 한 번 이상 로그인했습니다. 데이터베이스는 모든 행을 읽습니다.
이 특정 예시에서, 데이터베이스는 첫 번째 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
이것은 우리의 테이블과 새로 생성된 인덱스가 이렇게 보입니다:
이 인덱스 정의는 id
및 sign_in_count
열에 대한 조건을 포함하므로 each_batch
쿼리를 매우 효과적으로 만듭니다 (단순한 반복 예시처럼).
사용자가 한 번도 로그인하지 않은 경우는 드물기 때문에 작은 인덱스 크기가 예상됩니다. 인덱스 정의에서 id
만 포함하는 것은 인덱스 크기를 작게 유지하는 데 도움이 됩니다.
열별 인덱스
나중에 우리는 테이블을 순회하면서 다른 sign_in_count
값을 필터링하고 싶어질 수 있습니다. 그런 경우 이전에 제안된 조건부 인덱스를 사용할 수 없기 때문에 새로운 필터링에 맞는 조건부 인덱스를 생성하거나 더 일반화된 구성으로 인덱스를 대체해야 합니다.
참고: 동일한 테이블 및 동일한 열에 여러 개의 인덱스를 갖는 것은 데이터 작성 시 성능 병목이 될 수 있습니다.
다음과 같은 인덱스를 고려해 봅시다 (피하세요):
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_count
가 0
인 첫 번째 항목을 찾아야 합니다.
다음과 같은 인덱스 정의는 each_batch
와 잘 작동하지 않습니다 (피하세요).
CREATE INDEX index_on_users_never_logged_in ON users (sign_in_count)
each_batch
는 id
열을 기반으로 범위 쿼리를 구축하기 때문에 이 인덱스를 효율적으로 사용할 수 없습니다. DB는 테이블의 행을 읽거나 기본 키 인덱스도 읽는 바이트맵 검색을 사용합니다.
“느린” 반복
“느린” 반복은 우리가 좋은 인덱스 구성을 사용하여 테이블을 반복하고 생성된 관계에 필터링을 적용하는 것을 의미합니다.
User.each_batch(of: 5) do |relation|
relation.where(sign_in_count: 0).each { |user| puts user inspect }
end
이 반복은 이미 제한된(범위) id
에 필터(sign_in_count: 0
)가 적용되어 있기 때문에 주요 키 인덱스(id
열)를 사용합니다. 행 수가 제한됩니다.
느린 반복은 일반적으로 완료하는 데 더 많은 시간이 걸립니다. 반복 횟수가 많아지고 하나의 반복이 배치 크기보다 적은 레코드를 생성할 수도 있습니다. 때로는 반복이 0개의 레코드를 생성할 수도 있습니다. 이는 최적의 솔루션이 아니지만, 일부 경우(특히 대규모 테이블을 다룰 때)에는 유일한 선택사항일 수 있습니다.
서브쿼리 사용
each_batch
쿼리에서 서브쿼리를 사용하는 것은 대부분의 경우 잘 작동하지 않습니다. 다음 예제를 고려해보세요.
projects = Project.where(creator_id: Issue.where(confidential: true).select(:author_id))
projects.each_batch do |relation|
# 무언가 수행
end
이 반복은 projects
테이블의 id
열을 사용합니다. 배치 처리는 서브쿼리에 영향을 주지 않습니다.
즉, 각 반복마다 서브쿼리가 데이터베이스에서 실행됩니다. 이로 인해 쿼리에 일정한 “부하”가 추가되어
대부분의 경우 명령 시간 초과로 이어집니다. 우리는 알려지지 않은 수의 기밀 사항이 있으며,
실행 시간 및 액세스하는 데이터베이스 행은 issues
테이블의 데이터 분포에 따라 달라집니다.
참고: 서브쿼리는 서브쿼리가 작은 수의 행을 반환할 때에만 작동합니다.
서브쿼리 개선
서브쿼리를 다룰 때, 느린 반복 접근 방식이 동작할 수 있습니다: 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
테이블이 먼저 배치 처리되는 것이 합리적일 것입니다.
JOIN
및 EXISTS
사용
JOIN
을 사용해야 하는 경우:
- 테이블 간에 1:1 또는 1:N 관계가 있고 결합된 레코드가 (거의) 항상 존재한다는 것을 알고 있는 경우. 이는 “확장 같은” 테이블에 잘 작동합니다:
-
projects
-project_settings
-
users
-user_details
-
users
-user_statuses
-
- 이 경우
LEFT JOIN
이 잘 작동합니다. 결합된 테이블의 조건은 생성된 relation에 있어야 하므로 반복이 결합된 테이블의 데이터 분포에 영향을 받지 않습니다.
예시:
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
사용의 단점은 생성된 relation 객체에서 추가적인 count 쿼리가 실행된다는 것입니다.
each_batch_count
메서드는 불필요한 count 쿼리를 제거하여 더 효율적으로 레코드를 계산하는 방법을 제공합니다. 이 메서드를 호출함으로써, 반복 프로세스를 일시 중지하고 필요에 따라 재개할 수 있습니다. 이 기능은 특히 Sidekiq 워커 내에서 5분 후에 오류 예산 위반이 트리거되는 상황과 같이 도움이 됩니다.
예를 들어, EachBatch
를 사용하여 레코드를 계산하려면 다음과 같이 추가적인 count 쿼리를 호출해야 합니다:
count = 0
Issue.each_batch do |relation|
count += relation.count
end
puts count
반면, each_batch_count
메서드는 불필요한 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
의 성능을 개선하기 위한 대부분의 팁과 제안은 EachBatch
에도 적용됩니다.
키팅 페이지네이션을 사용하여 반복
EachBatch
로는 작동하지 않는 특수한 경우가 몇 가지 있습니다. EachBatch
는 하나의 구분되는 열(일반적으로 기본 키)을 필요로 하기 때문에 타임스탬프 열 및 복합 기본 키가 있는 테이블에 대한 반복을 불가능하게 합니다.
EachBatch
가 작동하지 않는 경우, 키팅 페이지네이션을 사용하여 테이블이나 일련의 행을 반복할 수 있습니다. 확장 및 성능 특성은 EachBatch
와 매우 유사합니다.
예시:
- 특정 순서(타임스탬프 열)로 테이블 반복 및 정렬 기준 열에 고유한 값이 포함되어 있지 않은 경우 tie-breaker와 함께
- 복합 기본 키가 있는 테이블 반복
프로젝트별 이슈 반복(생성 날짜별)
특정 순서(예: created_at DESC
)로 데이터베이스 열을 반복하기 위해 키팅 페이지네이션을 사용할 수 있습니다. created_at
에 대해 동일한 값의 반환된 레코드의 일관된 순서를 보장하기 위해 고유한 값을 가진 tie-breaker 열을 사용할 수 있습니다(예: 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 is the tie-breaker
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 is the tie-breaker
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 is the tie-breaker
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
참고:
반복을 안정적이고 예측 가능하게 유지하려면 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,
distinct: false,
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'relative_order',
order_expression: MergeRequestDiffCommit.arel_table[:relative_order].asc,
nullable: :not_nullable,
distinct: false,
)
])
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
절에 있는 열을 설명해야 합니다(두 번째 예시).
키팅 페이지네이션 라이브러리의 ORDER BY
구성을 자동으로 결정할 수 없는 경우에는 오류가 발생합니다.
Gitlab::Pagination::Keyset::Order
및 Gitlab::Pagination::Keyset::ColumnOrderDefinition
클래스의 코드 주석을 통해 ORDER BY
절을 구성하는 가능한 옵션에 대한 개요를 찾을 수 있습니다. 또한 키팅 페이지네이션 설명서에 몇 가지 코드 예제를 찾을 수 있습니다.