N+1 및 일반 쿼리 최적화

3431 단어 databaserails
나는 이제 성능 쿼리를 작성하는 데 짜증이 난다는 것을 알고 있습니다. 나는 이번 주에 관리자가 내가 푸시한 코드를 로드하는 데 5초가 걸렸고 1000개 이상의 쿼리를 호출했기 때문에 시작했습니다. 1000? 불가능한. 내 코드가 아닙니다. 정말 그랬나요??(스포일러: 그랬어요) 설명하겠습니다.

활성 레코드



Rails의 활성 기록은 볼만한 보석입니다.(말장난 의도)하지만 강력한 힘을 가지고 있습니다... 이 두 코드 스니펫을 고려하십시오.

users = User.all
users.count



users = User.all
users.size


어느 것이 더 빨리 달릴 것 같습니까? 둘 다 미리 로드되어 있으므로 둘 중 하나의 두 번째 줄은 상관없이 일정한 O(1) 시간으로 실행되어야 합니다. 잘못된

Count는 SQL 쿼리를 사용하여 요소 수를 계산하기 때문에 항상 데이터베이스에 대한 쿼리를 트리거합니다(SELECT COUNT(*) FROM...).

반면에 크기는 로드되지 않은 항목에서 호출되는 경우에만 DB에 대한 쿼리를 트리거합니다. 그렇다면 크기는 메모리에서 작동합니다.
여기에서 훌륭한 언급은 길이가 사전 로드된 요소에서 호출되는 경우에만 값을 반환하는 것입니다. 자세히 알아보기here . 이것은 작은 순간이지만 이러한 쿼리를 루프에서 사용하면 합산됩니다.

엔 + 1



이것은 많은 사람들이 저지르는 일반적인 오류입니다. 특히 나 같은 사람들. 데이터를 가져오기 위해 쿼리를 만들려고 하지만 데이터베이스 내의 연결 특성으로 인해 요청이 추가 N 쿼리를 트리거할 때 발생합니다.

이렇게 생각해보세요. 당신은 평생 단 하루도 구운 적이 없지만 오늘은 케이크를 굽기로 결심합니다. 그래서 당신은 온라인에서 당신에게 정규 재료가 필요하다는 것을 알려주는 레시피를 얻습니다. 그래서 당신은 가게에 가서 그것들을 사서 집으로 돌아옵니다. 집에 도착하면 케이크를 굽기 위해 오븐이 필요하다는 것을 깨닫고 가게로 돌아가서 오븐을 사서 돌아옵니다. 그런 다음 베이킹 트레이가 필요하다는 것을 깨닫고 우버로 돌아가서 베이킹 통을 사러갑니다. '첫 번째 여행에 필요한 모든 것을 포함시켰더라면 훨씬 쉬웠을 텐데'라고 스스로에게 말합니다. 이것이 N+1 문제입니다. 당신은 단지 케이크를 굽고 싶었을 뿐인데 결국 굽는 것보다 물건을 가져오는 데 훨씬 더 많은 시간을 소비하게 되었습니다.

그렇다면 상점에 처음 방문했을 때 어떻게 모든 것을 얻을 수 있습니까? 구조에 다시 ActiveRecord.

포함, 사전 로드 및 EagerLoad



여기서는 Active Record Query Interface을 참조하겠습니다.

이 예를 고려하십시오

books = Book.limit(10)

books.each do |book|
  puts book.author.last_name
end


루프 내의 각 책에 대해 연관된 저자를 가져오고 그들의 성을 가져오는 쿼리가 있습니다. 책 10권의 경우 11개의 쿼리입니다. (10 + 1 ... N + 1). 여기서 문제는 이 쿼리의 크기 복잡성 O(N)입니다. 이것이 왜 나쁜가요? 음, 메모리는 생산에서 주요 부동산입니다. 서버가 수백만 개의 레코드를 가져오느라 바쁘다면 다른 사람에게 서비스를 제공하지 않을 가능성이 있습니다.

포함, 사전 로드 및 즉시 로드는 이 문제에 대한 솔루션입니다.
포함 => 활성 레코드는 가능한 최소 수의 쿼리를 사용하여 지정된 모든 연결이 로드되도록 합니다.

books = Book.includes(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end


작성자도 메모리에 로드되었기 때문에 이제 11개가 아닌 2개의 쿼리만 실행됩니다.

사전 로드 => 활성 레코드는 연결당 하나의 쿼리를 사용하여 지정된 각 연결을 로드합니다.

즉시 로드 => 활성 레코드는 LEFT OUTER JOIN을 사용하여 지정된 모든 연결을 로드합니다.

그렇다면 메모리에 모두 로드하지 않는 이유는 무엇입니까?



내가 주목한 것은 메모리에 로드하는 것이 종종 데이터베이스에 핑하는 것보다 더 성능이 좋다는 것입니다. 그러나 다시 메모리에 얼마나 많이 로드하느냐에 대한 문제가 있습니다. 이걸 고려하세요

# we have a million users and limited memory
User.all.each do |user|



이로 인해 모든 사용자를 메모리에 로드하려고 시도하므로 메모리가 매우 빨리 부족해집니다. 특히 연결에 포함하여 로드하는 경우

User.find_each() do |user|


이것은 사용자를 1000개의 배치로 로드하여 메모리의 로드를 줄여 서버에 약간의 여유 공간을 제공하므로 메모리에서 더 쉽습니다.

결론



DB 기술과 관련하여 아직 작업해야 할 것이 많습니다. 내 기술 세트에 이러한 기능을 추가하면 성능이 더 뛰어난 코드를 작성하는 데 도움이 되기를 바랍니다. 쿼리를 더 잘 수행하는 방법에 대한 팁이 있습니까? 아래 댓글에서 반원들과 공유하세요.

좋은 웹페이지 즐겨찾기