🎃

実例で学ぶ、遅いコードを書く方法

2024/12/26に公開

遅いコードを書く方法

遅いコードを書く方法を知り、それに類似したコードを書かないことで、
高パフォーマンスなコードを書けるようになることを目的としています。
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件のみがデータベースから取得されるため、省メモリであることは明らかです。

case2: any? を使う

未使用なプレゼントはあるか?という処理です。関連するプレゼント全てにアクセスしています。

user.presents.any? { |present| present.used == false }

answer: exists?を使う

exists? を使用します。

user.presents.where(used: false).exists?

exists? は1件のみを確認するため、より効率的です。

case3: 関連を map で取得する

user.presents.map(&:product).uniq

このようにすると、プレゼントの数だけ product への問い合わせが発生(n+1回)するため、パフォーマンスの低下を招きます。

answer: eager_load または preload を利用する

user.presents.eager_load(:product).distinct

eager_loadpreload を使用することで、関連する product を事前にロードし、N+1問題を回避します。

case4: Rubyで並び替えをする

user.orders.sort_by(&:created_at)

answer: orderを使用する

ActiveRecordの order メソッドを利用します。

user.orders.order(:created_at)

Rubyで並び替えを行うということは、アプリケーションサーバー上で処理を行うことになります。

一般に、アプリケーションサーバーよりもデータベースサーバーの方がハイスペックであることが多いです。また、サーバー間の通信が発生するため、取得するデータ量を減らすことで通信速度が向上します。
また、この場合はgroup_byreverseにも適用できます。

まとめ

  • N+1問題を防ぐために eager_load または preload を利用する
  • データベース側でできることはすべてデータベースで行う
  • mapfilterselectany? などのイテレータに対してメソッドを呼び出す際、一度 whereeager_load などで絞り込みができないかを確認する
シロク エンジニアブログ

Discussion