페이지가 로드될 때마다 데이터베이스의 관련 레코드를 계산하는 대신 ActiveRecord의 카운터 캐싱 기능을 사용하면 카운터를 저장하고 연결된 개체가 생성되거나 제거될 때마다 카운터를 업데이트할 수 있습니다. AppSignal Academy의 이번 에피소드에서는 ActiveRecord의 캐싱 카운터에 대한 모든 것을 배울 것입니다.
기사와 응답이 있는 블로그의 고전적인 예를 들어 보겠습니다. 각 기사에는 응답이 있을 수 있으며 블로그 색인 페이지의 각 기사 제목 옆에 응답 수를 표시하여 인기도를 보여주고자 합니다.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
# ...
end
인덱스 페이지에 데이터를 표시하지 않기 때문에 응답을 미리 로드할 필요가 없습니다. 카운터를 표시하고 있으므로 각 기사에 대한 응답 수에만 관심이 있습니다. 컨트롤러는 모든 기사를 찾아 @articles
에 배치합니다. 사용할 보기에 대한 변수입니다.
<!-- app/views/articles/index.html.erb -->
<h1>Articles</h1>
<% @articles.each do |article| %>
<article>
<h1><%= article.title %></h1>
<p><%= article.description %></p>
<%= article.responses.size %> responses
</article>
<% end %>
보기는 각 기사를 반복하고 제목, 설명 및 수신된 응답 수를 렌더링합니다. article.responses.size
를 호출하기 때문에 보기에서 ActiveRecord는 각 응답에 대한 전체 레코드를 로드하는 대신 연결을 계산해야 한다는 것을 알고 있습니다.
팁 :#count
지만 응답 수를 계산하기 위한 더 직관적인 선택처럼 들리지만 이 예에서는 #size
를 사용합니다. , #count
항상 COUNT
를 수행합니다. 쿼리, 동안 #size
응답이 이미 로드된 경우 쿼리를 건너뜁니다.
Started GET "/articles" for 127.0.0.1 at 2018-06-14 16:25:36 +0200
Processing by ArticlesController#index as HTML
Rendering articles/index.html.erb within layouts/application
Article Load (0.2ms) SELECT "articles".* FROM "articles"
↳ app/views/articles/index.html.erb:3
(0.2ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 2]]
↳ app/views/articles/index.html.erb:7
(0.3ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 3]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 4]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 5]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 6]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 7]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 8]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 9]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 10]]
↳ app/views/articles/index.html.erb:7
(0.1ms) SELECT COUNT(*) FROM "responses" WHERE "responses"."article_id" = ? [["article_id", 11]]
↳ app/views/articles/index.html.erb:7
Rendered articles/index.html.erb within layouts/application (23.1ms)
Completed 200 OK in 52ms (Views: 45.7ms | ActiveRecord: 1.6ms)
ActiveRecord가 별도의 쿼리에서 각 기사에 대한 응답 수를 지연 로드하므로 블로그의 색인을 요청하면 N+1개의 쿼리가 발생합니다.
COUNT()
사용 쿼리에서
기사당 추가 쿼리 실행을 피하기 위해 기사와 응답 테이블을 함께 결합하여 단일 쿼리에서 연관된 응답을 계산할 수 있습니다.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.
joins(:responses).
select("articles.*", 'COUNT("responses.id") AS responses_count').
group('articles.id')
end
# ...
end
이 예에서는 기사 쿼리의 응답을 결합하고 COUNT("responses.id")
를 선택합니다. 응답 수를 계산합니다. 제품 ID별로 그룹화하여 기사당 응답 수를 계산합니다. 보기에서 responses_count
를 사용해야 합니다. size
를 호출하는 대신 응답 협회에서.
이 솔루션은 첫 번째 쿼리를 더 느리고 더 복잡하게 만들어 추가 쿼리를 방지합니다. 이것은 이 페이지의 성능을 최적화하는 좋은 첫 번째 단계이지만 한 단계 더 나아가 카운터를 캐시할 수 있으므로 모든 페이지 보기에서 각 응답을 계산할 필요가 없습니다.
카운터 캐시
블로그의 기사가 업데이트되는 것보다 더 자주 읽히기 때문에 카운터 캐시 이 페이지를 더 빠르고 간단하게 쿼리할 수 있는 좋은 최적화입니다.
기사가 표시될 때마다 응답 수를 계산하는 대신 카운터 캐시는 각 기사의 데이터베이스 행에 저장된 별도의 응답 카운터를 유지합니다. 응답이 추가되거나 제거될 때마다 카운터가 업데이트됩니다.
이렇게 하면 쿼리에서 응답을 조인할 필요 없이 기사 인덱스를 하나의 데이터베이스 쿼리로 렌더링할 수 있습니다. 설정하려면 belongs_to
에서 스위치를 뒤집으세요. counter_cache
를 설정하여 관계 옵션.
# app/models/response.rb
class Response
belongs_to :article, counter_cache: true
end
Article
에 대한 필드가 필요합니다. responses_count
라는 모델 . counter_cache
옵션은 응답이 추가되거나 제거될 때마다 해당 필드의 숫자가 자동으로 업데이트되도록 합니다.
팁 :true
대신 기호를 사용하여 필드 이름을 재정의할 수 있습니다. counter_cache
의 값으로 옵션.
카운트를 저장하기 위해 데이터베이스에 새 열을 만듭니다.
$ rails generate migration AddResponsesCountToArticles responses_count:integer
invoke active_record
create db/migrate/20180618093257_add_responses_count_to_articles.rb
$ rake db:migrate
== 20180618093257 AddResponsesCountToArticles: migrating ======================
-- add_column(:articles, :responses_count, :integer)
-> 0.0016s
== 20180618093257 AddResponsesCountToArticles: migrated (0.0017s) =============
이제 응답 수가 기사 테이블에 캐시되므로 기사 쿼리에서 응답을 조인할 필요가 없습니다. Article.all
을 사용하겠습니다. 컨트롤러의 모든 기사를 가져옵니다.
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@articles = Article.all
end
# ...
end
Rails는 #size
에 대한 카운터 캐시를 사용하는 것으로 이해하므로 보기를 변경할 필요가 없습니다. 방법.
<!-- app/views/articles/index.html.erb -->
<h1>Articles</h1>
<% @articles.each do |article| %>
<article>
<h1><%= article.title %></h1>
<p><%= article.description %></p>
<%= article.responses.size %> responses
</article>
<% end %>
인덱스를 다시 요청하면 하나의 쿼리가 실행되는 것을 볼 수 있습니다. 각 기사는 응답 수를 알고 있기 때문에 응답 테이블을 전혀 쿼리할 필요가 없습니다.
Started GET "/articles" for 127.0.0.1 at 2018-06-14 17:15:23 +0200
Processing by ArticlesController#index as HTML
Rendering articles/index.html.erb within layouts/application
Article Load (0.2ms) SELECT "articles".* FROM "articles"
↳ app/views/articles/index.html.erb:3
Rendered articles/index.html.erb within layouts/application (3.5ms)
Completed 200 OK in 42ms (Views: 36.5ms | ActiveRecord: 0.2ms)
범위 연결에 대한 카운터 캐시
ActiveRecord의 카운터 캐시 콜백은 레코드를 생성하거나 삭제할 때만 실행되므로 범위가 지정된 연결에 카운터 캐시를 추가하면 작동하지 않습니다. *게시된* 응답 수만 계산하는 것과 같은 고급 사례의 경우 counter_culture gem을 확인하세요.
카운터 캐시 채우기
카운터 캐시보다 앞선 기사의 경우 카운터는 기본적으로 0이므로 동기화되지 않습니다. .reset_counters
를 사용하여 객체에 대한 카운터를 "재설정"할 수 있습니다. 메서드를 사용하고 개체의 ID와 카운터를 업데이트해야 하는 관계를 전달합니다.
Article.reset_counters(article.id, :responses)
배포 시 프로덕션에서 실행되도록 하기 위해 마지막 마이그레이션에서 열을 추가한 직후 실행되는 마이그레이션에 넣습니다.
$ rails generate migration PopulateArticleResponsesCount --force
invoke active_record
create db/migrate/20180618093443_populate_article_responses_count.rb
마이그레이션에서는 Article.reset_counters
를 호출합니다. 각 기사에 대해 기사 ID 및 :responses
전달 협회 이름으로.
# db/migrate/20180618093443_populate_article_responses_count.rb
class PopulateArticleResponsesCount < ActiveRecord::Migration[5.2]
def up
Article.find_each do |article|
Article.reset_counters(article.id, :responses)
end
end
end
이 마이그레이션은 카운터 캐시 이전에 존재했던 문서를 포함하여 데이터베이스의 모든 문서에 대한 개수를 업데이트합니다.
콜백
카운터 캐시는 카운터를 업데이트하기 위해 콜백을 사용하기 때문에 SQL 명령을 직접 실행하는 메서드(예:#delete
#destroy
대신 ) 카운터를 업데이트하지 않습니다.
어떤 이유로 그런 일이 발생하는 상황에서는 주기적으로 카운트를 동기화하는 백그라운드 작업이나 Rake 작업을 추가하는 것이 합리적일 수 있습니다.
namespace :counters do
task update: :environment do
Article.find_each do |article|
Article.reset_counters(article.id, :responses)
end
end
end
캐시된 카운터
쿼리에서 연결된 개체를 계산하여 N+1 쿼리를 방지하면 도움이 될 수 있지만 카운터 캐싱은 대부분의 응용 프로그램에 대한 카운터를 표시하는 훨씬 빠른 방법입니다. ActiveRecord의 내장 캐시 카운터는 많은 도움이 될 수 있으며, 보다 정교한 요구 사항에는 counter_culture와 같은 옵션을 사용할 수 있습니다.
ActiveRecord의 카운터 캐시에 대해 질문이 있습니까? 주저하지 말고 @AppSignal로 알려주십시오. 물론 이 기사가 어떻게 마음에 들었는지, 또는 더 알고 싶은 다른 주제가 있는지 알고 싶습니다.