実例で学ぶ、遅いコードを書く方法
遅いコードを書く方法
遅いコードを書く方法を知り、それに類似したコードを書かないことで、
高パフォーマンスなコードを書けるようになることを目的としています。
Ruby(ActiveRecord)を例に説明しますが、他の言語でも同様の考え方が適用できます。
例で使用するER
- **USER(ユーザー)**
- 属性: `id`, `name`, `email`
- 関係:
- **has** 複数の `ORDER`(注文)を所有
- **has** 複数の `PRESENT`(プレゼント)を所有
- **ORDER(注文)**
- 属性: `id`, `created_at`, `status`, `user_id`
- 関係:
- **contains** 1つの `PRODUCT`(製品)を含む
- **PRESENT(プレゼント)**
- 属性: `id`, `used`, `user_id`, `product_id`
- 関係:
- **belongs_to** 1つの `PRODUCT` を所属
- **PRODUCT(製品)**
- 属性: `id`, `name`, `price`
case1: Rubyでフィルタリングをする
orders = user.orders
available_orders = orders.filter { |order| order.status == 'active' }
上記のように、ユーザーが持つオーダーをすべて取得し、Rubyのfilter
メソッドを利用してフィルタリングしています。
answer: SQLでフィルタリング
SQLでフィルタリングを行います。
available_orders = user.orders.where(status: 'active')
例えば、ユーザーが注文を50件持っていて、そのうち有効なものが10件あったとします。
Rubyでフィルタリングをする場合、50件すべてがメモリにロードされますが、SQLでフィルタリングを行う場合は10件のみがデータベースから取得されるため、省メモリであることは明らかです。
any?
を使う
case2: 未使用なプレゼントはあるか?という処理です。関連するプレゼント全てにアクセスしています。
user.presents.any? { |present| present.used == false }
answer: exists?を使う
exists?
を使用します。
user.presents.where(used: false).exists?
exists?
は1件のみを確認するため、より効率的です。
map
で取得する
case3: 関連を user.presents.map(&:product).uniq
このようにすると、プレゼントの数だけ product
への問い合わせが発生(n+1回)するため、パフォーマンスの低下を招きます。
eager_load
または preload
を利用する
answer: user.presents.eager_load(:product).distinct
eager_load
や preload
を使用することで、関連する product
を事前にロードし、N+1問題を回避します。
case4: Rubyで並び替えをする
user.orders.sort_by(&:created_at)
answer: orderを使用する
ActiveRecordの order
メソッドを利用します。
user.orders.order(:created_at)
Rubyで並び替えを行うということは、アプリケーションサーバー上で処理を行うことになります。
一般に、アプリケーションサーバーよりもデータベースサーバーの方がハイスペックであることが多いです。また、サーバー間の通信が発生するため、取得するデータ量を減らすことで通信速度が向上します。
また、この場合はgroup_by
やreverse
にも適用できます。
まとめ
- N+1問題を防ぐために
eager_load
またはpreload
を利用する - データベース側でできることはすべてデータベースで行う
-
map
、filter
、select
、any?
などのイテレータに対してメソッドを呼び出す際、一度where
やeager_load
などで絞り込みができないかを確認する
「N organic」、「FAS」等の化粧品ブランドを展開している株式会社シロクのエンジニアブログです。 ECサイトを中心とした自社サービスの開発・運用を行っています。 sirok.jp/norganic
Discussion