N + 1 쿼리에 대한 또 다른 게시물

11995 단어 sqlrailsbeginners
N + 1 쿼리 문제는 ActiveRecord와 같은 ORM (Object-Relational Mapping) 도구를 사용하여 작성된 쿼리에서 발견되는 일반적인 성능 병목 현상입니다.

모든 관련 레코드를 가져오기 위해 만들어진 첫 번째 쿼리 외에도 각 연결에 대해 N개의 쿼리를 실행해야 할 때 문제가 발생합니다.

예를 들어 UserAppointment 모델 간에 다음과 같은 관계가 있는 경우

# app/models/user.rb
class User < ApplicationRecord
 has_many :appointments
end



# app/models/appointment.rb
class Appointment < ApplicationRecord
 belongs_to :user
end


코드에서 아래 쿼리를 실행했습니다.

Appointment.all.limit(5).each do |appointment|
 puts "#{appointment.user.name} has an appointment at #{appointment.date}"
end


이것이 레일 콘솔에서 볼 수 있는 것입니다.

Appointment Load (0.5ms)  SELECT "appointments".* FROM "appointments" LIMIT ?  [["LIMIT", 5]]
 User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
 User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
 User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
 User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 7], ["LIMIT", 1]]
 User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 9], ["LIMIT", 1]]


위의 쿼리는 무해한 것처럼 보이지만 1,000개의 약속을 로드해야 한다고 상상해 보십시오! 데이터베이스에 1번(모든 약속을 잡기 위해) 더하기 1,000번(각 약속의 사용자를 잡기 위해), 총 1,001개의 쿼리가 필요합니다. 기술적인 측면에서 결과는 느리게 로드되고 있습니다. 즉, appointment.user.name 에서 요청한 대로 "N"개의 연속 쿼리가 수행됩니다.

독립 데이터베이스 쿼리 수를 줄이는 방법은 무엇입니까?



이에 대한 가능한 솔루션은 .includes 를 사용하는 것입니다. 그러면 쿼리 결과를 즉시 로드합니다. 즉, 관련 연결(상위 및 하위)이 단 몇 개의 쿼리를 통해 한 번에 모두 로드됩니다.

예를 들어 콘솔에서 다음 코드를 실행하면

Appointment.includes(:user).limit(5).each do |appointment|
 puts "#{appointment.user.name} has an appointment at #{appointment.date}"
end


이것은 우리가 돌려받을 것입니다:

Appointment Load (0.2ms)  SELECT "appointments".* FROM "appointments" LIMIT ?  [["LIMIT", 5]]
 User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)  [["id", 1], ["id", 3], ["id", 5], ["id", 7], ["id", 9]]


위의 결과에서 볼 수 있듯이 결과가 느리게 로드될 때 6개 대신 데이터베이스에 대해 2개의 쿼리만 실행됩니다.

데이터로 백업!



이 시점에서 "알겠습니다. 충분히 이야기했습니다! .includes를 사용하면 실제로 쿼리 성능이 향상된다는 것을 보여주는 데이터는 어디에 있습니까?"라고 궁금해할 수 있습니다.

100명의 사용자 약속을 지연 로드하려고 시도하면 밀리초 단위로 아래와 같은 결과를 얻습니다.

user     system      total        real
0.101984   0.096377   0.198361 (  0.761556)


반면에 동일한 100개의 레코드를 즉시 로드하려고 하면 실행 시간이 빨라집니다.

user     system      total        real
0.026250   0.010747   0.036997 (  0.078877)


코드를 실행하는 데 소요된 시간(사용자)과 커널(시스템)에서 소요된 시간을 더한 총 시간을 고려하면 레코드를 즉시 로드하는 속도가 5.36배 빨라집니다!

Ruby의 벤치마크 모듈here에 대해 자세히 알아볼 수 있습니다.

애플리케이션에서 N + 1 쿼리를 식별하는 방법



애플리케이션에서 속도가 느려지는 모든 N + 1 쿼리를 찾기 위해 커뮤니티에서는 Bullet gem 을 사용할 것을 권장합니다.

참조



  • Faster Rails: Eliminating N+1 queries
  • Benchmarking Ruby Code

  • 좋은 웹페이지 즐겨찾기