SQL Server
Performance Tuning
Essentials
Masaki Hirose
Chapter 1
受講の準備
Chapter 1
Lesson 1
本コースについて
本資料のパフォーマンスチューニングの定義
1. 個別最適なチューニング
特定のクエリの「実行時間」または「CPU使用時間」の削減
2. 全体最適なチューニング
特定のインスタンスで実行されている全クエリの「実行時間の
合計値」または「CPU使用時間の合計値」の削減
本資料で学べること
• 個別最適の観点でひとつのクエリをチューニングするための知
識と技術
• 全体最適の観点でインスタンス全体の総実行時間および総CPU
使用時間を削減するための知識と技術
本資料の特徴
• 専用のサンプルDBを使った実践的なノウハウを知ることができる
• 知識としてだけではなく、実際にデモを見ながらチューニングに
よる速度アップを体感できる
対象者
• 日頃SQL Server / Azure SQL Databaseを使って開発・運用・管理
などを行っており、単一または全体的なクエリパフォーマンス
やサーバーのCPU高負荷状態に悩んでいる人
• CPU負荷を削減することで、クラウドで稼働中のDBをスケール
ダウン/スケールインしてコストカットしたい人
前提条件
• SQLの使用経験
• SQL Serverの使用経験
• SQL ServerのPaaSの使用経験 (Azure/AWS/GCP)
• SQL Server Management Studio (SSMS)の使用経験
チューニング力を上げるためのポイント
• 自分で考える
• 自分で手を動かす
Chapter 1
Lesson 2
サンプルDBの準備
サンプルDBの内容
DB名 Member レコード数 MemberEMail レコード数
MyTuningDB_small 1,000万 1,500万
MyTuningDB_middle 2,000万 3,000万
MyTuningDB_large 3,000万 4,500万
サンプルDBのダウンロード
• https://drive.google.com/drive/u/2/folders/13xVm-
alJ2kzuDQyDVvWxSyUR5i6lWx_5
• バックアップファイルを圧縮した3ファイル
• 展開したあとにリストア
サンプルDBについて
• SQL Server 2019以降でリストア可能
• 用途:サンプルDBを使ってクエリチューニングを体感してもらう
• データはすべてランダムに生成したデータ
• 電話番号やメールアドレスはランダムだが、実在の可能性がある
ため別用途での使用は禁止
個別最適
Chapter 2
チューニングの流れ
Chapter 2
Lesson 1
パフォーマンスチューニングの定義
本コースにおけるパフォーマンスチューニン
グの定義
1. 個別最適なチューニング
特定のクエリの「実行時間」または「CPU使用時間」の削減
2. 全体最適なチューニング
特定のインスタンスで実行されている全クエリの「実行時間の
合計値」または「CPU使用時間の合計値」の削減
Chapter 2
Lesson 2
チューニングの流れと評価方法
評価時に使うふたつの指標
• ユーザーの待ち時間に直結する「実行時間(duration)」
• サーバーの負荷に直結する「CPU使用時間(cpu)」
基本的にはチューニングすると両方減るが、そうでない場合も。
どちらを削減したいかあらかじめ決めておく。
クエリチューニングの流れ
1.対象クエリと削減したい値(duration/cpu)の決定
2.指標の計測 ①
3.チューニング実施(クエリ書き換え、インデックス作成等)
4.指標の計測 ②
5.評価
クエリチューニングの例
1. 実行時間 (duration) を削減したい
2. 指標の計測 ①
3. クエリチューニング(クエリの書き換え、インデックス作成など)
4. 指標の計測 ②
5. 評価
例:チューニングの結果、実行時間を1000msから10msへと99%
削減できた
評価するさいの注意点
• sys.dm_exec_query_statsにはコンパイルの時間は含まれない
ポイント:体感ではなく数値で比較する
×「かなり時間がかかっていたのが一瞬で完了するようになった」
〇「10秒かかっていたのが100 msecで完了するようになった」
Chapter 2
Lesson 3
set statistics time onとは
duration
計測方法① set statistics time on
cpu
Chapter 2
Lesson 4
sys.dm_exec_query_statsとは
計測方法② sys.dm_exec_query_stats
• キャッシュされたクエリプランのパフォーマンス統計を取得
• プランがキャッシュから削除されると、そのプランに紐づく
統計データも消える
• 粒度はステートメント単位
→ 1ストアド内で3クエリ実行するストアドだと3行取得
Chapter 2
Lesson 5
計測方法の使い分け
duration/cpu 計測方法の使い分け
• リリース前:set statistics time on
SSMSでの検証時に手動で実行するクエリ
• リリース後:sys.dm_exec_query_stats
アプリケーションが実行しているクエリ
なぜsys.dm_exec_query_statsを使うのか?
• プロダクション環境ではさまざまなパラメータでクエリが実行
• パラメータによって実行時間が大幅に変化するケースもある
例)リリース前の手動での検証時は「where col1 = 1」で1秒だったが、リリース後は
「where col1 = 2」が指定されることが多く、この場合だと10秒かかる。
• sys.dm_exec_query_statsを使えば、プロダクション環境で指定さ
れるさまざまなパラメータによって実行された実際の
duration/cpuを取得できる
• プロダクション環境でのチューニングの評価方法として妥当
注意点① durationとcpuは大きく違う場合も
• duration >> cpu
ロック競合などで待たされる時間が多いと、この関係になる
• duration << cpu
並列クエリの場合はcpu時間のほうが大きくなることがある
• duration/cpuは個別に把握することが大事
• コストとは
• あるスペックのマシンでそのクエリを実行した場合に想定さ
れる実行時間
• コストが高くても「実行時間が長そうだとSQL Serverが思っ
ている」ということまでしかわからない
• 基本的にはコストが低いほどduration/cpuも低いが、コストが下
がってもduration/cpuが増えることもある
⇒ 指標として不適当
注意点② 評価の指標にコスト値は使わない
Chapter 3
インデックスをマスターする
Chapter 3
Lesson 1
インデックスを使う理由
SQLは宣言型言語
最大の利点は「なにを」と「どのように」を分離できること
select * from Member
where MemberID = 18629764
なにを取得したいか=SQL
どのように取得するか=実行プラン
「どのように」は本来知らなくていい
• 「なにを=SQL」が書ければ、欲しいデータはとれる
• 「どのように」は本来知らなくていいが、チューニングのため
に踏み込む必要あり
duration/cpuに影響を与えるのは実行プラン
• SQLをいくら書き換えても、実行プランが変化しなければ
durationもcpuも同じ
• 実行プランを変化させる有効な手段のひとつが「インデックス」
durationの差と実行プランの違い
• インデックス効いてる
• select * from [Member] where LoginName = 'HunterGreen45744363'
• インデックス効いてない
• select * from [Member] with(index(PK_Member)) where LoginName =
'HunterGreen45744363’
Chapter 3
Lesson 2
テーブルとインデックスのアーキテ
クチャ
論理的な構造
8KBの連続領域
物理的な構造
テーブルのアーキテクチャ
ツリー構造
インデックスとは
• クエリ高速化に有効なデータ構造
• さまざまな種類があるが、SQL Serverでもっとも一般的なのは
ツリー構造のインデックス
2種類の基本的なインデックス
• クラスタ化インデックス
• 1テーブルに1個
• テーブルの実データ(=全カラム)が格納
• 非クラスタ化インデックス
• 1テーブルに0~999個
• テーブルの一部のカラムが格納
⇒ どちらもページを論理的につなげてツリー構造にしたもの
page
page page
page page
page page
双方向連結リスト
B-Tree (Balanced-Tree)
Root
Branch
(中間)
Leaf
Chapter 3
Lesson 3
クラスタ化インデックスとは
クラスタ化インデックスの定義
[MemberID]で並び替えられたツリー構造になっている
ALTER TABLE [dbo].[Member] ADD CONSTRAINT
[PK_Member] PRIMARY KEY CLUSTERED
(
[MemberID] ASC
)
クラスタ化インデックス:キーで並び替え
Root
Branch
Leaf
実データ
インデックスキーの範囲情報
+
対応するページ番号
MemberID Page
1-50000 ①
50001-100000 ②
① ②
MemberID Page
1-25000 ③
25001-50000 ④
③ ④
MemberID LoginName … RegistDate
1 a1 … 2020/10/1
2 a2 … 2020/10/2
… … … …
25000 b1 … 2020/10/3
Chapter 3
Lesson 4
Index SeekとIndex Scan
ポイント
インデックスは「うまく使われれば」クエリを高速化できる
ひとつずつ実行してみよう
select * from Member where MemberID = 18629768
select * from Member where LoginName = 'Janita1317'
どちらも使われるインデックスは同じ
⇒ インデックスはどう使われたのか?
瞬時に実行完了したクエリ:Index Seek
select * from Member where MemberID = 18629768
時間がかかったクエリ:Index Scan
select * from Member where LoginName = 'Janita1317'
矢印の向きが違うだけなのになぜ速い/遅い?
DECLARE @DB_ID int, @Object_ID int
set @DB_ID = DB_ID('MyTuningDB_small')
set @Object_ID = OBJECT_ID('Member')
SELECT
name, index_id, index_type_desc, index_depth, index_level, page_count, record_count,
avg_fragmentation_in_percent as 断片化率
FROM sys.dm_db_index_physical_stats (@DB_ID, @Object_ID, NULL , NULL, 'DETAILED') as A
JOIN sys.objects as B with(nolock) on A.object_id = B.object_id
ORDER BY index_id, index_level desc
ツリーの深さ(4) << リーフページ数(160350)
リーフページ数:160350
ツリーの深さ:4
Level=2
Level=1
Level=0
Level=3
ツリーの深さ(4) << リーフページ数(160350)
リーフページ数:160350
ツリーの深さ:4
Level=2
Level=1
Level=0
Level=3
seek時の読み取りページ数 << scan時の読み取りページ数
Seek = 読み取りページ数が圧倒的に小さい(160350 → 4) = 高速
リーフページ数:160350
ツリーの深さ:4
Level=2
Level=1
Level=0
Level=3
Chapter 3
Lesson 5
非クラスタ化インデックスで
ScanからSeekへ変化させる
インデックスが効く(=seek)条件
例:「インデックスの並び順」=「where句で指定した条件」
ALTER TABLE [dbo].[Member] ADD CONSTRAINT [PK_Member] PRIMARY
KEY CLUSTERED
(
[MemberID] ASC
)
効く:select * from Member where MemberID = 18629768
効かない:select * from Member where LoginName = 'Janita1317'
インデックス≠万能薬
set statistics io onで読み取りページ数を確認
seek
scan
set statistics time, io on で両方確認も可能
時間がかかったクエリを高速化するには?
select * from Member where LoginName = 'Janita1317'
これが
こうなればOK
そのための新しいインデックス
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON
[dbo].[Member]
(
[LoginName] ASC
)
非クラスタ化インデックス
Root
Branch
Leaf
キー+クラスタ化インデックスキー
インデックスキーの範囲情報
+
対応するページ番号
LoginName Page
a1-c1 ①
c2-d1 ②
① ②
LoginName Page
a1-b1 ③
b2-c1 ④
③ ④
LoginName MemberID
a1 1
a2 2
… …
b1 25000
Chapter 3
Lesson 6
非クラスタ化インデックスの
バリエーション
付加列インデックス
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON
[dbo].[Member]
(
[LoginName] ASC
) INCLUDE (RegistDate)
付加列インデックスの構造
Root
Branch
Leaf
キー+クラスタ化インデックスキー+付加列
インデックスキーの範囲情報
+
対応するページ番号 LoginName Page
a1-c1 ①
c2-d1 ②
① ②
LoginName Page
a1-b1 ③
b2-c1 ④
③ ④
LoginName MemberID RegistDate
a1 1 2020/10/1
a2 2 2020/10/2
… … …
b1 25000 2020/10/3
複合インデックス
CREATE NONCLUSTERED INDEX
[IX_Member_Sei_PrefectureID]
ON [dbo].[Member]
(
[Sei] ASC,
[PrefectureID] ASC
)
複合インデックス = 複数のカラムをインデックスキーに指定
複合インデックス
Root
Branch
Leaf
① ②
③ ④
Sei PrefectureID Page
Adena 40 ①
Barrett 25 ②
Sei PrefectureID Page
Adena 40 ③
Adrian 3 ④
Sei PrefectureID MemberID
Adena 40 523
Adena 42 613
作成時に指定したカラムの順序で
インデックスキーが作成される
複合インデックスを使用した検索
インデックスキーに指定したカラムの順序が検索効率に影響する
SELECT COUNT(*) FROM Member WHERE Sei = 'Adena' AND PrefectureID = 1
→ インデックス作成時に指定したカラムの両方が検索条件に含まれる
SELECT COUNT(*) FROM Member WHERE Sei = 'Adena‘
→ インデックス作成時に指定した「先頭のカラム」が検索条件に含まれる
SELECT COUNT(*) FROM Member WHERE PrefectureID = 1
→ インデックス作成時に指定したカラムは含むが、「先頭のカラム」が検索条件に
含まれていない
• Index Seekで検索されるケース
• Index Seekで検索が行われないケース
⇒ インデックスの先頭には検索に使われる頻度の多い項目を指定する
Chapter 3
Lesson 7
キー参照と
カバリングインデックス
キー参照
非クラスタ化インデックスだけではカラムを返せないとき
• 非クラスタ化インデックスでIndex Seek
• リーフページでクラスタ化インデックスキー取得
• 取得したキーでクラスタ化インデックスをIndex Seek
( = キー参照)
LoginName MemberID RegistDate
a1 1 2020/10/1
a2 2 2020/10/2
… … …
b1 25000 2020/10/3
SELECT LoginName, RegistDate, Sei FROM Member WHERE LoginName = 'b1'
リーフページに[Sei]が無い
非クラスタ化インデックス
実データ
MemberID LoginName … Sei
1 a1 … Asai
2 a2 … Tanaka
… … … …
25000 b1 … Yokota
クラスタ化インデックス
キー参照 (MemberID =
25000でIndex Seek)
カバリングインデックス
• 上記ようなインデックスが作成されている場合
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member]
([LoginName] ASC) INCLUDE (RegistDate)
キー参照が発生しないインデックスを「カバリングインデックス」
SELECT LoginName, RegistDate FROM Member WHERE LoginName = 'Tawny265167'
インデックスキー
インデックスキー INCLUDE
カラム
クエリに必要なカラムをカバーするインデックスになっている
=カバリングインデックス
カバリングインデックス(続き)
非クラスタ化インデックスで完結するため、
実行プランにもキー参照があらわれない。
カバリングインデックスのポイント
• カバリングかどうかはクエリごとに決まる
• インデックスは下記のクエリに対して「カバリング」
• インデックスは 下記のクエリに対して「カバリングではない」
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member]
([LoginName] ASC) INCLUDE (RegistDate)
SELECT LoginName, RegistDate FROM Member WHERE LoginName = 'Tawny265167'
SELECT LoginName, RegistDate, Sei FROM Member WHERE LoginName = 'Tawny265167'
Chapter 3
Lesson 8
インデックスのまとめ
インデックスのまとめ①
• クラスタ化インデックス
• キーの順番で並び替えられている
• 1テーブルにつき1個だけしか作れない
• 非クラスタ化インデックス
• キーの順番で並び替えられている
キーの範囲情報
キーの範囲情報
実データ
キー + クラスタ化インデックスキー
Seek
Scan
Seek
Scan
• 付加列インデックス
• 非クラスタ化インデックスとほぼ同じ
• リーフページに付加列も含まれている
• 複合インデックス
• 非クラスタ化インデックスとほぼ同じ
• 複数のカラムをインデックスキーに指定
キー+クラスタ化インデックスキー+付加列
インデックスのまとめ②
キーの範囲情報
複数列のキー
の範囲情報
Seek
Scan
Seek
Scan
キー + クラスタ化インデックスキー
インデックスのまとめ③
カバリングインデックス
• あるクエリに必要な全カラムを含むインデックス
• クエリ①ではカバリングだがクエリ②ではカバリングではないという
ふうに、クエリ単位でカバリングかどうかは変わってくる
Seek vs Scan
• Seekが有利なとき
• レコードを大幅に絞り込めるとき
• Scanが有利なときやScanしか使われないとき
• レコード数が少ないテーブル
• count(*)などの集計処理で全件走査が必要なとき
インデックスのメリット:「データ透過性」
ロジックに影響しないので、インデックスを追加・削除しても
同じSQLなら取得できるデータは変わらない
⇒ 機能面のテスト不要
インデックスのデメリット
• ディスク容量が増える
• 各インデックスは物理的に独立している
• インデックスを作成したテーブルの更新速度が落ちる
• 1レコードをINSERTするときも、最大でインデックスの数と同じだけ
書き込みが発生する
Chapter 3
Lesson 9
インデックスの練習問題
問題:インデックスを設計してください
DECLARE @Tel VARCHAR(20)
SET @Tel = '0292866656'
SELECT MemberID
FROM Member
WHERE tel = @Tel
回答
※MemberIDは主キーなので自動的にインデックスに含まれる
CREATE NONCLUSTERED INDEX [IX_Member_Tel]
ON [dbo].[Member] ([Tel])
問題:インデックスを設計してください
DECLARE @LoginName VARCHAR(20)
SET @LoginName = 'Keg River4714'
SELECT MemberID
,GenderID
,PrefectureID
FROM Member
WHERE LoginName = @LoginName
回答①
CREATE NONCLUSTERED INDEX [IX_Memer_LoginName]
ON [dbo].[Member] ([LoginName])
回答②:付加列を追加
CREATE NONCLUSTERED INDEX [IX_Memer_LoginName]
ON [dbo].[Member] ([LoginName]) INCLUDE ([GenderID],
[PrefectureID])
Chapter 4
テーブルサイズの変化と
インデックス構造
Chapter 4
Lesson 1
レコード数増加の影響調査
サービス成長に伴ってデータは増え続ける
レコード数が増えていったときに
• インデックスの構造はどう変わるかを理解する
• クエリの実行速度はどう変わるかを理解する
レコード数だけが異なる3つのDB
事前準備:各DBでインデックス再構築
use MyTuningDB_small
alter index PK_MemberEMail on MemberEMail rebuild
go
use MyTuningDB_middle
alter index PK_MemberEMail on MemberEMail rebuild
go
use MyTuningDB_large
alter index PK_MemberEMail on MemberEMail rebuild
go
各DBごとにインデックスの構造を確認
DECLARE @OBJECT_ID int
set @OBJECT_ID = OBJECT_ID('MemberEMail')
SELECT
index_id
,index_type_desc
,index_depth
,index_level
,page_count
,record_count
FROM sys.dm_db_index_physical_stats (DB_ID(), @OBJECT_ID, NULL , NULL,
'DETAILED') as A
JOIN sys.objects as B on A.object_id = B.object_id
ORDER BY index_id, index_level
Small
1500万レコード
Middle
3000万レコード
Large
4500万レコード
レコード数増加によるインデックス構造の変化まとめ
• 1000万単位のレコード数の差でもツリーの深さはほぼ同じ
(index_depth = 3 or 4)
• リーフノードのページ数(page_count)はレコード数にほぼ比例
Chapter 4
Lesson 2
パフォーマンスへの影響
パフォーマンスへの影響:Index Scanのとき
Small
1500万レコード
Middle
3000万レコード
Large
4500万レコード
パフォーマンスへの影響:Index Scanのとき
Index Scanのポイント
• レコード数が増えるほど「論理読み取り数」が増加
• リーフノードのページ数にほぼ一致
• レコード数が増えるほど如実に実行時間が増加
• 1.5秒 → 3秒 → 5秒
パフォーマンスへの影響:Index Seekのとき
Small
1500万レコード
Middle
3000万レコード
Large
4500万レコード
パフォーマンスへの影響:Index Seekのとき
Index Seekのポイント
• レコード数が増えても「論理読み取り数」がほぼ同じ
• インデックスの階層数にほぼ一致
• そのためレコードが大幅に増加しても実行時間はほぼ同じ
• Index Seekの強力な特徴
パフォーマンスへの影響のまとめ
• データ量増加に伴うクエリ実行時間の変化を推定するときに
• 「そのクエリの実行はScanなのか、Seekなのか」を
理解しておくことがとても重要
• 信頼度が低い推定
• 「データ量が増えたから遅くなった/遅くなりそう」
• 信頼度が高い推定
• 「データ量が増えてもSeek処理なので速度は変わらないはず」
• 「データ量が増えるとScan処理なので徐々に遅くなっていく懸念
がある」
Chapter 5
実行プランについて
Chapter 5
Lesson 1
実行プランとは
このチャプターの目標
• 実行プランの見方を理解する
• 実行プランで押さえておくべき演算子を理解する
• チューニング目的で実行プランを見る際のポイントを理解する
実行プランとは
• 「どのように」データを取ってくるかの計画図
• SQL Serverがプランを作成する
• グラフィカルなものとテキストベースの2種類がある
• 今回はグラフィカルな実行プランを用いる
基本的な実行プランの見方
• 右から左、上から下の順番で実行される
• ふたつのインデックスから読み取ったデータを合体させる
複雑なプランでも考え方は同じ
③
④
①
②
⑤
ふたつのインデックスを読み取って合体を繰り返す
Chapter 5
Lesson 2
データへのアクセス方法
実行プランの基本的な演算子まとめ
• データへのアクセス方法
• Scan / Seek / キー参照
• 結合方法
• Nested Loops / Hash Match / Merge Join
• 並び替え
• Sort
他にもたくさんあるが、ボトルネックはこの中のどれかである場合
が多い
① Index Scan
① Index Scan
② Index Seek
② Index Seek
③キー参照
③キー参照
Chapter 5
Lesson 3
結合方法と並び替え
① Nested Loops
① Nested Loops – 2重for文のイメージ
Table_A
1
2
3
4
Table_B
6 D
1 B
5 C
1 A
① Nested Loops – 2重for文のイメージ
Table_A
1
2
3
4
Table_B
6 D
1 B
5 C
1 A
① Nested Loops – 2重for文のイメージ
Table_A
1
2
3
4
Table_B
6 D
1 B
5 C
1 A
① Nested Loops – 2重for文のイメージ
Table_A
1
2
3
4
Table_B
6 D
1 B
5 C
1 A
1 1 B
1 1 A
② Hash Match
② Hash Match
Table_A
1
2
3
4
ハッシュテーブルを作る
Table_A ハッシュ値
1 1agsc3
2 fasd98
3 42cf89
4 fgt2cc
ハッシュテーブル
Table_B
6 D
1 B
5 C
3 A
35vxxv
ハッシュ値計算、
マッチング
② Hash Match
Table_A
1
2
3
4
ハッシュテーブルを作る
Table_A ハッシュ値
1 1agsc3
2 fasd98
3 42cf89
4 fgt2cc
ハッシュテーブル
Table_B
6 D
1 B
5 C
3 A
35vxxv
1agsc3
ハッシュ値計算、
マッチング
h577v
② Hash Match
Table_A
1
2
3
4
ハッシュテーブルを作る
Table_A ハッシュ値
1 1agsc3
2 fasd98
3 42cf89
4 fgt2cc
ハッシュテーブル
Table_B
6 D
1 B
5 C
3 A
35vxxv
1agsc3
h577v
42cf89
1 1 B
3 3 A
ハッシュ値計算、
マッチング
③ Merge Join
③ Merge
Join
Table_B
6 D
1 B
5 C
1 A
Table_A
1
2
3
5
Table_B
1 A
1 B
5 C
6 D
ソート不要 ソート
③ Merge
Join
Table_B
6 D
1 B
5 C
1 A
Table_A
1
2
3
5
Table_B
1 A
1 B
5 C
6 D
ソート不要 ソート
③ Merge
Join
Table_B
6 D
1 B
5 C
1 A
Table_A
1
2
3
5
Table_B
1 A
1 B
5 C
6 D
ソート不要 ソート
③ Merge
Join
Table_B
6 D
1 B
5 C
1 A
Table_A
1
2
3
5
Table_B
1 A
1 B
5 C
6 D
ソート不要 ソート
③ Merge
Join
Table_B
6 D
1 B
5 C
1 A
Table_A
1
2
3
5
Table_B
1 A
1 B
5 C
6 D
ソート不要
1 1 A
1 1 B
5 5 C
ソート
JOINの使い分け
• 基本的にSQL Server側が最適な結合方法を判断する
• JOINする2テーブルのサイズやインデックスの有無で最適な方法が違う
• Nested Loops
• 小さめのデータセットや
where句でレコード数が大幅に絞り込めるとき向き
• Merge Join / Hash Match
• 大規模テーブル同士でレコード数が絞り込めないとき向き
• メモリ消費大。場合によってはtempDBへの物理書き込み発生で速度低下
常に最適なJOINが選択されるわけではない
「Nested Loopsが良いはずなのにHash Matchになっている」
「Hash MatchがいいはずなのにNested Loopsになっている」
といった箇所がボトルネックになる可能性はある
Sort:データの並び替え
Chapter 5
Lesson 4
実行プランを確認するさいの
ポイント
実行プランで見るべきポイント①
• シーク述語
• Index Seek時にレコードを絞り込む条件
• 述語
• リーフページ走査時にレコードを絞り込む条件
• オブジェクト
• 走査対象のクラスタ化インデックス/非クラスタ化インデックス/ヒープ
実行プランで見るべきポイント②
• 予測行数 / 実際の行数 / およびその差
• 予測実行回数 / 実際の実行回数 / およびその差
• 予測と実際の乖離が大きく、実行時間が長い場合は他に最適なプランが
存在する可能性がある
実行プランを見るさいのマインドセット
• 全部を完全に理解していなくてもチューニングは可能
• 「どこがボトルネックで遅いのか」を突き止めるよう意識する
Chapter 5
Lesson 5
インデックス設計の練習問題
問題:インデックスを設計してください
SELECT TOP 10 *
FROM MemberEMail
ORDER BY Email ASC
回答:ソートをカットするインデックス
create index [IX_MemberEMail_Email] on [MemberEmail] ([Email] asc)
問題:インデックスを設計してください
SELECT TOP 10 *
FROM MemberEMail
ORDER BY DeleteFlag ASC
,Email DESC
回答:order byとキーの並び順を同じにする
create index [IX_MemberEMail_DeleteFlag_Email] on [MemberEmail]
([DeleteFlag] asc, [EMail] desc)
問題:インデックスを設計してください
SELECT TOP 10 *
FROM MemberEMail
WHERE DeleteFlag = 0
ORDER BY Email
回答:クエリ実行順序とキーの順番を同じにする
create index [IX_MemberEMail_DeleteFlag_Email] on
[MemberEmail]([DeleteFlag], [EMail])
Chapter 6
クエリの実行
Chapter 6
Lesson 1
クエリが実行されるまでの流れ
クエリ実行までの流れの概略図
統計情報など
クエリツリー
実行プラン
SQL Parser
Optimizer
Query
Executor
実行プランがキャッシュされている場合
プランキャッシュ
実行プラン
SQL Parser
Query
Executor
実行プランを理解するためのポイント
• 実行プランを生成するタイミングでは、オプティマイザは
WHERE句でどれだけレコードが絞り込まれるかわからない
⇒ 推定するしかない
• 絞り込まれるレコード数の推定方法 = 基数推定アルゴリズム
• 基数推定に使用する重要な情報が統計情報
Chapter 6
Lesson 2
統計情報について
統計情報の概要
統計情報はカラムの値の分布をヒストグラム情報として保持
• インデックス作成時に対応する統計情報が
自動作成される
• PK_Member
• MemberテーブルのPKの統計情報
• _WA_Sys_***
• クエリ実行時に自動作成される場合あり
統計情報の概要
PK_Memberの統計情報 ①
• 名前: 統計情報の名前
• 更新 : 統計情報の更新日時
• 行 : レコード数
• サンプリングされた行数
• 手順 : ヒストグラムのステップ数
• 列 : どのカラムの情報か
• RANGE_HI_KEY:ステップの上限キー
• RANGE_ROWS:ステップ内の行数(上限は含まない)
• EQ_ROWS:上限の値と列の値が等しい行数
PK_Memberの統計情報 ②
0
1
3178694
1
2392063
1
4429238
1
0
1
0
select count(*) from Member where MemberID < 18629764
select count(*) from Member where MemberID = 18629764
select count(*) from Member where 18629764 < MemberID and MemberID < 23169772
select count(*) from Member where MemberID = 23169772
select count(*) from Member where 23169772 < MemberID and MemberID < 26721041
select count(*) from Member where MemberID = 26721041
select count(*) from Member where 26721041 < MemberID and MemberID < 33503451
select count(*) from Member where MemberID = 33503451
select count(*) from Member where 33503451 < MemberID and MemberID < 33503454
select count(*) from Member where 33503454 = MemberID
select count(*) from Member where 33503454 < MemberID
ヒストグラム化するとこうなる
通常はレコード数 >> サンプル
数
• 先ほどの例はサンプリング率100%(フルスキャン)
update statistics member
update statistics member with fullscan
• with fullscan無し:レコード数に応じて自動でサンプリング率が決定
統計情報のポイント①
• 統計情報は「不完全な情報」
• 各テーブルの各レコードの全カラムの情報がわかれば、最適なプランは
生成しやすい
• ただし全レコードを実行前にチェックしていると時間がかかってしまう
→ 最適化に時間がかかっては意味がない
• そこで統計情報を利用する
→ 完全な情報ではなく、ざっくりとしたカラム値の分布で代用
• 基数推定アルゴリズムも完ぺきではない = 誤差は生じる
• 統計情報を使って、WHERE句の絞り込みレコード数を推定する
→ 統計情報の時点でざっくりとした情報なので、完璧な精度は出ない
⇒ 自分で「このプランは妥当なのか」を評価する力が重要
• 統計情報は常に更新されるわけではない
• 統計の自動更新がONになっていても、全レコードの20%が更新されて
はじめて統計も更新される
• 実際のデータ分布と統計情報に乖離が生まれる場合がある
• 乖離が大きいほど、最適な実行プランが生成されない可能性も上がる
統計情報のポイント②
統計情報についてのまとめ
• オプティマイザの仕事は最高のプランを見つけることではなく、
限られた時間で「良いプラン」を生成すること
• オプティマイザは実行プランを生成する際に統計情報を利用
• 統計情報はざっくりとしたカラムの分布をヒストグラムで保持
• 諸条件により最適でないプランが生成される場合もある
• 統計情報のサンプリング数
• 統計情報の更新日時
• 基数推定アルゴリズムの限界
Chapter 7
Selectivityを理解する
Chapter 7
Lesson 1
Selectivityとは
高速なクエリ selectivityの良い検索述語 適切なインデックス
= +
このレッスンで使う用語について
• 検索述語:WHERE句の各条件のこと
• selectivity (選択性):検索述語が行をどれだけ絞り込めるかの指標
• selectivityが良い:少ない行に絞り込みができること
• 適切なインデックス:作成することでIOを劇的に削減できるインデックスのこと
検索述語のselectivity評価例①
検索述語:MemberID = 18629764
1,000万レコードを1行に絞り込み →「selectivityがもっとも良い」
検索述語のselectivity評価例②
検索述語:DeleteFlag = 0
1,000万レコードを約半数に絞り込み →「selectivityが悪い」
「selectivityが良い」のボーダーラインは?
• 「レコード全体の5%程度」まで絞り込めるか
• 1,000万レコードのテーブルであれば、5%の50万レコードまで
• ポイント:主キーやユニークキーはselectivityがもっとも良い
• 「5%」の基準はディスク性能など環境によって変わる
• あくまで目安と考えておく
検索述語のselectivity評価例③
検索述語:DeleteFlag = 0 and PrefectureID = 6 and GenderID = 2 and Sei = 'Marlin’
1,000万レコードを26行に絞り込みできるので「selectivityが良い」
複数の検索述語もひとまとめで考えてOK
selectivityが良いクエリにインデックスを作成
• demo用クエリ
select *
from Member
where DeleteFlag = 0
and PrefectureID = 6
and GenderID = 2
and Sei = 'Marlin'
create index IX_Member_1 on Member (DeleteFlag, PrefectureID, GenderID, Sei)
• demo用インデックス
「適切なインデックス」を作るポイント
selectivityの良い検索述語の組み合わせでインデックスを作ること
で「適切なインデックス」を作成できる
select *
from Member
where DeleteFlag = 0
and PrefectureID = 6
and GenderID = 2
and Sei = 'Marlin'
create index IX_Member_1 on Member (DeleteFlag, PrefectureID, GenderID, Sei)
まとめ
• 高速なクエリ = selectivityの良い検索述語 + 適切なインデックス
• クエリチューニングの際は以下の2点を確認
• selectivityの良い検索述語があるか
• 適切なインデックスが存在するか
Chapter 7
Lesson 2
複数テーブルを参照するクエリ
のSelectivity を評価する
複数テーブルJOIN時のselectivityは?
各テーブルごとにselectivityを評価する
• 「Memberの検索述語はselectivityが良い」
• 「MemberEmailの検索述語はselectivityが悪い」
複数テーブルJOIN時のselectivity評価のポイント
• 複数テーブルのJOINを含むクエリでは、selectivityが良い
テーブルがひとつ以上存在すれば適切なインデックスと
組み合わせることで高速化が見込める
• なぜか?
チューニング後の実行プラン
MemberEMail
評価時の
selectivity:悪い
(8552233行)
MemberEMail
実際の
selectivity:良い
(1行)
MemberEMailのシーク述語は「結合条件」
select MemberID
from Member
where DeleteFlag = 0
and Tel = '0698903494'
select MemberID
from MemberEMail
where MemberID =
18629764
and MainFlag = 1
元クエリ
SQL Serverが
クエリ実行する際の
イメージ
select A.MemberID
from Member A
join MemberEMail B on A.MemberID = B.MemberID
where A.DeleteFlag = 0 and A.Tel = '0698903494'
and B.MainFlag = 1
select MemberID
from Member
where DeleteFlag = 0
and Tel = '0698903494'
select MemberID
from MemberEMail
where MemberID = 18629764
and MainFlag = 1
①まずMemberから
MemberIDを取得
②取得したMemberIDを検索
述語に追加
実行プランのシーク述語をチェック
・Memberのシーク述語は元クエリのwhere句と同一
・MemberEMailのシーク述語は元クエリのwhere句「MainFlag = 1」
ではなく、元クエリの結合条件「A.MemberID = B.MemberID」
select A.MemberID
from Member A
join MemberEMail B on A.MemberID = B.MemberID
where A.DeleteFlag = 0 and A.Tel = '0698903494'
and B.MainFlag = 1
selectivityが悪い時:後続の処理にも影響
select *
from Member A
join MemberEMail B on A.MemberID = B.MemberID
where A.PrefectureID = 1 and A.DeleteFlag = 0 and A.Sei like 'a%'
and B.MainFlag = 1
Chapter 7
Lesson 3
selectivityのまとめ
selectivityのまとめ①
• JOINを含むSELECT文の実行の流れ
1. 各テーブル(正確にはインデックスまたはヒープ)ごとに
データを絞り込む
2. テーブルを合体させる
3. 1と2を繰り返す
• 検索述語に複数テーブルのカラムが指定されていても、
基本的には最もselectivityが良い検索述語のみがシーク述語
となり、それ以外は結合条件がシーク述語となる
selectivityのまとめ②
• もっともselectivityが良い検索述語による絞り込みレコード数は、
その後の各結合処理の実行回数(≒レコード数)へと
影響が伝搬していく
• selectivityが良い → パフォーマンス的な好影響が伝搬していく
• selectivityが悪い → パフォーマンス的な悪影響が伝搬していく
selectivityのまとめ③
• 複数テーブルのJOINを含むクエリでも、selectivityが良い検索述
語がひとつ以上存在すればチューニングは可能。なぜか?
⇒クエリ実行時、selectivityが良い検索述語によりレコード数
が大幅に絞り込まれ、その後の結合時にパフォーマンス的な
好影響が伝搬していくため
Chapter 7
Lesson 4
インデックス設計の練習問題
問題:インデックスを設計してください
DECLARE @MemberID INT
SET @MemberID = 18629764
SELECT *
FROM Member a
JOIN MemberEMail b ON a.MemberID = b.MemberID
WHERE a.MemberID = @MemberID
ORDER BY MainFlag DESC
回答:MemberEMailのScanをSeekにしたい
CREATE NONCLUSTERED INDEX [IX_MemberEmail_MemberID]
ON [dbo].[MemberEMail] ([MemberID])
問題:インデックスを設計してください
DECLARE @Tel VARCHAR(100)
SET @Tel = '09002505878'
SELECT LoginName, Sei, Mei, Tel
FROM Member a
WHERE EXISTS (
SELECT *
FROM MemberEMail b
WHERE a.MemberID = b.MemberID
AND MainFlag = 1
AND b.DeleteFlag = 0
)
AND Tel = @Tel
回答:まずMemberのScanをSeekにする
CREATE NONCLUSTERED INDEX [IX_Member_Tel]
ON [dbo].[Member] ([Tel])
次にMemberEMailのScanをSeekにする
CREATE NONCLUSTERED INDEX [IX_MemberEMail_MemberID]
ON [dbo].[MemberEMail] ([MemberID])
INCLUDE ([MainFlag], [DeleteFlag])
インデックス設計のポイント
• とりあえず実行して「実際の実行プラン」をチェックする
• 「ボトルネックはどこか」という観点で実行プランを見る
• インデックスをひとつずつ作成していく
• プラン確認 → インデックス作成 → プラン確認
をチューニング完了まで繰り返す
Chapter 8
全体最適なチューニング
Chapter 8
Lesson 1
全体最適なチューニングとは
パフォーマンスチューニングの定義
1. 個別最適なチューニング
特定のクエリの「実行時間」または「CPU使用時間」の削減
2. 全体最適なチューニング
特定のインスタンスで実行されている全クエリの
「実行時間の合計値」または「CPU使用時間の合計値」の削減
全体最適なチューニングをするときの前提
• インスタンスでは多数のクエリが実行されている
• 各クエリの詳細は把握していない
• 各クエリの実行頻度も把握していない
• 「どのクエリをチューニングすればいいのか」を調査すべき
パレートの法則(80:20の法則)
• 「全体の数値の大部分は全体を構成するうちの一部の要素が
生み出している」という理論
• 例:ビジネスにおいて、売上の8割は全顧客の2割が生み出している
• 例:商品の売上の8割は、全商品銘柄のうちの2割が生み出している
パレートの法則をDBサーバーに当てはめる
• DBサーバー1台にかけるCPU負荷の8割は、
全クエリの2割が生み出している
• DBサーバー1台で実行されるクエリの総実行時間の8割は、
全クエリの2割が生み出している
• この「2割」のクエリを見つけ出すことが、
少ない労力で大きなCPU負荷減や総実行時間減につながる
※ 実際は1割以下のこともある
ZOZOTOWNのDBにおける全体最適なチューニング例
全体の1%のクエリをチューニングしたことで、CPU負荷が50%減
パレートの法則を元にした重要な問い
• CPU負荷を下げたい場合
「今自分がチューニングしようとしている箇所は
CPU負荷全体の何割を占めるのか?」
• 実行時間を下げたい場合
「今自分がチューニングしようとしている箇所は
総実行時間の何割を占めるのか?」
全体最適なチューニングでもっとも重要なこと
「どのクエリが全体のCPU負荷の何%を占めているのか」
「どのクエリが全体の実行時間の何%を占めているのか」
ということについての事前調査を行う
⇒ そのためにはツールを使う
Chapter 8
Lesson 2
拡張イベントとは
拡張イベントとは
軽量なパフォーマンス監視システム
⇒ SQL Server ProfilerやSQLトレースの上位互換
拡張イベントの例
チューニング目的でよく使用するイベント
• rpc_completed:リモートプロシージャコール完了
• sql_batch_completed:バッチ完了
• sp_statement_completed:ストアドプロシージャの
ステートメント完了
• sql_statement_completed:ステートメント実行完了
各イベントの関係性
cpu/durationともに
rpc_completed + sql_batch_completed ≒ コンパイル時間 +
sp_statement_completed + sql_statement_completed
まずはrpc_completed+sql_batch_completedの粒度で調査する
必要に応じてstatement系にドリルダウンする
Chapter 8
Lesson 3
DMVを使ったチューニング
対象の選定方法
DMV(Dynamic Management View)とは
• サーバーの情報が格納されたViewのこと。沢山種類がある
• sys.dm_exec_query_stats
⇒ クエリの実行統計(cpu/durationなど)をキャッシュ
• 累積値なので、2回情報を取得して差分をとれば該当時間帯の
各クエリのcpuやdurationの合計値を取得できる
⇒ 降順に並び替えると、cpu負荷をかけているクエリを見つけ
るといったことが可能
Chapter 8
Lesson 4
全体最適なインデックスとは
テーブルに作成すべきインデックスの数は?
• 一般的にインデックスが増えるほど
• 読み取りは高速になる
• 更新は低速になる
⇒ 明確な答えはない
• あるテーブルAに作成すべき最適なインデックスは
「テーブルAを参照する全クエリの総実行時間を最小化する
インデックスの組み合わせ」
総実行時間を最小化するインデックスの組み合わせ
• インデックスの組み合わせで総実行時間や総CPU時間は変化する
• 考えられるインデックス全パターンを試すのは時間的に無理
• そのため、実際には完璧な最適解は得られない
ではどうするか?
各テーブルに関するワークロードの性質を理解して柔軟に対応する
• write heavyならインデックスは必要最小限に留める
• read heavyならインデックスを積極的に作成する
インデックス作成の基本戦略
• 必要最小限のインデックス設計を心がける
• キーが同じで付加列だけ異なる等のインデックスはまとめる
• 意図した使われ方をしているか確認する
• DMVを使ってseek回数とscan回数の比率をチェックする
インデックス作成のアンチパターン
各クエリごとに最適なインデックスを作成する
⇒ 似たインデックスが大量に作成される
CREATE NONCLUSTERED INDEX
[IX_Member_LoginName] ON [dbo].[Member]
(
[LoginName] ASC
) INCLUDE (RegistDate)
CREATE NONCLUSTERED INDEX
[IX_Member_LoginName2] ON [dbo].[Member]
(
[LoginName] ASC
) INCLUDE (Sei, Mei)
CREATE NONCLUSTERED INDEX
[IX_Member_LoginName3] ON [dbo].[Member]
(
[LoginName] ASC, [RegistDate] ASC
) INCLUDE (Sei, Mei)
CREATE NONCLUSTERED INDEX
[IX_Member_LoginName4] ON [dbo].[Member]
(
[LoginName] ASC
) INCLUDE (DeleteFlag)
CREATE NONCLUSTERED INDEX
[IX_Member_LoginName] ON [dbo].[Member]
(
[LoginName] ASC
) INCLUDE (RegistDate)
CREATE NONCLUSTERED INDEX
[IX_Member_LoginName2] ON [dbo].[Member]
(
[LoginName] ASC
) INCLUDE (Sei, Mei)
CREATE NONCLUSTERED INDEX
[IX_Member_LoginName3] ON [dbo].[Member]
(
[LoginName] ASC, [RegistDate] ASC
) INCLUDE (Sei, Mei)
CREATE NONCLUSTERED INDEX
[IX_Member_LoginName4] ON [dbo].[Member]
(
[LoginName] ASC
) INCLUDE (DeleteFlag)
CREATE NONCLUSTERED INDEX [IX_Member_LoginName]
ON [dbo].[Member]
(
[LoginName] ASC , [RegistDate] ASC
) INCLUDE (RegistDate, Sei, Mei, DeleteFlag)
意図した使われ方をしているか確認する
declare @TableName varchar(1000) = 'Member'
select
OBJECT_NAME(i.object_id) as table_name
,i.name as index_name
,ps.row_count as row_count
,ps.reserved_page_count * 8.0 / 1024 as size_mb
,type_desc
,us.*
from
sys.dm_db_partition_stats ps
left join sys.indexes i
on ps.object_id = i.object_id and ps.index_id = i.index_id
left join sys.dm_db_index_usage_stats us
on ps.object_id = us.object_id and ps.index_id = us.index_id
where
OBJECT_NAME(i.object_id) = @TableName
order by
index_id
Chapter 8
Lesson 5
全体最適なインデックスの設計
必要最小限のインデックスを設計する
SELECT句でしか使わないカラムは基本的には付加列にする
select Sei, Mei from Member where LoginName = 'Test'
〇
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName])
INCLUDE ([Sei], [Mei])
×
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName], [Sei],
[Mei])
なぜSELECT用カラムは付加列か?
・付加列として加えておいた方がインデックスに拡張性がある
select top 10 Sei, Mei from Member
where LoginName like 'Te%' and DeleteFlag = 1
• 例えば、上記のクエリを使った処理を
新しくリリースした場合を考慮する
付加列にしたインデックスの場合
・既存のインデックスを一度DROPして作り直せばOK
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName])
INCLUDE ([Sei], [Mei])
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName],
[DeleteFlag])
INCLUDE ([Sei], [Mei])
・ひとつのインデックスで2種類のクエリに対応可能
CREATE NONCLUSTERED INDEX [IX_Memaber_LoginName] ON [dbo].[Member]
([LoginName], [DeleteFlag])
INCLUDE ([Sei], [Mei])
select Sei, Mei from Member where LoginName like 'Te%' and DeleteFlag = 1
select Sei, Mei from Member where LoginName = 'Test'
付加列にしないインデックスの場合
• 追加で別のインデックスを作るしかなくなる
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName],
[Sei], [Mei])
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName],
[DeleteFlag], [Sei], [Mei])
• このインデックスにまとめようとするのはNG。なぜか?
• インデックスを修正する時点でプロダクション環境で
実行されている全クエリを把握するのは難しい
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName],
[Sei], [Mei])
select * from Member where LoginName = 'Test' and Sei = 'aaa' and Mei = 'bbb'
• この既存インデックスだけみると、下記のようなクエリが
すでに実行されていると考えるのが妥当
・よってインデックスを増やすしかなくなる
• 結果的に以下のふたつのインデックスを作成する必要が出てくる
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member]
([LoginName], [Sei], [Mei])
CREATE NONCLUSTERED INDEX [IX_Member_LoginName2] ON [dbo].[Member]
([LoginName], [DeleteFlag], [Sei], [Mei])
• 無駄な容量や更新時のコスト増につながる
・一般化:良いselectivityが得られる最小カラム構成をとる
例:MemberテーブルはLoginNameでUniqueとなる性質がある
↓これが0レコード
select LoginName from Member group by LoginName having count(*) > 1
select Sei, Mei from Member where LoginName = 'Test' and DeleteFlag = 1
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName])
INCLUDE ([Sei], [Mei], [DeleteFlag])
・したがって、上記のクエリのインデックスは下記でOK
インデックスの設計タイミング
• 新機能作成に伴ってテーブルが追加される場合
1.必要なクエリをすべて作成
2.各クエリに最適なインデックスを設計する(個別最適)
3.2で設計したインデックスをできる限りまとめる(全体最適)
• 既存クエリをチューニングする場合
1.チューニングしたいクエリに最適なインデックスを設計(個別最適)
2.既存インデックスとまとめられる場合はまとめる(全体最適)
全体最適なインデックス設計のまとめ
• 良いselectivityが得られる最小カラム構成をとる
• SELECT句でしか使わないカラムは基本付加列にする
• WHERE句で使っているカラムでも、
それ以外のカラムで良いselectivityが得られるなら付加列でOK
• 理由:インデックスの拡張性を保ち、できるだけ少ない
インデックスで多くのクエリに対応できるため
Chapter 8
Lesson 6
クエリを書くときのポイント
ポイント①:アドホッククエリを避ける
• アドホッククエリ
• where句などで値が直接指定されているクエリのこと
• SQLインジェクションの危険性
• 毎回コンパイルされる可能性が高くCPU負荷増、実行時間増に
つながりやすい
• 1文字でも異なれば各クエリすべてがキャッシュされるため
メモリ効率が悪い
select * from Member where PrefectureID = 2
開発時の基本方針
• パラメータ化クエリまたはストアドプロシージャを使用する
• パラメータごとのselectivityが大きく異なる場合は
パラメータスニッフィングによる実行速度の低下に注意する
• クエリプランの後退にも注意する
パラメータごとのselectivity
例:1,000万レコードのテーブル
• select * from Member where PrefectureID = 1
→ 9,999,995レコード
• select * from Member where PrefectureID = 2
→ 1レコード
• select * from Member where PrefectureID = 3
→ 1レコード
• select * from Member where PrefectureID = 4
→ 1レコード
• select * from Member where PrefectureID = 5
→ 1レコード
• select * from Member where PrefectureID = 6
→ 1レコード
「PrefectureID = 1」だけ
selectivityが大きく異なる
パラメータスニッフィングとは
コンパイル時に受け取ったパラメータにとって最適な実行プランを
生成する挙動のこと
declare @PrefID int = 1
select top 10 * from Member where PrefectureID = @PrefID
IndexScanの実行プランがキャッシュされ、@PrefIDで別の値が指定されても
IndexScanでクエリ実行される
@PrefID=1の場合はテーブルの99%以上のレコードが取得されるためIndexScan
パラメータスニッフィングによる実行速度の低下
• @PrefID=2の場合:1レコード取得のためIndexSeekが理想
• @PrefID=1に最適なプランがキャッシュされているため、
IndexScanで実行されてしまい実行速度が低下する
実行速度の低下に対する対策
リコンパイルヒントを付けて毎回コンパイルする
• ストアドプロシージャ:with recompile
• パラメータ化クエリ:option (recompile)
クエリプランの後退とは
本来はもっと高速に実行できる実行プランがあるのに
低速な別の実行プランで実行されてしまう現象のこと
⇒ いつもは問題なく実行できているクエリがなにもしていないのに
突然遅くなった場合はクエリプランの後退を疑う
クエリプランの後退への対策
• クエリで使っている統計情報を更新する
• 該当クエリをリコンパイルする
• DBCC FREEPROCCACHE (plan_handle)
• リコンパイルヒントを付けて毎回コンパイルする
• ストアドプロシージャ:with recompile
• パラメータ化クエリ:option (recompile)
ポイント②:インデックスが効かないケース
• 暗黙の型変換
• where col1 = 1234 -- col1がchar(4)の場合は’1234’にすべき
• カラムを加工する
• where (col1*3) = 5
• where func(col1) = 5
• like ‘%***%’とする
ポイント③:ヒント句は使わない
• ヒント句は使わず、基本的にはオプティマイザに任せる
• 以下のヒント句は場合によっては有効な場合もある
• option (maxdop 10) : 並列クエリの多重度を変更する
• with(index(index_name)):指定したインデックスの使用を強制
• with(forceseek):index seekを強制
Chapter 8
Lesson 7
全体最適なインデックスの
練習問題
問題:全体最適な観点でインデックスを再設計
してください
①
CREATE INDEX [IX_Member_Tel] ON [Member] ([tel]) INCLUDE ([RegistDate])
②
CREATE INDEX [IX_Member_LoginName] ON [Member] ([LoginName]) INCLUDE ([Sei], [Mei])
③
CREATE INDEX [IX_Member_LoginName2] ON [Member] ([LoginName])
INCLUDE ([GenderID], [PrefectureID])
④
CREATE INDEX [IX_Member_DeleteFlag_RegistDate] ON [Member] ([DeleteFlag], [RegistDate])
⑤
CREATE INDEX [IX_Member_LoginName_HashedPassword] ON [Member] ([LoginName],
[HashedPassword]) INCLUDE ([DeleteFlag])
⑥
CREATE INDEX [IX_Member_Tel_DeleteFlag] ON [Member] ([tel], [DeleteFlag])
この問題の考え方
各インデックスが使っているクエリの速度を落とさずに
できるだけインデックスの数を減らす
解説①
① + ⑥
CREATE INDEX [IX_Member_Tel_DeleteFlag] ON [Member] ([tel], [DeleteFlag])
INCLUDE ([RegistDate])
①
CREATE INDEX [IX_Member_Tel] ON [Member] ([tel]) INCLUDE ([RegistDate])
⑥
CREATE INDEX [IX_Member_Tel_DeleteFlag] ON[Member] ([tel], [DeleteFlag])
解説②
② + ③ + ⑤
CREATE INDEX [IX_Member_LoginName] ON [Member] ([LoginName], [HashedPassword])
INCLUDE ([Sei], [Mei], [GenderID], [PrefectureID], [DeleteFlag])
②
CREATE INDEX [IX_Member_LoginName] ON [Member] ([LoginName]) INCLUDE ([Sei], [Mei])
③
CREATE INDEX [IX_Member_LoginName2] ON [Member] ([LoginName])
INCLUDE ([GenderID], [PrefectureID])
⑤
CREATE INDEX [IX_Member_LoginName_HashedPassword] ON [Member] ([LoginName], [HashedPassword])
INCLUDE ([DeleteFlag])
最終的な回答
① + ⑥
CREATE INDEX [IX_Member_Tel_DeleteFlag] ON [Member] ([tel], [DeleteFlag])
INCLUDE ([RegistDate])
② + ③ + ⑤
CREATE INDEX [IX_Member_LoginName] ON [Member] ([LoginName], [HashedPassword])
INCLUDE ([Sei], [Mei], [GenderID], [PrefectureID], [DeleteFlag])
④
CREATE INDEX [IX_Member_DeleteFlag_RegistDate] ON [Member] ([DeleteFlag], [RegistDate])

SQL Server Performance Tuning Essentials

Editor's Notes

  • #163 デモ:想定1分
  • #218 使ったクエリ https://github.com/masaki-hirose/SQL-Server-Performance-Tuning/blob/master/EvaluationQuery.sql select top (100) qt.text as parent_query ,SUBSTRING(qt.text, qs.statement_start_offset / 2, (case when qs.statement_end_offset = - 1 then LEN(CONVERT(nvarchar(MAX), qt.text)) * 2 else qs.statement_end_offset end - qs.statement_start_offset) / 2) as statement -- average ,total_worker_time / qs.execution_count / 1000 as average_cpu_time_ms ,total_elapsed_time / qs.execution_count / 1000 as average_duration_ms ,total_physical_reads / qs.execution_count / 1000 as average_physical_reads_ms -- execution count ,qs.execution_count as execution_count -- creation / execution time ,last_execution_time ,creation_time -- total ,total_worker_time / 1000 as total_cpu_time_ms ,total_elapsed_time / 1000 as total_duration_ms ,total_physical_reads / 1000 as total_physical_reads_ms -- query plan ,qp.query_plan from sys.dm_exec_query_stats qs outer apply sys.dm_exec_sql_text(qs.sql_handle) as qt outer apply sys.dm_exec_query_plan(plan_handle) as qp where qt.text like '%%' --filtering by text order by total_worker_time desc