대량 업데이트: 활성 레코드 및 네이티브 SQL
11054 단어 activerecordrubyrailssql
약 150.000개의 기록에서 실행되는 동적 대량 업데이트의 세 가지 다른 방법을 비교했다.
다음 예제에서는 Active Recovery 및 원래 SQL 방식으로 해결된 대량 작업 문제를 복제합니다.나는 세 가지 다른 해결 방안을 소개하고 그것들의 성능을 비교할 것이다.모든 해결 방안에 대해 저는 코드를 공유하고 제 기계(개발 환경)에서 코드를 운행하며 해결 방안마다 약 15만 개의 기록을 성공적으로 갱신하는 데 소요되는 시간을 등록할 것입니다.
그리고 다음db 모델과 모델을 고려해 보겠습니다.
class PlayerGameStatistic < ApplicationRecord
belongs_to :player
belongs_to :team
belongs_to :game
end
class Player < ApplicationRecord
belongs_to :team
has_many :player_game_statistics
enum status: %i[active inactive retired]
end
class Team < ApplicationRecord
belongs_to :team
has_many :player_game_statistics
has_many :games
enum status: %i[inactive active]
end
우리는 세 개의 탁자가 있다. players
, teams
, player_game_statistics
.나는 이 연습과 무관하기 때문에 이곳의 게임스 시계(player_game_statistics
도 이 시계에 속한다)를 배제하고 싶다.그것들은 모두 장기적으로 존재하는 표이다.그것들은 모두 수천 줄을 가지고 있지만, 그 성질로 말하자면,
player_game_statistics
은 더 많은 줄을 싣는다.지금 내가 새로운 열
player_name
에서 player_game_statistics
을 추가했다고 가정해 보자.이 열이 비어 있는 (기본값은 null
) 의 기존 기록에 대해, 나는 name
표의 players
열에서 사용할 수 있는 데이터로 그것을 채우고 싶다.행수를 조금 줄이기 위해 현역과 부상 선수(은퇴 선수 포함)와 현역 팀(선수와 팀 모두 컨디션 속성이 있는 값)의 통계 정보를 갱신하는 데만 흥미를 느낀다.
버전 1: 활동 로깅 방법
첫 번째 방법은 각각
player_game_statistic
을 교체하여 관련 player
열을 얻고 player_name
치를 바탕으로 통계 데이터의 player.name
열을 업데이트하는 것이다.이것은 고전적인
n + 1
조회를 초래할 것이다.우리는 player_game_statistics_to_update
을 불러오고 n
의 추가 조회를 실행하여 모든 유저의 이름을 얻을 것입니다.이것이 바로
:includes
이 통상적으로 나타나는 곳이다.보충해 봅시다.class UpdatePlayerGameStatisticsV1
def execute!
player_game_statistics_to_update.each do |stat|
stat.update!(player_name: stat.player.name)
end
end
private
def user_statistics_to_update
PlayerGameStatistic
.includes(:player)
.joins(:team)
.references(:player)
.where('team.status = 1')
.where('player.status IN (0, 1)')
end
end
:includes
은 :preload
또는 :eager_load
을 사용합니다. 이것은 하위 조회 (예:where 자구) 에 불러오는 관련을 인용하는지 여부에 달려 있습니다.우리의 예에서 우리는 이 점을 하기를 희망한다. 이렇게 하면 우리는 유저 상태에 따라 결과를 필터할 수 있다.이로써 player_game_statistics
에서 단일 조회를 하고 players
에서 왼쪽 외부 연결을 하고 teams
에서 내부 연결을 하게 된다.:eager_load
은 :preload
보다 느리지만 우리가 있는 이곳은 선택이 없고 :includes
을 전혀 사용하지 않는 것보다 낫다.:includes
이 어떻게 작동하는지에 대한 더 자세한 정보는 great article by Julianna Roen on the gusto blog을 사용하는 것을 권장합니다.결과: 약 00:04:33에 약 150.000줄이 업데이트되었습니다.
버전 2: 기억 사전이 있는 활동 기록
내가 흥미를 느끼는 두 번째 방법은 사전 캐시 방법이다. 이런 방법에서 모든 참여자를 조회하고
player_id
과 player_name
을 추출하여 결과를 산열로 바꾸어 기억한다.우리는 여전히 모든 통계 데이터를 교체하지만, 게이머들이 통계 데이터를 통해 게이머를 한 번 조회하게 하는 것은 아니다. 다음 교체는 사전만 조회할 수 있다.버전 1과 비교하면, 이런 방법은 추가 조회를 만들 수 있지만, 나는 그것이 그렇게 무겁지 않을 것이라고 가정한다. 왜냐하면 내가 사용하는 것은
:pluck
이기 때문이다.class UpdatePlayerGameStatisticsV2
def execute!
player_game_statistics_to_update.each do |stat|
stat.update!(player_name: player_name_idx[stat.player_id])
end
end
private
def player_game_statistics_to_update
PlayerGameStatistic.where(player_id: player_name_idx.keys)
end
def player_name_idx
@player_name_idx ||=
Player
.joins(:team)
.where('teams.status = 1')
.where("players.status IN (0,1)")
.pluck('players.id, players.name')
.to_h
end
end
결과: 약 00:04:58에 약 150.000줄이 업데이트되어 버전 1과 관련이 없습니다.버전 3: 원본 SQL
마지막 방법은 원본 SQL을 사용하는 것입니다.Postgres UPDATE 문을 사용하여 단일 질의에서 통계 정보를 업데이트합니다.
class UpdatePlayerGameStatisticsV3
def execute!
ActiveRecord::Base.connection.execute(Arel.sql(update_sql))
end
private
def update_sql
<<-SQL
UPDATE player_game_statistics AS stats
SET player_name = players.name,
updated_at = LOCALTIMESTAMP
FROM (#{players_sql}) AS players
WHERE stats.player_id = players.id
SQL
end
def players_sql
<<-SQL
SELECT players.id, players.name
FROM players
LEFT OUTER JOIN teams ON teams.id = players.team_id
WHERE teams.status = 1 AND players.status IN (0,1)
SQL
end
end
결과: 약 00:00:11에 약 15만 줄이 업데이트되었다.세 가지 버전 모두에 대한 벤치마크 테스트:
다시 한 번 실시간성을 보면 v1이 v2보다 조금 좋은 것 같다. 비록 차이가 그리 크지 않을 수도 있지만.그러나 v3는 다른 버전의 약 28배 속도가 높기 때문에 여기서 뚜렷한 이긴 사람이다.
[#<Benchmark::Tms:0x00007fe48268deb8
@cstime=0.0,
@cutime=0.0,
@label="v1",
@real=309.4179189999704,
@stime=15.808668,
@total=241.84425000000002,
@utime=226.035582>,
#<Benchmark::Tms:0x00007fe47ee8eda0
@cstime=0.0,
@cutime=0.0,
@label="v2",
@real=341.4523780000163,
@stime=14.207616000000002,
@total=231.90784299999999,
@utime=217.70022699999998>,
#<Benchmark::Tms:0x00007fe48073ef08
@cstime=0.0,
@cutime=0.0,
@label="v3",
@real=12.004358999955002,
@stime=0.001827999999996166,
@total=0.003549999999968634,
@utime=0.0017219999999724678>]
결론:
다른 개발자들이 어떻게 Rails 방식으로 이 문제들을 해결하는지 다른 활동 기록 솔루션을 탐색할 수 있었는데, 이것은 매우 재미있을 것이다.하지만 지금까지 Active Record는 복잡하고 데이터의 양이 많은 대량 업데이트/삽입에서 성능 병목을 보여 왔습니다.이것이 바로 SQL 파이가 쓸모가 있는 곳이다.
비록 기억 사전 (v2) 의 성능이 전체 활동 기록 방식 (v1) 보다 좋지는 않지만, 이것은 다른 상황에서 성능 향상에 도움이 되지 않는다는 것을 의미하지는 않는다.나는 자주 이런 상황을 만나는데, 그것은 확실히 매우 도움이 된다.
마지막으로 나는 서로 다른 문제/조회에 대해 이런 분석을 하는 것이 매우 중요하다는 것을 발견했다.모든 사례는 하나의 사례다.나의 경험에 따르면 SQL은 결국 대량의 데이터와/또는 복잡한 업데이트를 얻었다.
성능은 내가 가장 좋아하는 화제 중의 하나이다. 나는 여기, 나의 사이트와 다른 사이트에서 성능에 관한 글을 더 많이 쓸 것이다.나는 다른 사람과 성능 문제와 해결 방안을 토론하는 것을 매우 기쁘게 생각하므로 언제든지 방문하거나 연락 주십시오.
Reference
이 문제에 관하여(대량 업데이트: 활성 레코드 및 네이티브 SQL), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/anakbns/bulk-update-active-record-vs-raw-sql-a-performance-analysis-291m텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)