0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

業務でRAGチャットボット開発をしていますが、常駐先の社員の方が、約20テーブル、現時点で50万行のビューを作成して、それに対してLIKE句やWHERE句を用いて検索するシステム案になりそうだと相談を受け、初心者の私は、重い処理がえぐい数発生するけど、パフォーマンス大丈夫か!?と心配になり、基本的な事を調べてみようと思ったのが、この記事のきっかけです。

対象者

  • 未来の自分
  • 駆け出しエンジニアの同志
  • DBのパフォーマンスってSQLクエリの書き方一つだろ!と過去の私のような方

使用ツール

SQL Server(SSMS 21)

1. 私の勘違い

SQLクエリをチューニングする際、以下のように「JOINする前にサブクエリで絞る」書き方と、「JOINした後にWHERE句で絞る」書き方をしたとします。

-- パターン1:JOIN後にWHERE句で絞る
SELECT *
FROM tableA
JOIN tableB ON tableB.id = tableA.id
WHERE tableA.Name = 'TEST';

-- パターン2:サブクエリを使って先に絞ってからJOINする
SELECT *
FROM (SELECT * FROM tableA WHERE Name = 'TEST') tableA_filtered
JOIN tableB ON tableB.id = tableA_filtered.id;

無知な私は、先に絞った方て件数を少なくしてからJOINした方が速いだろ!!と思っていました。

しかし、この2つはDB側(オプティマイザ)が自動で実行順序を最適化してくれるため、SQLの書き方を工夫しただけではパフォーマンスが改善しないことがよくあります(ヒント句などで強制的に実行計画を作れば別ですが、基本的にはそこまでしない想定です)。

では、どのようにしてDBのボトルネックを探し、対処していけばいいのか?
その具体的な手順を見ていきます。

2. 実行計画を出力する方法

  1. SSMS(SQL Server Management Studio)の【新しいクエリ】で検証したいクエリを作成します。
  2. 上のツールバーにある 【推定実行プランの表示 (Ctrl + L)】 または、【実際の実行プランを含める (Ctrl + M)】 を選択してからクエリを実行します。
  3. 実行するとツリーが表示されます。これは 右から左に向かって実行された順 に並んでいます。

チューニング前後の比較用に、表示されたツリーをスクリーンショットで残しておくことをお勧めします。
併せて、メッセージタブにある「経過時間」もチェックしておくと、本当に速くなったのか定量的に測れます。

時間が出力されない場合は、クエリの先頭に以下のコードを追加してください。

SET STATISTICS TIME ON; -- 実行時間を表示
SET STATISTICS IO ON;   -- I/O統計を表示(超重要)

3. 実行計画の読み取り方

Point0. コストが高い所を見つける

実行計画のツリーを見ると、各処理が全体の処理コストの中で「何%」の割合を占めているかが分かります。
基本的には コストが高い(%が大きい)= ボトルネックになっている と判断して調査を進めます。

Point1. I/O統計情報を確認する

SET STATISTICS IO ON; を有効にして実行すると、メッセージタブに以下のような情報が出力されます。

テーブル 'pig'。スキャン数 1、論理読み取り数 2040、物理読み取り数 0、(以下省略)
テーブル 'dog'。スキャン数 1、論理読み取り数 456、物理読み取り数 0、(以下省略)
テーブル 'cat'。スキャン数 1、論理読み取り数 1874、物理読み取り数 0、(以下省略)

SQL Server 実行時間: 
CPU 時間 = 94 ミリ秒、経過時間 = 90 ミリ秒。

ここでは以下の3つの情報を読み取ります。

  1. 論理読み取り(Logical Reads):メモリ上のデータページを何ページ読み取ったか。この数値が大きいテーブルがボトルネックの可能性大です。
  2. 物理読み取り(Physical Reads):0であれば、データが全てメモリに載っているためディスクI/Oは発生していません。それでも処理時間が掛かっている場合は、処理ロジック自体が重い(Index ScanやHash Joinなど)と判断できます。
  3. 経過時間:純粋なクエリの実行時間です。

これらから、どのテーブルの読み取りに負荷がかかっているかを特定します。

Point2. テーブル読み取り方法の確認

実行計画のアイコンを見て、テーブルの読み取りが Index Seek になっているか確認します。

  • Index Seek:インデックスを利用して、ピンポイントで必要なデータだけを読み取っている理想的な状態です。
  • Index Scan / Table Scan:データを全件スキャンして読み込んでいる状態です。

WHERE句で条件を指定しているにも関わらず、Index ScanやTable Scanになっている場合は、全件読み込みながら対象を探しているためコストが高くなります。

Point3. JOINの種類(Nested Loop vs Hash Join)

JOINの結合アルゴリズムには、主に Nested Loop, Hash Join, Merge Join があります。

例えば、1回目の結合時にWHERE句で10件程度に絞り込まれたデータを、50万行のテーブルと結合する場合は Nested Loop が適しています。
逆に、数十万件同士の大きなデータを結合する場合は Hash Join が選ばれることが多いです。これはロジックの違いによるものです。

Nested Loop と Hash Join の違い

ざっくりとイメージで表現すると以下のようになります。

  • Nested Loop:件数が多いテーブル(50万行)のインデックスを使って、絞った10件を1件ずつ探しにいく。
  • Hash Join:行数が少ない方のテーブル(Build側)を一度全部読み込んで「ハッシュテーブル(辞書)」を作った後、もう片方のテーブル(Probe側)を上から一行ずつハッシュ化しながら辞書と突き合わせていく。インデックスは使わない。

【疑問】データ量が多くても、結局全件探すという過程は一緒なら、インデックスを使ってNested Loopで探した方が速いのでは?

私は学習中、ここで一番引っかかりました。この違いの本質は 「データへのアクセスパターン」 にあります。

  • Nested Loop:インデックスを利用するため ランダムI/O が発生します。つまり、インデックスがあるので目的地は分かっていますが、目的地が(A → Z → F ・・・)のようにばらばらになるので、データを読み込むまでに待ち時間が発生します。
  • Hash Join:テーブルを上から順番に読み込むため シーケンシャルI/O になります。また、一度ハッシュ辞書を作ってしまえば、メモリ上での高速な突き合わせ処理だけで済みます。

つまり、件数が少ないうちは「あちこち取りに行く(Nested Loop)」方が速いですが、件数が多くなると移動のロス(ランダムI/Oのコスト)が膨大になるため、「最初に上から順番に読んで辞書を作る(Hash Join)」方が結果的に速くなる現象が起きます。

4.ボトルネックの特定と改善アプローチ

実行計画から以下のポイントをチェックし、改善策を検討します。

1. Index Scanが使われている場合

本来WHERE句で絞り込めるはずなのに全件読み込み(Scan)されている場合は、適切なインデックスが作成されていない可能性があります。
→ 検索条件やJOINの結合キーになっているカラムにインデックスの作成を検討します。

2. JOINの方式に違和感がある場合

実行計画上で、明らかに件数が数件に絞り込まれているのに Hash Join が使われている場合などは、「インデックスが存在しない」、または「統計情報がおかしくてオプティマイザが件数を誤認している」可能性があります。

実行計画のノードにマウスカーソルを合わせると詳細が表示されます。
【実際の行数(実際に読み取られた行数)】【推定行数(すべての実行の予測行数)】 を比較してください。
ここが大きく乖離している場合(例:予測は100万件なのに実際は10件だった)、オプティマイザの予測ミスです。これは 統計情報が無い、または古い ことが原因で発生します。

💡 Tips: SSMSを使用している場合、実際の実行プランの空いているところを右クリックし、「実際の実行プランの分析」を押すと、予実対比での分析を自動で行ってくれます。

5.統計情報の確認と更新方法

予測行数と実際の行数にズレがあった場合は、統計情報を確認します。

5-1. 統計情報一覧の取得

SELECT *
FROM sys.stats
WHERE object_id = OBJECT_ID('対象のテーブル名');

取得できる主な情報:
① インデックスに紐づく統計:インデックスを作成すると自動作成されます。
② 自動生成統計(_WA_Sys_00000003_XXXXXXなど):AUTO_CREATE_STATISTICS = ON の場合、WHERE句などで検索されたカラムにインデックスがないと、SQL Serverが自動で統計情報だけを作成してくれます。

5-2. 統計情報の詳細確認

5-1で調べた統計情報の名前(name列の値)を使って詳細を確認します。

DBCC SHOW_STATISTICS ('対象のテーブル名', '5-1で調べた統計情報のname');

5-3. 見るべきポイント

出力結果は3つの表に分かれていますが、特に重要なのは以下の2つです。

A. Header情報
Rows(全行数)と Rows Sampled(サンプリングされた行数)、そして Updated(最終更新日時)を確認します。ここが古いと、推定ミスの原因になります。

B. Histogram(ヒストグラム)
データの分布状況(どの値が何件くらいあるか)を示しています。オプティマイザはこのヒストグラムを見て「この条件なら約〇〇件ヒットするはず」と予測を立てています。

4. 統計情報の更新

情報が古い、またはデータが大きく変動した場合は、手動で統計情報を更新します。

-- 対象テーブルのすべての統計情報をフルスキャンで更新
UPDATE STATISTICS 対象のテーブル名 WITH FULLSCAN;

まとめ

SQL Serverのパフォーマンス改善は、クエリの書き方を試行錯誤する前に、まずは 「実行計画とI/O統計を見て事実を確認する」 ことが最短ルートです。

  1. SET STATISTICS IO ON; でボトルネックのテーブル(論理読み取りが多いテーブル)を特定する。
  2. 実行プランで、Scanになっている箇所や、予実(推定行数と実際の行数)がズレている箇所を見つける。
  3. 必要に応じてインデックスの作成や、統計情報の更新(UPDATE STATISTICS)を行う。

この手順を踏むことで、闇雲なチューニングから脱却できるようになります!

併せて、そもそものDB設計が終わっているとどうにもならなかったりするので、DB設計がうまくいってるか?という所の確認も必要だと思います。

DB設計についてもまとめているので、良ければ参考にしてください。

蛇足

基本的にはよくない方針だが、あまりにもパフォーマンス改善しない場合は、疑似マテリアライズドビューを作成して、検索の度にJOIN処理を走らせないのも手ではある。
ただ、これは非正規化になるので、最終奥義ぐらいの気持ち。

DBってデータを保存するだけの場所という認識やったけど、知れば知るほど奥深いし難しいしおもしろすぎる・・・

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?