일괄로 테이블 반복하기
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
위 쿼리는 프로젝트 생성자를 반복하고 중복을 제왕치 않고 출력합니다.
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
쿼리에서 데이터의 읽기만 (인덱스만의 스캔(INDEX ONLY SCAN)) 하고 테이블에는 접근하지 않습니다. 데이터베이스 인덱스는 정렬되어 있기 때문에 첫 번째 항목을 가져오는 것은 매우 저렴한 작업입니다.
다음 단계는 일괄 크기 구성을 따르는 다음 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
행의 위치를 얻기 위해 인덱스를 조사하고 행을 읽습니다. 반환된 배열이 루비에서 처리됩니다.
첫 반복이 완료되었습니다. 다음 반복에서는 마지막 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
값을 위해 필터링을 해야하는 경우, 이전에 제안된 조건부 인덱스를 사용할 수 없는 경우가 있습니다. 새로운 쿼리에 대한 WHERE
조건이 새 필터와 일치하지 않기 때문입니다.
이 문제를 해결하기 위해 두 가지 옵션이 있습니다:
- 새로운 쿼리를 커버하기 위해 다른 조건부 인덱스를 생성합니다.
- 보다 일반적인 구성으로 인덱스를 교체합니다.
다음과 같은 인덱스(피함)를 고려해봅시다:
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
인 첫 번째 항목을 찾아야 합니다.
우리는 인덱스 정의에서 열을 교환함으로써 쿼리를 크게 개선할 수 있습니다 (선호).
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_batch
는 id
열을 기반으로 범위 쿼리를 작성하기 때문에 이 인덱스를 효과적으로 사용할 수 없습니다. 데이터베이스는 테이블의 행을 읽거나 기본 키 인덱스도 읽는 바이너리 검색을 사용해야 합니다.
“느린” 반복
느린 반복이란 좋은 인덱스 구성을 사용하여 테이블을 반복하고 생성된 관계에 대해 필터링을 적용하는 것을 의미합니다.
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
테이블의 데이터 분포에 의존합니다.
서브쿼리 개선
서브쿼리를 다룰 때 느린 반복 접근 방식을 사용할 수 있습니다: 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
이 잘 작동합니다. 결합된 테이블의 조건은 생성된 관계에 주입되어 결합된 테이블의 데이터 분포에 영향을 받지 않습니다.
예시:
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
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::Order
및 Gitlab::Pagination::Keyset::ColumnOrderDefinition
클래스의 코드 주석은 ORDER BY
절을 구성하는 데 사용할 수 있는 옵션에 대한 개요를 제공합니다. 또한 키셋 페이지네이션 문서에 몇 가지 코드 예제를 찾을 수 있습니다.