INDEX-onlyとかカバリングインデックスとか
ちょっと前までT字形で出てくるINDEX-only(http://www.sdi-net.co.jp/sdi_228.htm)ってよくわかっていなかったんですが、MSの説明だとクエリをカバーするインデックス(カバリングインデックス)とか包括するクエリとかいうのにに相当するみたいです。ああ、そういうことなんだーと思いました。
http://www.microsoft.com/japan/msdn/sqlserver/columns/sysbuild/sysbuild3.aspx#sysbuild3-6
http://msdn2.microsoft.com/ja-jp/library/ms179325.aspx
T字型のページにもMSのページにも単一テーブルへのクエリの例がのっていますが、JOINしたときの例がないので試してみます。
EmployeeテーブルとDepartmentテーブルとこれらの関連テーブルとしてBelongToを用意して、BelongToテーブルにインデックスをつけたりつけなかったりしてみます。
(テーブルの構造はすらすらと手が動くようになるSQL書き方ドリルからインスパイアされました)
カバリングインデックスだけに着目したいので今回はクラスタ化インデックスはすべてデフォルトのまま主キーにつけときます。
まずテーブルの定義とサンプルデータを作成します。
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[BelongTo]') AND type in (N'U')) DROP TABLE BelongTo IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Employee]') AND type in (N'U')) DROP TABLE Employee IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Department]') AND type in (N'U')) DROP TABLE Department GO CREATE TABLE [Employee] ( [EmployeeID] int IDENTITY(1,1) PRIMARY KEY NOT NULL, [EmployeeNo] nvarchar(32) NOT NULL ) CREATE TABLE [Department] ( [DepartmentID] int IDENTITY(1,1) PRIMARY KEY NOT NULL, [DepartmentNo] nvarchar(32) NOT NULL ) CREATE TABLE [BelongTo] ( [BelongToID] int IDENTITY(1,1) PRIMARY KEY NOT NULL, [EmployeeID] int NOT NULL, [DepartmentID] int NOT NULL, [StartDate] datetime, [EndDate] datetime ) ALTER TABLE [BelongTo] WITH CHECK ADD CONSTRAINT [FK_BelongTo_Employee] FOREIGN KEY([EmployeeID]) REFERENCES [Employee] ([EmployeeID]) ALTER TABLE [BelongTo] WITH CHECK ADD CONSTRAINT [FK_BelongTo_Department] FOREIGN KEY([DepartmentID]) REFERENCES [Department] ([DepartmentID]) GO --サンプルデータの作成 SET NOCOUNT ON DECLARE @i int DECLARE @j int DECLARE @EmployeeID int DECLARE @DepartmentID int SET @i = 0 SET @j = 0 WHILE (@i < 10) BEGIN INSERT INTO Department ([DepartmentNo]) VALUES (N'Department' + CAST(@i as varchar)) SET @DepartmentID = SCOPE_IDENTITY() WHILE (@j < 100) BEGIN INSERT INTO Employee ([EmployeeNo]) VALUES (N'Employee' + CAST(@i as varchar) + '-'+ CAST(@j as varchar)) SET @EmployeeID = SCOPE_IDENTITY() INSERT INTO BelongTo (EmployeeID, DepartmentID, StartDate) VALUES (@EmployeeID, @DepartmentID, GetDate()) SET @j = @j + 1 END SET @i = @i + 1 SET @j = 0 END GO
実行するクエリは次のものにします。
SELECT Department.DepartmentNo FROM Employee INNER JOIN BelongTo ON (Employee.EmployeeID = BelongTo.EmployeeID) INNER JOIN Department ON (BelongTo.DepartmentID = Department.DepartmentID) WHERE Employee.EmployeeNo = 'Employee8-45'
3パターン試してみます。
パターン1:何のインデックスをつけない場合
実行計画は次のようになりました。インデックスがないのでHash Joinが使われたようです。
|--Nested Loops(Inner Join, OUTER REFERENCES:([sql].[dbo].[BelongTo].[DepartmentID])) |--Hash Match(Inner Join, HASH:([sql].[dbo].[Employee].[EmployeeID])=([sql].[dbo].[BelongTo].[EmployeeID])) | |--Clustered Index Scan(OBJECT:([sql].[dbo].[Employee].[PK__Employee__02925FBF]), WHERE:([sql].[dbo].[Employee].[EmployeeNo]=N'Employee8-45')) | |--Clustered Index Scan(OBJECT:([sql].[dbo].[BelongTo].[PK__BelongTo__0662F0A3])) |--Clustered Index Seek(OBJECT:([sql].[dbo].[Department].[PK__Department__047AA831]), SEEK:([sql].[dbo].[Department].[DepartmentID]=[sql].[dbo].[BelongTo].[DepartmentID]) ORDERED FORWARD)
パターン2:BelongToのEmployeeIDにインデックスをつけた場合
インデックをつけてクエリを実行します。
CREATE NONCLUSTERED INDEX [IDX_BelongTo_EmployeeID] ON [BelongTo] ( [EmployeeID] ASC )WITH (PAD_INDEX = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF) ON [PRIMARY]
実行計画は次のようになりました。
|--Nested Loops(Inner Join, OUTER REFERENCES:([sql].[dbo].[BelongTo].[DepartmentID])) |--Nested Loops(Inner Join, OUTER REFERENCES:([sql].[dbo].[BelongTo].[BelongTo])) | |--Nested Loops(Inner Join, OUTER REFERENCES:([sql].[dbo].[Employee].[EmployeeID])) | | |--Clustered Index Scan(OBJECT:([sql].[dbo].[Employee].[PK__Employee__5C6CB6D7]), WHERE:([sql].[dbo].[Employee].[EmployeeNo]=N'Employee8-45')) | | |--Index Seek(OBJECT:([sql].[dbo].[BelongTo].[IDX_BelongTo_EmployeeID]), SEEK:([sql].[dbo].[BelongTo].[EmployeeID]=[sql].[dbo].[Employee].[EmployeeID]) ORDERED FORWARD) | |--Clustered Index Seek(OBJECT:([sql].[dbo].[BelongTo].[PK__BelongTo__603D47BB]), SEEK:([sql].[dbo].[BelongTo].[BelongTo]=[sql].[dbo].[BelongTo].[BelongTo]) LOOKUP ORDERED FORWARD) |--Clustered Index Seek(OBJECT:([sql].[dbo].[Department].[PK__Department__5E54FF49]), SEEK:([sql].[dbo].[Department].[DepartmentID]=[sql].[dbo].[BelongTo].[DepartmentID]) ORDERED FORWARD)
パターン3:BelongToにEmployeeIDとDepartmentIDの複合インデックス(DepartmentIDをカバーするインデックス)をつけた場合
インデックをつけてクエリを実行します。
CREATE UNIQUE NONCLUSTERED INDEX [IDX_BelongTo_EmployeeID_DepartmentID] ON [BelongTo] ( [EmployeeID] ASC, [DepartmentID] ASC )WITH (PAD_INDEX = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF) ON [PRIMARY]
実行計画は次のようになりました。
|--Nested Loops(Inner Join, OUTER REFERENCES:([sql].[dbo].[BelongTo].[DepartmentID])) |--Nested Loops(Inner Join, OUTER REFERENCES:([sql].[dbo].[Employee].[EmployeeID])) | |--Clustered Index Scan(OBJECT:([sql].[dbo].[Employee].[PK__Employee__4D2A7347]), WHERE:([sql].[dbo].[Employee].[EmployeeNo]=N'Employee8-45')) | |--Index Seek(OBJECT:([sql].[dbo].[BelongTo].[IDX_BelongTo_EmployeeID_DepartmentID]), SEEK:([sql].[dbo].[BelongTo].[EmployeeID]=[sql].[dbo].[Employee].[EmployeeID]) ORDERED FORWARD) |--Clustered Index Seek(OBJECT:([sql].[dbo].[Department].[PK__Department__4F12BBB9]), SEEK:([sql].[dbo].[Department].[DepartmentID]=[sql].[dbo].[BelongTo].[DepartmentID]) ORDERED FORWARD)
パターン2と3の違いをみてみます。パターン2ではBelongToテーブルに対するClustered Index Seekがありますがパターン3ではないです。パターン3ではインデックスIDX_BelongTo_EmployeeID_DepartmentIDにアクセスするだけでDepartmentIDがわかるのでBelongToテーブルのデータページにアクセスする必要(BelongToテーブルに対してClustered Index Seekする必要)がなかったということですね、たぶん。
ちなみにサブツリーの推定コストはそれぞれ次のようになりました。
- パターン1: 0.0415365
- パタンー2: 0.0184275
- パターン3: 0.0151403
この例ではあまり差がないですけど、もっとデータが多くなって結合するテーブルも増えれば差がはっきりしそうな気がします。