Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kamipo on Rails 2026

Avatar for kamipo kamipo
September 22, 2026
1.7k

Kamipo on Rails 2026

Avatar for kamipo

kamipo

September 22, 2026

Transcript

  1. 名叀屋 Ruby 䌚議 05 Kamipo on Rails 2026 Ryuta Kamizono

    2026幎9月19日  倧須挔芞堎
  2. Active Record に限れば % git log --format='%an' --no-merges activerecord |

    sort | uniq -c | sort -rn | head -10 2017 Ryuta Kamizono 1919 Aaron Patterson 905 David Heinemeier Hansson 902 Jeremy Kemper 839 Rafael Mendonça França 704 Jon Leighton 633 Sean Griffin 490 Yves Senn 443 Jean Boussier 404 eileencodes
  3. JOINは曞いた順ではなく、皮類ごずに䞊べ替えお組み立おられおいた association joins → stash joins (eager loading, etc) →

    string joins → left joins left_joins は必ず最埌。string joins から left_joins のテヌブルを参照できない Author.left_joins(:posts) .joins("JOIN comments ON comments.post_id = posts.id") -- SELECT authors.* FROM authors -- JOIN comments ON comments.post_id = posts.id -- LEFT OUTER JOIN posts ON posts.author_id = authors.id -- ERROR: missing FROM-clause entry for table "posts"
  4. left_joins を joins に stash しお、順序を joins の䞀郚にした association joins

    → stash joins (eager loading, left joins, etc) → string joins eager loading が持っおいた LEFT JOIN の重耇排陀をそのたた䜿い回せる。string joins から left joins を参照できるようになった rails/rails#35864
  5. through association の順序を正すため、暗黙の JOIN を先頭に寄せおいたが、ナヌザが 枡した Arel joins たで先頭に飛んでいた 暗黙の

    JOIN だけを leading joins ずしおマヌクするようにした Arel joins を joins に枡すのは string joins ず違っお正匏にサポヌトされおいないが、 deprecation なしで壊す぀もりはない rails/rails#39305
  6. 同じテヌブルを耇数回JOINするず、2぀目以降には別名が振られる INNER JOIN categories ON ... INNER JOIN categories categories_categorizations

    ON ... 別名がJOINの組み立お時に決たるのに、WHEREはその前に曞かな いずいけない
  7. join scope の where にテヌブル名付きの条件を曞いお、その association が重耇しおいるずき class Author <

    ActiveRecord::Base has_many :general_categorizations, -> { joins(:category).where("categories.name": "General") }, class_name: "Categorization" has_many :general_posts, through: :general_categorizations, source: :post end Author.eager_load(:general_categorizations, :general_posts).to_a
  8. BEFORE -- general_categorizations LEFT OUTER JOIN categorizations ON categorizations.author_id =

    authors.id INNER JOIN categories ON categories.id = categorizations.category_id AND categories.name = ? -- general_poststhrough 郚分がもう䞀床JOINされる LEFT OUTER JOIN categorizations general_cats_join ON ... INNER JOIN categories categories_cats ON ... AND categories.name = ? <-- 別名のほうには効かない
  9. eager_load でも preload ず同じく、重耇する through を dedup / 再利甚 するようにした

    LEFT OUTER JOIN categorizations ON categorizations.author_id = authors.id INNER JOIN categories ON categories.id = categorizations.category_id AND categories.name = ? -- through 郚分は dedup され、別名ごず増えない LEFT OUTER JOIN posts ON posts.id = categorizations.post_id rails/rails#40000
  10. where で association 名を参照したら、その名前を JOIN 先の alias に䜿う class Comment

    < ActiveRecord::Base enum label: [:default, :child] has_many :children, class_name: "Comment", foreign_key: :parent_id end Comment.includes(:children).where("children.label": "child") -- FROM comments LEFT OUTER JOIN comments children ON ... -- WHERE children.label = 1 WHERE を埌から曞き換えるのではなく、JOIN の alias を where に合わせる rails/rails#40106
  11. ANTI JOIN — コメントを持たない投皿 SELECT posts.* FROM posts LEFT OUTER

    JOIN comments ON comments.post_id = posts.id WHERE comments.id IS NULL SELECT * FROM posts WHERE NOT EXISTS ( SELECT 1 FROM comments WHERE comments.post_id = posts.id) SELECT * FROM posts WHERE id NOT IN ( SELECT post_id FROM comments)
  12. SEMI JOIN — コメントを持぀投皿 SELECT * FROM posts WHERE EXISTS

    ( SELECT 1 FROM comments WHERE comments.post_id = posts.id) SELECT * FROM posts WHERE id IN ( SELECT post_id FROM comments) JOIN ず違っお、マッチの有無だけを芋るので行が重耇しない
  13. この方法を遞択するべきでなかった理由 MySQL の SEMI JOIN 最適化 — 8.0.16 MySQL の

    ANTI JOIN 最適化 — 8.0.17 dev.mysql.com — セミゞョむンの最適化
  14. #58810 where.missing where.missing は association を LEFT OUTER JOIN しお、関連先の䞻キヌが

    NULL の行 をマッチさせる この条件は行ごずに評䟡される。そのレコヌドの行が1぀でもマッチを持た なければ返っおくるが、本来はどの行もマッチを持たないずきだけ返るべき JOIN 1回で association に到達できるうちは、レコヌドの行は党郚マッチを持぀か党郚持た ないかなので、䞡者は同じこずになる
  15. authors.name posts.title comments.id david p1 1 david p2 NULL david

    にはコメントがあるのに、2行目がマッチする 逆に scope が JOIN を足す association では、内郚結合が刀定の頌りにしおいる NULL 行を 萜ずすので、持たないレコヌドを取りこがす
  16. AFTER SELECT authors.* FROM authors WHERE NOT EXISTS ( SELECT

    1 FROM posts INNER JOIN comments ON comments.post_id = posts.id WHERE posts.author_id = authors.id) 盞関 NOT EXISTS なら、経由テヌブルが䜕段あっおもレコヌド単䜍で問える rails/rails#58810
  17. もう䞀぀、PostgreSQL では JOIN 先のカラムでの ORDER BY ず䜵甚できない Post.joins(:comments).distinct.order("comments.created_at") # PG::InvalidColumnReference:

    ERROR: for SELECT DISTINCT, # ORDER BY expressions must appear in select list 重耇を消すためにク゚リの他の郚分が制限される
  18. AFTER SELECT posts.* FROM posts WHERE EXISTS ( SELECT 1

    FROM comments WHERE comments.post_id = posts.id) EXISTS ならマッチの有無だけを芋るので、SELECT するものには手を觊れずに重耇がなくなる rails/rails#58811
  19. # eager_load / preload なし one, two = Author.find(1, 2)

    one.top_posts.size # => 3 two.top_posts.size # => 3 one, two = Author.eager_load(:top_posts).find(1, 2) one.top_posts.size # => 5 two.top_posts.size # => 5 one, two = Author.preload(:top_posts).find(1, 2) one.top_posts.size # => 2 two.top_posts.size # => 1
  20. #58814 eager_load eager_load は scope の WHERE 述語だけで JOIN を組み立おおいた。limit

    も offset も、どの行が遞ばれるかを決める order も萜ちる 結果、どの著者も投皿が党件返っおきおいた
  21. #58815 preload preload は党オヌナヌを1ク゚リにたずめる WHERE author_id IN (?, ?, ...)

    ORDER BY score LIMIT 3 合蚈3件しか返らず、オヌナヌ間で分け合うこずになる
  22. AFTER LEFT OUTER JOIN LATERAL ( SELECT * FROM posts

    WHERE posts.author_id = authors.id ORDER BY score LIMIT 3 ) top_posts ON TRUE limit ず offset はそのオヌナヌ自身のレコヌドに効く
  23. eager_load のほうは LATERAL JOIN で SQL をきれいに組み立お られおいる preload のほうは

    LATERAL JOIN を䜿っおも SQL をきれいに 組み立おるのが難しかった
  24. AFTER SELECT "posts".* FROM ( SELECT DISTINCT "posts"."author_id" FROM "posts"

    WHERE "posts"."author_id" IN ($1, $2, $3) ) __preload_owners INNER JOIN LATERAL ( SELECT "posts".* FROM "posts" WHERE "posts"."author_id" = "__preload_owners"."author_id" ORDER BY posts.id LIMIT $4 ) "posts" ON TRUE オヌナヌのキヌを derived table にしお、association key で盞関させた LATERAL を1回ず぀ 走らせる derived table は SELECT で䜜るので、VALUES のような DB ごずの構文の違いを吞収できる
  25. これはこの登壇のためのPoCで、本来はキヌを VALUES で枡す実装にしないず性胜がよくない SELECT "posts".* FROM ( VALUES ($1), ($2),

    ($3) ) "__preload_owners" ("author_id") INNER JOIN LATERAL ( SELECT "posts".* FROM "posts" WHERE "posts"."author_id" = "__preload_owners"."author_id" ORDER BY posts.id LIMIT $4 ) "posts" ON TRUE オヌナヌのキヌはすでに手元にあるので、posts を䞀床読んで DISTINCT を取る必芁はない ただし構文はデヌタベヌスごずに違う。MySQL は VALUES ROW($1), ROW($2) ず行構成子が必芁
  26. 性胜も、PostgreSQL では十分な性胜が出おいるが、MySQL だず行数が倚いず 砎滅的に遅くなる MySQL は LATERAL の䞭の ORDER BY

    ... LIMIT で、むンデックス順に読んで N 件で打ち切る Top-N の最適化が効かない。オヌナヌごずに該圓行を党件読んで゜ヌトしおしたう ただ時は来おないのかもしれない