Webサービスの動作が重くてお困りではありませんか?LaravelとMySQLの最適化によりAPIレスポンス時間を大幅短縮する実践的な手法をご紹介します。
こんな悩みありませんか?
「Webサービスの動作が重くて、ユーザーから苦情が来ている...」 「データが増えるにつれて、管理画面の表示が遅くなってきた...」 「APIのレスポンスが遅くて、フロントエンドの動作に影響している...」
横浜のWeb制作会社「Fivenine Design」では、これまで20年以上にわたり多くのWebサービスのパフォーマンス改善を手がけてきました。特にLaravelとMySQLを使った案件では、適切な最適化により処理速度を3倍以上改善できるケースがほとんどです。
今回は、実際のプロジェクトで効果を実証した最適化手法を、中級レベルの技術者向けに詳しく解説します。
実案件での課題:ECサイトのレスポンス遅延
ある中小企業のECサイトで、こんな問題が発生していました:
- 商品一覧APIのレスポンス時間:3.2秒
- 注文履歴取得:2.8秒
- 管理画面での売上データ表示:5秒以上
商品数が1万点を超えた頃から急激に遅くなり、「買い物カゴに入れるボタンを押しても反応が遅い」というユーザーからの声が増えていました。売上にも影響が出始めていた状況です。
原因調査で見つかった3つの問題
詳細な調査を行った結果、以下の問題が判明しました:
- N+1クエリ問題:商品一覧で各商品のカテゴリ情報を個別取得
- 非効率なJOIN処理:複数テーブルを結合する際のインデックス不備
- 不適切なページング:全件数取得のためのCOUNT処理
最適化手法1:Eloquentクエリの改善
N+1問題の解決
改善前のコード(問題あり):
// 商品一覧取得
$products = Product::paginate(20);
foreach ($products as $product) {
// 各商品ごとにクエリが実行される(N+1問題)
echo $product->category->name;
echo $product->reviews->count();
}
改善後のコード:
// Eager Loadingで一度にデータを取得
$products = Product::with(['category', 'reviews'])
->select(['id', 'name', 'price', 'category_id'])
->paginate(20);
foreach ($products as $product) {
echo $product->category->name;
echo $product->reviews->count();
}
この改善により、商品一覧のクエリ実行回数が21回から3回に削減されました。
集計クエリの最適化
改善前:
// 各商品のレビュー数を個別に取得
$products = Product::all();
foreach ($products as $product) {
$product->review_count = $product->reviews()->count();
}
改善後:
// withCount()で一度に集計
$products = Product::withCount('reviews')
->orderBy('reviews_count', 'desc')
->paginate(20);
最適化手法2:データベースインデックスの追加
複合インデックスの戦略的配置
商品検索でよく使われるクエリパターンを分析し、以下のインデックスを追加しました:
-- 商品検索用の複合インデックス
ALTER TABLE products
ADD INDEX idx_search (category_id, status, created_at);
-- 注文履歴取得用
ALTER TABLE orders
ADD INDEX idx_user_orders (user_id, created_at DESC);
-- レビュー集計用
ALTER TABLE reviews
ADD INDEX idx_product_rating (product_id, rating);
インデックスの効果測定
MySQLのEXPLAINを使って、クエリ実行計画を確認:
EXPLAIN SELECT * FROM products
WHERE category_id = 1
AND status = 'active'
ORDER BY created_at DESC
LIMIT 20;
インデックス追加後、rowsの値が10,000から20に削減され、大幅な改善が確認できました。
最適化手法3:キャッシュ戦略の導入
Redisを活用したクエリキャッシュ
use Illuminate\Support\Facades\Cache;
class ProductService
{
public function getPopularProducts($categoryId = null)
{
$cacheKey = "popular_products_{$categoryId}";
return Cache::remember($cacheKey, 3600, function () use ($categoryId) {
return Product::withCount('reviews')
->when($categoryId, function ($query, $categoryId) {
return $query->where('category_id', $categoryId);
})
->orderBy('reviews_count', 'desc')
->limit(10)
->get();
});
}
}
適切なキャッシュ無効化
// 商品更新時にキャッシュをクリア
class ProductObserver
{
public function updated(Product $product)
{
Cache::forget("popular_products_{$product->category_id}");
Cache::forget('popular_products_');
}
}
実装時の注意点とよくある失敗
1. 過度なEager Loadingは逆効果
「とりあえず全部withしておけば安心」と考えがちですが、不要なデータまで取得してしまうと、むしろパフォーマンスが悪化します。
避けるべき例:
// 必要以上にデータを取得してしまう
$products = Product::with([
'category', 'reviews', 'images', 'variants', 'tags'
])->get();
2. インデックスの重複と肥大化
似たようなインデックスを複数作成すると、INSERT/UPDATE処理が遅くなります。定期的な見直しが重要です。
3. キャッシュの適切な有効期限設定
データの更新頻度に応じて、キャッシュ時間を調整する必要があります:
- 商品マスタ:1時間
- 在庫情報:5分
- リアルタイム性が重要なデータ:キャッシュなし
最適化後の結果
3つの手法を組み合わせた結果、以下の改善を達成しました:
- 商品一覧API:3.2秒 → 0.8秒(4倍改善)
- 注文履歴取得:2.8秒 → 0.6秒(4.7倍改善)
- 管理画面表示:5秒 → 1.2秒(4.2倍改善)
クライアントからは「お客様から『サイトが軽くなった』との声をいただくようになった」「管理業務の効率が大幅に向上した」と喜びの声をいただいています。
まず始めるべき3つのアクション
- 現状分析:LaravelのQuery Logを有効にして、遅いクエリを特定する
- N+1問題の確認:Laravel Debugbarでクエリ実行回数をチェック
- 基本的なインデックス追加:WHERE句でよく使うカラムにインデックスを設定
// config/logging.phpでSQLログを有効化
'channels' => [
'sql' => [
'driver' => 'daily',
'path' => storage_path('logs/sql.log'),
],
],
パフォーマンス改善は、ユーザー体験の向上だけでなく、売上やコンバージョン率の改善に直結します。技術的な課題でお困りの際は、20年以上の実績を持つFivenine Designにお気軽にご相談ください。お客様のWebサービスを次のレベルへと導きます。