QueryRecorder
QueryRecorder는 테스트에서 N+1 queries problem을 감지하는 도구입니다.
spec/support/query_recorder.rb에 구현되었으며, 9c623e3e를 통해 확인할 수 있습니다.
일반적으로, 머지 리퀘스트는 쿼리 수를 늘리지 않아야 합니다. 만약 N+1
쿼리를 피하기 위해 .includes(:author, :assignee)
와 같은 것을 추가하게 된다면, 이를 테스트로 강제하는 데 QueryRecorder를 사용하는 것을 고려해보세요. 이를 통해 추가된 모델로 인해 문제가 재진입되는 것을 알지 못한 채로 새로운 기능을 확인할 수 있습니다.
작동 방식
이 스타일의 테스트는 ActiveRecord에 의해 실행된 SQL 쿼리의 수를 세어 작동합니다. 먼저 실험을 수행하여 초기 카운트를 얻고, 그런 다음 레코드를 데이터베이스에 추가하고 카운트를 다시 실행합니다. 쿼리 수가 크게 증가했다면 N+1
쿼리 문제가 존재합니다.
it "N+1 데이터베이스 쿼리를 피합니다", :use_sql_query_cache do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { visit_some_page }
create_list(:issue, 5)
expect { visit_some_page }.to issue_same_number_of_queries_as(control)
end
원한다면, 기대값과 컨트롤 모두 QueryRecorder
인스턴스로 만들 수 있습니다.
it "N+1 데이터베이스 쿼리를 피합니다" do
control = ActiveRecord::QueryRecorder.new { visit_some_page }
create_list(:issue, 5)
action = ActiveRecord::QueryRecorder.new { visit_some_page }
expect(action).to issue_same_number_of_queries_as(control)
end
예를 들어, 카운트 사이에 5개의 이슈를 생성할 수 있는데, 이는 N+1 문제가 있는 경우 쿼리 수를 5회 증가시킵니다.
어떤 경우에는, 쿼리 수가 무관한 이유로 실행간에 약간 변경될 수 있습니다. 이 경우, 가능하다면 control_count + acceptable_change
를 테스트하여 값을 테스트해야 합니다. 그러나 가능하다면 이를 피해야 합니다.
만약 이 테스트가 실패했고 컨트롤이 QueryRecorder
로 전달되었다면, 실패 메시지는 추가적인 쿼리의 위치를 찾기 위해 가장 긴 공통 접두어에서 유사한 쿼리를 그룹화하여 나타냅니다.
어떤 경우에는, N+1 스펙은 캐시를 데욜어오기 위한 세 번의 요청을 포함합니다. 캐시를 데욜어오기 위해 추가 요청을 만들기 대신, 두 요청(컨트롤 및 테스트)을 선호하고 N+1 스펙에서 cached queries를 무시하도록 테스트를 구성하세요.
it "N+1 데이터베이스 쿼리를 피합니다" do
# 데욜어
visit_some_page
control = ActiveRecord::QueryRecorder.new(skip_cached: true) { visit_some_page }
create_list(:issue, 5)
expect { visit_some_page }.to issue_same_number_of_queries_as(control)
end
캐시된 쿼리
기본적으로, QueryRecorder는 카운트할 때 cached queries를 무시합니다. 그러나 문제를 마스킹할 수 있는 N+1 쿼리를 도입하지 않도록 하려면 모든 쿼리를 세는 것이 더 좋을 수 있습니다. 이를 위해서는 :use_sql_query_cache
플래그를 설정해야 합니다. 그리고 skip_cached
변수를 QueryRecorder
에 전달하고 issue_same_number_of_queries_as
매처를 사용해야 합니다.
it "N+1 데이터베이스 쿼리를 피합니다", :use_sql_query_cache do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { visit_some_page }
create_list(:issue, 5)
expect { visit_some_page }.to issue_same_number_of_queries_as(control)
end
RequestStore 사용
RequestStore
/ Gitlab::SafeRequestStore
를 사용하여 요청의 기간 동안 데이터를 메모리에 캐시함으로써 N+1 쿼리를 피할 수 있습니다. 그러나 이것은 기본적으로 테스트에서 비활성화되어 있으며 N+1 쿼리를 테스트할 때 거짓 부정 결과를 낼 수 있습니다.
테스트에서 RequestStore
를 활성화하려면 필요할 때 request_store
헬퍼를 사용하세요.
it "N+1 데이터베이스 쿼리를 피합니다", :request_store do
control = ActiveRecord::QueryRecorder.new(skip_cached: true) { visit_some_page }
create_list(:issue, 5)
expect { visit_some_page }.to issue_same_number_of_queries_as(control)
end
컨트롤러 스펙 대신 요청 스펙 사용
컨트롤러 스펙 대신 요청 스펙을 사용하여 컨트롤러 수준에서 N+1 테스트를 작성하세요.
컨트롤러 스펙을 사용하여 N+1 테스트를 작성하지 마세요. 왜냐하면 컨트롤러는 예제 당 한 번만 초기화되기 때문에 후속 “요청”에서 쿼리가 감소할 수 있기 때문에 잘못된 성공으로 이어질 수 있습니다(예: 메모이제이션 때문).
실패한 테스트에 대한 신뢰하지 마세요
N+1 쿼리에 대한 테스트를 추가하기 전에, 먼저 여러분의 변경 없이 테스트가 실패하는지 확인하세요. 왜냐하면 테스트가 망가져 있을 수도 있고, 테스트가 잘못된 이유로 통과할 수 있기 때문입니다.
쿼리의 출처 찾기
쿼리의 출처를 찾는 다양한 방법이 있습니다.
-
QueryRecorder
의data
속성을 검사하세요. 이것은file_name:line_number:method_name
으로 쿼리를 저장합니다. 각 항목은 다음 필드를 갖는 해시입니다:-
count
: 이file_name:line_number:method_name
에서 쿼리가 호출된 횟수 -
occurrences
: 각 호출의 실제SQL
-
backtrace
: 각 호출의 스택 트레이스(두 가지 옵션 중 하나가 활성화된 경우)
QueryRecorder#find_query
를 사용하여file_name:line_number:method_name
및count
속성에 의해 쿼리를 필터링할 수 있습니다. 예를 들어 다음과 같습니다:control = ActiveRecord::QueryRecorder.new(skip_cached: false) { visit_some_page } control.find_query(/.*note.rb.*/, 0, first_only: true)
QueryRecorder#occurrences_by_line_method
은data
에 기반하여count
로 정렬된 배열을 반환합니다. -
-
특정
QueryRecorder
인스턴스에 대한 호출 스택 트레이스를 보려면ActiveRecord::QueryRecorder.new(query_recorder_debug: true)
를 사용하세요. 결과는test.log
파일에 저장됩니다. -
QUERY_RECORDER_DEBUG
환경 변수를 사용하여 모든 테스트에 대한 호출 스택 트레이스를 활성화할 수 있습니다.이를 활성화하려면
QUERY_RECORDER_DEBUG
환경 변수가 설정된 상태에서 스펙을 실행하세요. 예:QUERY_RECORDER_DEBUG=1 bundle exec rspec spec/requests/api/projects_spec.rb
이를 통해 QueryRecorder 호출이
test.log
파일에 기록됩니다. 예를 들어:QueryRecorder SQL: SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 AND ("issues"."state" IN ('opened')) AND "issues"."confidential" = $2 --> /home/user/gitlab/gdk/gitlab/spec/support/query_recorder.rb:19:in `callback' --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:127:in `finish' --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `block in finish' --> ...
출처: source
참고 자료
-
Bullet
N+1
쿼리 문제를 찾기 위한 도구 - 성능 가이드라인
- 병합 요청 성능 가이드라인 - 쿼리 횟수
- 병합 요청 성능 가이드라인 - 캐시된 쿼리
-
RedisCommands::Recorder Redis에서
N+1
호출을 테스트하기 위한 도구