「勘で改善するな、まず計測せよ」──SQLチューニングで40秒が1秒になった奮闘記──

こんにちは。入社2年目でSEをやっているT・Sです。

ECサイトで改修をした際に、「カート処理が異常に遅い」という報告が舞い込んできました。 通常なら1秒程度の処理が、特定の条件下で40秒以上もかかってしまう。ECサイトにとって、これは致命的な問題です。

この記事では、その問題をどう分析し、どう解決したのか。その一連のプロセスを、ストーリー形式でサンプルコード比喩を交えながらご紹介します。

単なるテクニックの羅列ではなく、「勘で改善するな、まず計測せよ」という原則に基づいた、パフォーマンスチューニングの思考プロセスをお伝えできればと思います。

1. ある日突然、カート処理が激遅に

問題が発覚したのは、改修作業後のことでした。

お客様が複数の商品をカートに入れ、在庫確認や価格計算を行う際に、画面が固まったようになってしまう。モニタリングツールを確認すると、特定のSQLクエリが20秒以上かかっていることが判明しました。

改修前はなかったはずの問題

興味深いことに、この現象は改修前には起きていませんでした。

改修の結果、在庫テーブルの役割が増え、扱うレコード数が大幅に増加しました。それ以降、処理時間が急増したのです。

つまり、SQL自体は以前から非効率な書き方だったものの、データ量が少なかったために問題が表面化していませんでした。今回の改修によるデータ量の増加が、この“隠れた問題”を炙り出したのです。

2. 実行計画はデータベースの業務日誌

次にすべきことは「推測」ではなく「計測」です。ここで登場するのが「実行計画」です。

実行計画とは、SQLクエリがデータベースエンジンによって実際にどのような手順で処理されるかを示した情報です。

SQLクエリを渡すと、データベースはそれを受け取って「どの順番で、どの方法で処理するか」のプランを作成してくれます。

私はよく、この実行計画を「データベースの業務日誌」と呼んでいます。

SQL Serverの場合、Management Studioで簡単に確認できます。

  • クエリウィンドウで Ctrl + L:推定実行計画を表示
  • クエリウィンドウで Ctrl + M:「実際の実行プランを含める」をONにして実行

実行計画は、各処理がアイコンで表示され、全体の処理のうち、どこに時間がかかっているか(コスト)が一目瞭然になります。

(↑このような図で、処理の流れとコストが可視化されます)

今回の問題のSQLの実行計画を見てみると、全体のコストのほとんどが 、1つの処理に集中していました。これが今回の“犯人”です。

3. 原因分析:相関サブクエリという「繰り返し電話」

犯人は「相関サブクエリ」でした。実行計画上では「Nested Loop(入れ子ループ)」として現れます。

問題のクエリは、カート内の各商品について、在庫情報を取得するような処理でした。

-- 問題のクエリ(簡略版)
SELECT 
    c.cart_item_id,
    c.product_id,
    -- ここが問題!カートの商品1行ごとにサブクエリが実行される
    (
     SELECT SUM(stock_quantity) 
     FROM inventory i 
     WHERE i.product_id = c.product_id
    ) AS total_stock
FROM cart_items c
WHERE c.user_id = @user_id;

このクエリは、カート内の商品1つ1つに対して、括弧内のサブクエリ(在庫確認)を実行します。カートに10商品あれば10回、100商品あれば100回です。

これは、会社の業務に例えると「繰り返し電話」をしているようなものです。

1つ目の商品について、在庫管理部に電話:「商品Aの在庫合計を教えてください」
在庫管理部が計算して回答
2つ目の商品について、また在庫管理部に電話:「商品Bの在庫合計を教えてください」
在庫管理部が計算して回答
...これをカートの商品数だけ繰り返す...

カートに100個の商品があれば、100回も電話をかけているのです。これでは遅くなるのも当然です。実行計画の「Nested Loop」は、まさにこの非効率な動きを図で示してくれていました。

4. 解決策:アプローチの変更で劇的改善

原因が分かれば、解決策も見えてきます。

解決策:JOINへの書き換えで「まとめて処理」
相関サブクエリの「繰り返し電話」問題を解決する鍵は、処理のやり方を根本的に変えることです。
つまり、1行ずつ在庫を確認するのではなく、「先に全商品の在庫をまとめて集計し、その結果をカート情報と結合(JOIN)する」というアプローチに切り替えます。

このような複数ステップの処理を分かりやすく記述するために、CTE(Common Table Expression)、通称WITH句が役立ちます。

先ほどの電話の例で言えば、こういうことです。

最初に在庫管理部に「全商品の在庫リストを作って共有してください」と一度だけ依頼します。 あとはその共有リストを参照するだけです。 100回の電話が、たった1回の電話に変わるわけです。

WITH ProductTotalStock AS (
    SELECT 
        product_id,
        SUM(stock_quantity) AS total_stock
    FROM inventory
    GROUP BY product_id
)
-- ステップ2:集計済みCTEとJOINするだけ
SELECT 
    c.cart_item_id,
    c.product_id,
    pts.total_stock
FROM cart_items c
LEFT JOIN ProductTotalStock pts ON c.product_id = pts.product_id
WHERE c.user_id = @user_id;

このアプローチにより、非効率な「Nested Loop」が消え、効率的な「Merge Join」に変わりました。

5. 結果:24秒が5秒に!劇的なパフォーマンス改善

実際の環境では最大で40秒以上かかっていましたが、今回記事用に用意した検証環境では、改善前のクエリで24秒の実行時間がかかりました。 これまでに登場した業務日誌もこの環境で作ったものです。

観点 Before(改善前) After(改善後)
実行時間 24秒 5秒
実行計画 Nested Loop Merge Join

この改善によってカート処理は快適になり、良い影響を与えることができました。

6. まとめ:パフォーマンスチューニングの思考プロセス

今回の経験から学んだ、SQLチューニングの思考プロセスは以下の通りです。

フェーズ アクション 具体例
問題発見 遅延の特定 実行ログやモニタリングツールで遅いSQLを特定
分析 実行計画の確認 EXPLAINやCtrl+Mでコストの高い処理を調査
仮説・立案 無駄を減らす設計 「繰り返し電話」→まとめて処理、「行ったり来たり」→一度に取得
実行 SQL・インデックス修正 相関サブクエリをCTE+JOINに
検証 再計測 実行時間や論理読み取りが改善されたことを確認

パフォーマンス問題に直面したとき、私たちはつい「このパターンはあれが原因だろう」と推測したくなります。しかし、推測は外れることもあります。

「勘で改善するな、まず計測せよ。」

実行計画という「業務日誌」を見れば、データベースが実際にどう動いているかという「証拠」が手に入ります。それこそが、確実な改善への第一歩なのです。

もしあなたのSQLが「なぜか遅い」と感じたら、まずは実行計画を開いてみてください。きっと、そこに解決の糸口が書かれています。

お知らせ

ecbeingではAIに関する記事も公開しております!

ecbeingでは新進気鋭のエンジニアを募集しております!