Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

いまさらサブクエリ

233 views

Published on

Otemachi.rb #7で発表した資料です。
ActiveRecordにおけるサブクエリの書き方について紹介しています。

Published in: Engineering
  • Be the first to comment

  • Be the first to like this

いまさらサブクエリ

  1. 1. いまさらサブクエリ 2018.06.12 Otemachi.rb #7 Yuya Taki
  2. 2. self.inspect [1] https://github.com/muramurasan/okuribito [2] http://poject.herokuapp.com/ ➢ gemのロゴを書いたり… ➢ たまにQiitaの記事を書いたり… ➢ 新卒で某SIerにてスマートメータのヘッドエ ンドシステムの開発に携わったあと、渋谷の ベンチャー企業にてRuby on Railsを学び、 2016年10月エネチェンジに入社。 Name : Yuya Taki GitHub : yuyasat Qiita : yuyasat [1] 若輩者ですので、何卒優しくご教授いた だければと思います。 (commitはしていない) ➢ ○よ○よ風ゲームをReact.jsで実装し たり [2]
  3. 3. 今回のモデル 0 * 0 * id | name ----+------------ 1 | 多部未華子 2 | 佐津川愛美 3 | 新垣結衣 4 | 堀北真希 5 | 吉高由里子 6 | 悠城早矢 id | actress_id | year | title ----+------------+------+--------------------- 1 | 2 | 2005 | 蝉しぐれ 2 | 1 | 2006 | 夜のピクニック 3 | 4 | 2005 | ALWAYS 三丁目の夕日 4 | 2 | 2012 | 忍道-SHINOBIDO- 5 | 2 | 2016 | 貞子vs伽椰子 6 | 4 | 2013 | 県庁おもてなし課 7 | 5 | 2013 | 真夏の方程式 id | movie_id | key ----+----------+------------ 1 | 1 | 時代劇 2 | 1 | 子役 3 | 3 | 昭和 4 | 5 | ホラー 5 | 7 | ミステリー 6 | 7 | 夏 7 | 6 | 公務員 8 | 6 | 地方活性 9 | 1 | 夏 class Actress < ApplicationRecord has_many :movies has_many :tags, through: :movies end class Movie < ApplicationRecord has_many :tags belongs_to :actress end class Tag < ApplicationRecord belongs_to :movie end actresses movies tags
  4. 4. サブクエリとは ➢ クエリ内のクエリ SELECT "movies".* FROM "movies" WHERE "movies"."actress_id" IN ( SELECT "actresses"."id" FROM "actresses" WHERE "actresses"."name" = '堀北真希' ) ; 0 * 0 * id | actress_id | year | title ----+------------+------+--------------------- 3 | 4 | 2005 | ALWAYS 三丁目の夕日 6 | 4 | 2013 | 県庁おもてなし課
  5. 5. moviesに紐づけられているactressを取得したい ➢ moviesテーブルと内部結合すればいい Actress.joins(:movies) SELECT "actresses".* FROM "actresses" INNER JOIN "movies" ON "movies"."actress_id" = "actresses"."id" ; id | name ----+------------ 2 | 佐津川愛美 1 | 多部未華子 4 | 堀北真希 2 | 佐津川愛美 2 | 佐津川愛美 4 | 堀北真希 5 | 吉高由里子 0 * 0 * actresses movies
  6. 6. 重複を排除するためにdistinctする SELECT DISTINCT "actresses".* FROM "actresses" INNER JOIN "movies" ON "movies"."actress_id" = "actresses"."id" ; id | name ----+------------ 2 | 佐津川愛美 1 | 多部未華子 4 | 堀北真希 5 | 吉高由里子 0 * 0 * Actress.joins(:movies).distinct
  7. 7. よく使いそうなのでscopeにする class Actress < ApplictionRecord scope :having_movies, -> { joins(:movies).distinct } end 0 * 0 *
  8. 8. scope内でjoinはあまり使いたくない Tag.joins(movie: :actress).merge(Actress.having_movies) SELECT DISTINCT "tags".* FROM "tags" INNER JOIN "movies" ON "movies"."id" = "tags"."movie_id" INNER JOIN "actresses" ON "actresses"."id" = "movies"."actress_id" INNER JOIN "movies" ON "movies"."actress_id" = "actresses"."id" ; ERROR: table name "movies" specified more than once 0 * 0 * movieに紐づいているactressに紐づいているmovieのtagを取得したい(かなり作為的。例がよくない)
  9. 9. 回避策(サブクエリ) SELECT "tags".* FROM "tags" INNER JOIN "movies" ON "movies"."id" = "tags"."movie_id" INNER JOIN "actresses" ON "actresses"."id" = "movies"."actress_id" WHERE "actresses"."id" IN ( SELECT DISTINCT "actresses"."id" FROM "actresses" INNER JOIN "movies" ON "movies"."actress_id" = "actresses"."id" ) ; Tag.joins(movie: :actress).where(actresses: { id: Actress.having_movies }) 0 * 0 * ➢ 突然のテーブル名 ○ モデルで記述したい ➢ サブクエリの中でもJOIN ➢ なんか匂う
  10. 10. やっぱりmergeを使いたい Tag.joins(movie: :actress).merge(Actress.having_movies) SELECT "tags".* FROM "tags" INNER JOIN "movies" ON "movies"."id" = "tags"."movie_id" INNER JOIN "actresses" ON "actresses"."id" = "movies"."actress_id" WHERE "actresses"."id" IN ( SELECT DISTINCT "movies"."actress_id" FROM "movies" ) ; ➢ 実行したいSQLはこれ class Actress < ApplictionRecord scope :having_movies, -> { where(id: Movie.select("actress_id").distinct) } end 0 * 0 *
  11. 11. 実行コスト(EXPLAIN) class Actress < ApplictionRecord scope :having_movies, -> {  joins(:movies).distinct } end Tag.joins(movie: :actress).where(actresses: { id: Actress.having_movies }) class Actress < ApplictionRecord scope :having_movies, -> { where(id: Movie.select("actress_id").distinct) } end Tag.joins(movie: :actress).merge(Actress.having_movies) class Actress < ApplictionRecord scope :having_movies, -> { where(id: Movie.select("actress_id")) } end Tag.joins(movie: :actress).merge(Actress.having_movies) 171.22..215.55 90.27..115.51 93.93..122.11 0 * 0 *
  12. 12. (補足)もう少し現実的な例 SELECT DISTINCT "tags".* FROM "tags" INNER JOIN "movies" ON "movies"."id" = "tags"."movie_id" INNER JOIN "actresses" ON "actresses"."id" = "movies"."actress_id" INNER JOIN "actress_song_relationships" ON "actress_song_relationships"."actress_id" = "actresses"."id" INNER JOIN "songs" ON "songs"."id" = "actress_song_relationships"."song_id" INNER JOIN "songs" ON "songs"."id" = "movies"."theme_song_id" WHERE "songs"."name" = '世界が終わる夜に ' ORDER BY "tags"."id" ; 「世界が終わる夜に」がすきな曲としてもつ女優が主演かつ、主題歌が「かざぐる ま」の映画のタグを取得したい。 ERROR: table name "songs" specified more than once Tag.joins(movie: :actress).merge( Actress.joins(:favorite_songs).merge(Song.where(name: '世界が終わる夜に ')).distinct ).merge( Movie.joins(:theme_song).merge(Song.where(name: 'かざぐるま ')).distinct ) 例が非現実的だっ たので ここをscopeにしたとする
  13. 13. (補足)回避策:サブクエリ SELECT DISTINCT "tags".* FROM "tags" INNER JOIN "movies" ON "movies"."id" = "tags"."movie_id" INNER JOIN "actresses" ON "actresses"."id" = "movies"."actress_id" INNER JOIN "Actress_song_relationships" ON "actress_song_relationships"."actress_id" = "actresses"."id" INNER JOIN "songs" ON "songs"."id" = "actress_song_relationships"."song_id" WHERE "songs"."name" = '世界が終わる夜に ' AND "movies"."theme_song_id" IN ( SELECT "songs"."id" FROM "songs" WHERE "songs"."name" = 'かざぐるま ' ) Tag.joins(movie: :actress).merge( Actress.joins(:favorite_songs).merge(Song.where(name: '世界が終わる夜に ')).distinct ).merge( Movie.where(theme_song: Song.where(name: 'かざぐるま ')) ) 「世界が終わる夜に」がすきな曲としてもつ女優が主演かつ、主題歌が「かざぐる ま」の映画のタグを取得したい。 片方をサブクエリにする
  14. 14. もう少し複雑なサブクエリの例 ➢ 映画公開年の降順にActressを取り出したい id | actress_id | year | title ----+------------+------+--------------------- 1 | 2 | 2005 | 蝉しぐれ 2 | 1 | 2006 | 夜のピクニック 3 | 4 | 2005 | ALWAYS 三丁目の夕日 4 | 2 | 2012 | 忍道-SHINOBIDO- 5 | 2 | 2016 | 貞子vs伽椰子 6 | 4 | 2013 | 県庁おもてなし課 7 | 5 | 2013 | 真夏の方程式 id | name ----+------------ 2 | 佐津川愛美 4 | 堀北真希 5 | 吉高由里子 1 | 多部未華子 0 * 0 * id | name ----+------------ 1 | 多部未華子 2 | 佐津川愛美 3 | 新垣結衣 4 | 堀北真希 5 | 吉高由里子 6 | 悠城早矢 actresses movies
  15. 15. SQLで記述 SELECT actresses.* FROM actresses INNER JOIN ( SELECT "movies"."actress_id" AS actress_id, MAX(year) AS max_year FROM "movies" GROUP BY "movies"."actress_id" ) AS movies_max_year ON actresses.id = movies_max_year.actress_id ORDER BY max_year DESC, actresses.id ASC actress_id | max_year ------------+---------- 5 | 2013 4 | 2013 2 | 2016 1 | 2006 id | name ----+------------ 1 | 多部未華子 2 | 佐津川愛美 3 | 新垣結衣 4 | 堀北真希 5 | 吉高由里子 6 | 悠城早矢 id | name ----+------------ 2 | 佐津川愛美 4 | 堀北真希 5 | 吉高由里子 1 | 多部未華子 0 * 0 * actressesmovies_max_year
  16. 16. ActiveRecordで記述(find_by_sql) Actress.find_by_sql(%| SELECT actresses.* FROM actresses INNER JOIN ( SELECT "movies"."actress_id" AS actress_id, MAX(year) AS max_year FROM "movies" GROUP BY "movies"."actress_id" ) AS movies_max_year ON actresses.id = movies_max_year.actress_id ORDER BY max_year DESC, actresses.id ASC |) ➢ Arrayで出力 ➢ Kaminariが使えない ○ Kaminari.paginate_arrayを使えばArrayでも使える が、scopeで使いたい。 [#<Actress:0x007f8978525928 id: 2, name: "佐津川愛美 ">, #<Actress:0x007f8972e37968 id: 4, name: "堀北真希">, #<Actress:0x007f897851d0c0 id: 5, name: "吉高由里子 ">, #<Actress:0x007f897850fd58 id: 1, name: "多部未華子 ">] 0 * 0 *
  17. 17. ActiveRecordで記述(Arelを使う) SELECT actresses.* FROM actresses INNER JOIN ( SELECT "movies"."actress_id" AS actress_id, MAX(year) AS max_year FROM "movies" GROUP BY "movies"."actress_id" ) AS movies_max_year ON actresses.id = movies_max_year.actress_id ORDER BY max_year DESC, actresses.id ASC actress_at = Actress.arel_table movie_at = Movie.arel_table movies_max_year = movie_at.project( Arel.sql(%| "movies"."actress_id" as actress_id, MAX(year) as max_year |) ).group("movies.actress_id").as("movies_max_year") join_conds = actress_at.join( movies_max_year, Arel::Nodes::InnerJoin ).on( movies_max_year[:actress_id].eq(actress_at[:id]) ).join_sources Actress.joins(join_conds).order("movies_max_year.max_year DESC") 0 * 0 *
  18. 18. ActiveRecordで記述(joinsの中にSQL直書き) SELECT actresses.* FROM actresses INNER JOIN ( SELECT "movies"."actress_id" AS actress_id, MAX(year) AS max_year FROM "movies" GROUP BY "movies"."actress_id" ) AS movies_max_year ON actresses.id = movies_max_year.actress_id ORDER BY max_year DESC, actresses.id ASC Actress.joins(%| INNER JOIN ( SELECT "movies"."actress_id" AS actress_id, MAX(year) AS max_year FROM "movies" GROUP BY "movies"."actress_id" ) AS movies_max_year ON actresses.id = movies_max_year.actress_id |).order("max_year desc, actresses.id asc") 0 * 0 *
  19. 19. scopeにして class Actress < ApplicationRecord scope :order_by_movie_year, -> { actress_at = arel_table movie_at = Movie.arel_table movies_max_year = movie_at.project( Arel.sql(%| "movies"."actress_id" as actress_id,       MAX(year) as max_year |) ).group("movies.actress_id").as("movies_max_year") join_conds = actress_at.join( movies_max_year,     Arel::Nodes::InnerJoin ).on( movies_max_year[:actress_id].eq(actress_at[:id]) ).join_sources joins(join_conds).order("movies_max_year.max_year DESC") } end pry> Actress.order_by_movie_year.class => Actress::ActiveRecord_Relation scope :order_by_movie_year, -> { joins(%| INNER JOIN ( SELECT "movies"."actress_id" AS actress_id, MAX(year) AS max_year FROM "movies" GROUP BY "movies"."actress_id" ) AS movies_max_year ON actresses.id = movies_max_year.actress_id |).order("max_year DESC, actresses.id ASC") } 0 * 0 *
  20. 20. まとめ ➢ サブクエリを使ったほうが内部結合するよりもうまくscopeを定義できることが ある ➢ where句でサブクエリを使うときは、 selectを使う ○ pluckを用いるとクエリが2つに ○ SQLのパフォーマンス次第では二つに分ける のもアリ。 ➢ 結合先にサブクエリを用いる場合 ○ Arelを使う ○ joinsの中にSQLを直書きする Actress.where(id: Movie.select("actress_id").distinct) Actress.where(id: Movie.pluck(:actress_id).uniq) 「ActiveRecord サブクエリ」で検索するとArelの例が出てくるけど joinsの中にSQLを書けば良いんじゃないかな。。
  21. 21. 資料 達人に学ぶDB設計 徹底指南書 Visual Representation of SQL Joins ActiveRecordでサブクエリのJOIN Arel でサブクエリ ActiveRecordでサブクエリ(副問い合わせ)と内部結合
  22. 22. 余談
  23. 23. ご静聴ありがとうございました。

×