599 Views
September 27, 24
スライド概要
『Tsukulink Tech Talk 〜Ruby on Rails トライアル&エラー〜 #2』
https://tsukulink.connpass.com/event/329274/
がんばります
⾒えないモノを⾒ようとして SQLを覗き込んだ #つくてくトーク ツクリンク株式会社 あっきー(@kuronekopunk)
⾃⼰紹介 ● あっきー @kuronekopunk ● ツクリンク株式会社 ● 経歴 ○ ○ エンジニアリングマネージャー 2011年 新卒 SESでPHP 2012年 ㈱ハンズシェア創業(現:ツクリンク㈱) ■ ツクリンクの0→1の開発 ■ PHP→Railsのリプレース ■ SEO、グロースハック ■ データアナリスト ■ DevRel ● イベント『EMゆるミートアップ』『カクカクシカジカモデリング』 ● ポッドキャスト『ぐんぐんfm』
ActiveRecordが発⾏するSQL ちゃんと⾒てますか?
find @user = User.find(1) SELECT <?> FROM "users" <?>
find @user = User.find(1) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
present? データの有無で分岐するケース。 ※user has_many posts @user = User.find(1) if @user.posts.present? # ←ここ puts "postあります!" end SELECT <?> FROM "posts" <?>
present? @user = User.find(1) if @user.posts.present? # ←ここ puts "postあります!" end SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 postsを全て取得している 参考リンク: present? , blank?
exists? データの有無で分岐するケース、exists?で存在確認 @user = User.find(1) if @user.posts.exists? # ←ここ puts "postあります!" end SELECT <?> FROM "posts" <?>
exists? @user = User.find(1) if @user.posts.exists? # ←ここ puts "postあります!" end SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1 limit 1を指定、SELECT句でもレコードのカラムを取得せず軽量なクエリ 参考リンク: exists?
present? と exists? どちらを使うのが良いでしょう?
present? vs exists? 実⾏時間を確認してみる @user = User.find(1) @posts = @user.posts @posts.present? @posts.exists?
present? vs exists? 実⾏時間を確認してみる # present? Post Load (1069.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 # exists? Post Exists? (0.6ms) SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1 ※user_id=1のpostsは20万件あります
present? vs exists? 全件取るケースってほぼないですよね。limitを追加してみる @user = User.find(1) @posts = @user.posts.limit(10) @posts.present? @posts.exists?
present? vs exists? limit追加 # present? Post Load (0.6ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 10 # exists? Post Exists? (0.4ms) SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1 present?は0.5~1.1ms exists?の勝利? exists?は0.3~0.5msあたりに落ち着いた
present? vs exists? ユースケースによって変わる postsの有無を確認するがpostsを参照しない場合 @user = User.find(1) @posts = @user.posts.limit(10) if @posts.present? # or exists? # postsを参照しない処理 puts "postあります!" end
present? vs exists? ユースケースによって変わる postsの有無を確認するがpostsを参照しない場合 # present? User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 Post Load (0.6ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 10 Completed 200 OK in 20ms (Views: 3.7ms | ActiveRecord: 0.9ms (2 queries, 0 cached) | GC: 0.0ms) # exists? User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 Post Exists? (0.3ms) SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1 Completed 200 OK in 12ms (Views: 5.7ms | ActiveRecord: 0.6ms (2 queries, 0 cached) | GC: 0.0ms) 誤差程度ではあるもののexists?の⽅が良さそう?
present? vs exists? ユースケースによって変わる postsを参照する場合 @user = User.find(1) @posts = @user.posts.limit(10) if @posts.present? # or exists? # postsを参照する処理 @posts.each { |post| puts post.id } end
present? vs exists? ユースケースによって変わる postsを参照する場合 # present? => 前回と変わらず User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 Post Load (0.6ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 10 Completed 200 OK in 20ms (Views: 3.7ms | ActiveRecord: 0.9ms (2 queries, 0 cached) | GC: 0.0ms) # exists? => 1クエリ増えてる User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 Post Exists? (0.3ms) SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 1 Post Load (0.5ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 10 Completed 200 OK in 12ms (Views: 5.7ms | ActiveRecord: 1.1ms (3 queries, 0 cached) | GC: 0.0ms) exists?の場合、1クエリ増えてしまった
present? vs exists? ユースケースによって変わる postsを参照するためLoadも発⾏されている 1 @user = User.find(1) # SQL1 2 @posts = @user.posts.limit(10) 3 if @posts.exists? # SQL2 4 5 @posts.each { |post| puts post.id } # SQL3 end # exists? => 1クエリ増えてる 1 User Load (0.3ms) SELECT "users".* 2 Post Exists? (0.3ms) SELECT 1 AS on →存在チェック後にデータを参照する場合はpresent?が妥当 3 Post Load (0.5ms) SELECT "posts".* Completed 200 OK in 12ms (Views: 5.7m
present? vs exists? ユースケースによって変わる 特定のデータがある場合に何かしたいケース 1 @user = User.find(1) 2 @posts = @user.posts.limit(10) 3 puts "id:3がある" if @posts.exists?(id: 3) 4 if @posts.present? 5 6 @posts.each { |post| puts post.id } end
present? vs exists? ユースケースによって変わる 特定のデータがある場合に何かしたいケース 1 User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 2 Post Exists? (0.3ms) SELECT 1 AS one FROM "posts" WHERE "posts"."user_id" = 1 AND "posts"."id" = 3 LIMIT 1 3 Post Load (0.5ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 10 exists?で1クエリ増えた。 Post Loadでデータ⾃体は持っているので再利⽤してExistsを消してみる @user = User.find(1) # SQL1 @posts = @user.posts.limit(10) # 特定のデータがある場合に何かしたいケース puts "id:3がある" if @posts.exists?(id: 3) # SQL2 if @posts.present? # SQL3 @posts.each { |post| puts post.id }
present? vs exists? ユースケースによって変わる Existsを消す⽅法を考える 1 @user = User.find(1) # SQL1 2 @posts = @user.posts.limit(10) 3 puts "id:3がある" if @posts.any?{ |p| p.id == 3 } # SQL2 4 if @posts.present? 5 @posts.each { |post| puts post.id } 1 User Load (0.3ms) SELECT "users".* FROM "users" 6 end 2 Post Load (0.5ms) SELECT "posts".* FROM "posts" exists?からany?に切り替えることでPost Load1個にできた。 SQLは軽くなるが件数や処理の内容次第ではメモリが⼤きく消費される可能性もある 状況に応じてexists?を使う
firstで1件⽬だけ先に参照するケース 1件⽬のデータのみ先になにか使う場合 1 @user = User.find(1) 2 @posts = @user.posts.limit(10) 3 puts @posts.first.id 4 @posts.each { |post| puts post.id }
firstで1件⽬だけ先に参照するケース 1件⽬のデータのみ先になにか使う場合 1 @user = User.find(1) 2 @posts = @user.posts.limit(10) 3 puts @posts.first.id 4 @posts.each { |post| puts post.id } 1 User Load SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 2 Post Load SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 ORDER BY "posts"."id" ASC LIMIT 1 3 Post Load SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 LIMIT 10 1件⽬を取得するSQLが発⾏されてしまった
firstで1件⽬だけ先に参照するケース findの中で呼ばれているメソッド 読み込み済み(loaded?)かで挙動が変わる 読み込まれていなければクエリが発⾏されてしまうみたい 参考リンク: find_nth_with_limit
firstで1件⽬だけ先に参照するケース firstは読み込み済み(loaded?)ならクエリが発⾏されないので先に@postsを取得する 1 @user = User.find(1) 2 @posts = @user.posts.limit(10) 3 puts @posts.to_a.first.id # to_aでpostsを取得 →変換 ※recordsでも可能 4 @posts.each { |post| puts post.id } 1 @user = User.find(1) 2 @posts = @user.posts.limit(10) 3 @posts.each { |post| puts post.id } # firstより先に postsへの参照 4 puts @posts.first.id 1 User Load SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1 2 Post Load SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 LIMIT
コードを追って気になった点 first以外にもsecond, third,,,fifthまである 参考リンク: second
コードを追って気になった点 exists?はlimitが0ならクエリを発⾏せずfalseを返す 参考リンク: exists?
コードを追って気になった点 limit(0)を実⾏してみた exists?ではクエリ発⾏されない、普通に取得すると発⾏された sample-app(dev)> Post.limit(0).exists? => false sample-app(dev)> Post.limit(0) Post Load (0.7ms) => [] SELECT "posts".* FROM "posts" LIMIT $1 [["LIMIT", 0]]
さいごに ● 発⾏されるSQLを最適化しよう ● ActiveRecordなど触っているライブラリのコードを読んでみよう
Thanks. 🙌