第175回 CLUB DB2「アクセスプラン(実行計画)の読み方入門」の資料です。
https://www.ibm.com/developerworks/community/wikis/home?lang=ja#!/wiki/ClubDB2/page/%E7%AC%AC175%E5%9B%9E
第175回 CLUB DB2「アクセスプラン(実行計画)の読み方入門」の資料です。
https://www.ibm.com/developerworks/community/wikis/home?lang=ja#!/wiki/ClubDB2/page/%E7%AC%AC175%E5%9B%9E
49. ひとつずつ実行してみよう
select * from Member where MemberID = 18629768
select * from Member where LoginName = 'Janita1317'
どちらも使われるインデックスは同じ
⇒ インデックスはどう使われたのか?
52. 矢印の向きが違うだけなのになぜ速い/遅い?
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
69. 複合インデックス
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
作成時に指定したカラムの順序で
インデックスキーが作成される
70. 複合インデックスを使用した検索
インデックスキーに指定したカラムの順序が検索効率に影響する
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で検索が行われないケース
⇒ インデックスの先頭には検索に使われる頻度の多い項目を指定する
74. カバリングインデックス
• 上記ようなインデックスが作成されている場合
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member]
([LoginName] ASC) INCLUDE (RegistDate)
キー参照が発生しないインデックスを「カバリングインデックス」
SELECT LoginName, RegistDate FROM Member WHERE LoginName = 'Tawny265167'
インデックスキー
インデックスキー INCLUDE
カラム
クエリに必要なカラムをカバーするインデックスになっている
=カバリングインデックス
76. カバリングインデックスのポイント
• カバリングかどうかはクエリごとに決まる
• インデックスは下記のクエリに対して「カバリング」
• インデックスは 下記のクエリに対して「カバリングではない」
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'
94. 事前準備:各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
95. 各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
160. 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
162. 通常はレコード数 >> サンプル
数
• 先ほどの例はサンプリング率100%(フルスキャン)
update statistics member
update statistics member with fullscan
• with fullscan無し:レコード数に応じて自動でサンプリング率が決定
174. 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用インデックス
184. 元クエリ
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を検索
述語に追加
193. 問題:インデックスを設計してください
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
196. 問題:インデックスを設計してください
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
223. インデックス作成のアンチパターン
各クエリごとに最適なインデックスを作成する
⇒ 似たインデックスが大量に作成される
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)
224. 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)
225. 意図した使われ方をしているか確認する
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
227. 必要最小限のインデックスを設計する
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])
229. 付加列にしたインデックスの場合
・既存のインデックスを一度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種類のクエリに対応可能
230. 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'
231. 付加列にしないインデックスの場合
• 追加で別のインデックスを作るしかなくなる
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。なぜか?
232. • インデックスを修正する時点でプロダクション環境で
実行されている全クエリを把握するのは難しい
CREATE NONCLUSTERED INDEX [IX_Member_LoginName] ON [dbo].[Member] ([LoginName],
[Sei], [Mei])
select * from Member where LoginName = 'Test' and Sei = 'aaa' and Mei = 'bbb'
• この既存インデックスだけみると、下記のようなクエリが
すでに実行されていると考えるのが妥当
・よってインデックスを増やすしかなくなる
233. • 結果的に以下のふたつのインデックスを作成する必要が出てくる
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])
• 無駄な容量や更新時のコスト増につながる
240. パラメータごとの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が大きく異なる
249. 問題:全体最適な観点でインデックスを再設計
してください
①
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])
251. 解説①
① + ⑥
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])
252. 解説②
② + ③ + ⑤
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])
253. 最終的な回答
① + ⑥
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])
#218: 使ったクエリ
https://github.com/masaki-hirose/SQL-Server-Performance-Tuning/blob/master/EvaluationQuery.sqlselect 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