關於MS SQLSERVER索引優化問題

**********************************************************

Author:黃山光明頂

mail:[email protected]

version:1.0.0

date:2004-1-30

***********************************************************有一網友問:關於MS SQLSERVER索引優化問題:
 有表Stress_test(id  int, key char(2))
        id 上有普通索引;
        key 上有簇索引;
        id 有有限量的重複;
        key 有無限量的重複;

現在我需要按邏輯與查詢表中key='Az' AND key='Bw' AND key='Cv' 的id

求教高手最有效的查詢語句

測試環境:
     Hardware:P4 2.6+512M+80G
     Software:windows server 2003(Enterprise Edition)+Sqlserver 2000 +sp3a

  首先我們建立一個測試的數據,爲使數據儘量的分佈和隨即,我們通過RAND()來隨機產生2個隨機數再組合成一個字符串,首先插入的數據是1,000,000條記錄,然後在循環插入到58,000,000條記錄。
   因爲是隨機產生的數據,所以如果你自己測試的數據集和我測試的會不一樣,但對索引的優化和運行的效率是一樣的。
   下面的“--//測試腳本”是產生測試數據的腳本,你可以根據需要修改 @maxgroup, @maxLoop的值,比如測試1百萬的記錄可以:

     Select @maxgroup=1000
     Select @maxLoop=1000

如果要測試5千萬:

     Select @maxgroup=5000
     Select @maxLoop=10000

所以如果你的SERVER或PC比較慢,請耐心等待.....,
 (在我的PC上運行的速度是插入1百萬條的時間是1.14m,插入5千八百萬條的時間是19.41m,重新建立INDEX的時間是34.36m)

 

作爲一般的開發人員很容易就想到的語句:

   --語句1

    select a.[id] from 
    (select distinct [id] from stress_test where [key] = 'Az') a,
    (select distinct [id] from stress_test where [key] = 'Bw') b ,
    (select distinct [id] from stress_test where [key] = 'Cv') c
    where a.id = b.id and a.id = c.id

   --語句2

     select [id] 
     from stress_test 
     where [key]='Az' or [key]='Bw' or [key]='Cv'
     group by id having(count(distinct [key])=3)
  

   --語句5

    SELECT distinct a.[id] FROM stress_test AS a,stress_test AS b,stress_test AS c
    WHERE a.[key]='Az' AND b.[key]='Bw' AND c.[key]='Cv'
      AND a.[id]=b.[id] AND a.[id]=c.[id]

但作爲T-SQL的所謂“高手”可能會認爲這種寫法很“土”,也顯得沒有水平,所以會選擇一些子查詢和外連接的寫法,按常理子查詢的效率是比較高的:

   --語句3

    select distinct [id] from stress_test A where
    not exists (
    select 1 from
     (select 'Az' as k union all select 'Bw' union all select 'Cv') B
    left join stress_test C on  C.id=A.id and B.[k]=C.[key]
    where C.id is null)

   --語句4

     select distinct a.id from stress_test a
     where not exists
     ( select * from keytb c
      where not exists
      ( select * from stress_test b
       where
             b.id = a.id
             and
             c.kf1 = b.[key]
       )
     )

我們先分析這幾條語句(針對5千8百萬條數據進行分析):

請大家要特別留心Estimated row count的值。

語句1:從執行規劃中我們可以看出,MSSQLSERVER選擇的索引優化非常有規律,先通過CLUSTERED INDEX篩選出符合[KEY]='Az'條件的ID,然後進行HASH MATCH,在找出ID相等的;依次類推最終檢索到符合所有條件的記錄。中間的Estimated row count的值都不大。

語句2:從執行規劃中我們可以看出,是先通過CLUSTERED INDEX篩選出符合 [key]='Az' or [key]='Bw' or [key]='Cv' 符合所有條件的ID,然後分組進行2次HASH MATCH 所有的ID。我們可以看出Estimated row count的值是越來越少,從最初的369,262到最後排序的只有402。

語句3:從執行規劃中我們可以看是非常複雜的,是先通過3組 通過CONSTANT SCAN和NON-CLUSTERED INDEX檢索出符合 A.ID=C.ID AND [key]='**' 的記錄3組,然後分組進行外鍵匹配,再將3組的數據合併,排序,然後再和一個NON-CLUSTERED INDEX檢索出的記錄集進行外鍵匹配,我們可以看出MSSQLSERVER會對所有的記錄(5千萬條)記錄進行分組,Estimated row count的值是:58,720,000,所以這句T-SQL的瓶頸是對5千萬條記錄進行分組。

語句4:從執行規劃中我們可以看和語句3有相似之處,都要對所有的記錄(5千萬條)記錄進行分組,所以這是檢索的瓶頸,而且使用的索引都是NON-CLUSTERED INDEX。

語句5:從執行規劃中我們可以看出,先通過CLUSTERED INDEX檢索出符合[Key]='Az'的記錄集,然後進行HASH MATCH和SORTS,因爲數量少所以是非常會的,在和通過NON-CLUSTERED INDEX檢索[KEY]='Bw'的記錄進行INNER JOIN,在和通過CLUSTERED INDEX檢索[KEY]='Cv'的記錄進行合併,最後是對4百萬條數據進行分組檢索,如果是6列,我們可以看出Estimated row count的值是遞增,越來越大,最後的分組檢索的Estimated row count的值是3.46E+15,這已經形成巨大的瓶頸。

我們測試百萬條以上的記錄:
 1.先對1百萬條記錄進行測試(選取3列)
 2.先對1百萬條記錄進行測試(選取6列)
 3.對5千萬條數據測試(選取3列)
 4.對5千萬條數據測試(選取6列)

統計表1:
 ----------------------------------------------------------------------
 |------------------語句 1----語句 2----語句 3----語句 4----語句 5----|
 | 1百萬(3列)    0.77%     0.41%    49.30%     48.99%     0.52%
 | 1百萬(6列)     1.61%     0.81%    48.99%     47.44%     1.14%
 | 5千萬(3列)     0.14%     0.18%    48.88%     48.86%     1.93%
 | 5千萬(6列)     0.00%     0.00%     0.00%      0.00%   100.00%
統計表2:
 ----------------------------------------------------------------------
 |------------------語句 1----語句 2----語句 3----語句 4----語句 5----|
 | 1百萬(3列)     9ms       22ms     723ms     753ms      4ms
 | 1百萬(6列)      15ms      38ms     764ms     773ms     11ms
 | 5千萬(3列)     575ms     262ms  110117ms  110601ms  12533ms
 | 5千萬(6列)    1070ms     576ms  107988ms  109704ms     10m以上


測試總結:(我們可以比較關注:語句 2和語句 5)
1.在1百萬條記錄的情況下,語句 5是最快的,但在5千萬條記錄下是最慢的。這說明INDEX的優化一定的情況下,數據量不同,檢索的效率也是不同的。我們平時在寫T-SQL時一般關注的時INDEX的使用,只要我們寫的T-SQL是利用CLUSTERED INDEX,我們就認爲是最優化了,其實這是一個誤區,我們還要關注Estimated row count的值,大量的I/O操作是我們應該關注的,所以我們應該根據數據量的不同選擇相應的T-SQL語句,不要認爲在小數據量下是最高的在大數據量的狀態下也許是最慢的:-(。

2.在執行規劃中最快的,並不是運行最快的,我們可以看在1百萬(6列)在這行中,語句 2和語句 5的比例是0.81%:1.14%,但實際的運行效率是,38ms:11ms。所以,我們在選擇T-SQL是要考慮本地I/O的速度,所以在優化語句時不僅要看執行規劃還要計算一下具體的效率。

在測試的語句上加入:

    SET STATISTICS TIME  ON/OFF
    SET STATISTICS IO  ON/OFF
是一個很好的調試方法。


3.綜合評價,語句 2的效率是最高的,執行效率沒有隨數據量變化而有很大的差別。

4.執行規劃越簡單的語句(語句1),綜合效率越高,反之則越低(語句3,語句4)。

5.在平時寫T-SQL語句時,一定要根據不同的數據量進行測試,雖然都是用CLUSTERED INDEX,但檢索的效率卻大相徑庭。

--//測試腳本
USE Northwind
GO
if exists(select * from sysobjects where name=N'stress_test' and type='U')
Drop table stress_test
GO
--//定義測試的表stress_test,存放所有的測試數據
Create table stress_test([id] int,[key] char(2))

GO
--//插入測試的數據
Set nocount on
--//變量定義
Declare @id int   --//Stress_test ID 值
Declare @key char(2)  --//Stress_test [key] 值
Declare @maxgroup int  --//組最大的循環數
Declare @maxLoop int  --//ID最大的循環數
Declare @tempGroup int  --//臨時變量
Declare @tempLoop int  --//臨時變量
Declare @tempint1 int  --//臨時變量
Declare @tempint2 int  --//臨時變量
Declare @rowcount int  --//記錄事務提交的行數

--//初始化變量
Select @id=1
Select @maxgroup=1000
Select @maxLoop=1000
Select @tempGroup=1
Select @tempLoop=1
Select @key=''
Select @rowcount=0

while @tempLoop<=@maxLoop
begin
 while @tempGroup<=@maxGroup
 begin
  select @tempint1=65+convert(int,rand()*50)
  select @tempint2=65+convert(int,rand()*100)
  if (@tempint1>=122 or @tempint2>=122)
    begin
    select @tempint1=@tempint1-100
    select @tempint2=@tempint2-100
   
    if (@tempint1<=65 or @tempint2<=65)
     begin
     select @tempint1=@tempint1+57
     select @tempint2=@tempint2+57
    end
   end
  select @key=char(@tempint1)+char(@tempint2)
  if @rowcount=0
  begin tran ins
     insert into stress_test([id],[key])values(@id,@key)
       select @rowcount=@rowcount+1
  
   if @rowcount>3000 --//判斷當行數達到3000條時,開始提交事務
   begin
       commit tran ins
      select @rowcount=0
   end
  
  select @tempGroup=@tempgroup+1
 end
 if @rowcount>0
 begin
  commit tran ins
  select @rowcount=0
 end

 select @tempGroup=1
 select @id=@id+1
 select @tempLoop=@tempLoop+1
end
GO
--//刪除KEY值爲NULL的記錄
delete stress_test where [key]is null
GO
--//建立簇索引PK_STRESS
Create Clustered index pk_stress on stress_test([Key])
--//建立非簇索引NI_STRESS_ID
Create NonClustered index NI_stress_id on stress_test([id])
GO
--//定義測試的表keytb
if exists(select * from sysobjects where name=N'keytb' and type='U')
Drop table keytb
GO
create table keytb   -----//存放你需要匹配的值的表
(
  kf1  varchar(20)
)

--//存放你需要匹配的值,暫定爲三個
insert into keytb(kf1) values('Az');
insert into keytb(kf1) values('Bw');
insert into keytb(kf1) values('Cv');

--insert into keytb(kf1) values('Du');
--insert into keytb(kf1) values('Ex');
--insert into keytb(kf1) values('Fy');
GO


下面我們就開始測試幾種T-SQL的INDEX優化問題:

--先對1百萬條/1億條記錄進行測試(選取3列)的T-SQL:

PRINT '第一種語句:'
SET STATISTICS TIME  ON
SET STATISTICS IO  ON
select a.[id] from
(select distinct [id] from stress_test where [key] = 'Az') a,
(select distinct [id] from stress_test where [key] = 'Bw') b ,
(select distinct [id] from stress_test where [key] = 'Cv') c
where a.id = b.id and a.id = c.id
GO
PRINT '第二種語句:'
select [id]
from stress_test 
where [key]='Az' or [key]='Bw' or [key]='Cv'
group by id having(count(distinct [key])=3)
GO
PRINT '第三種語句:'
select distinct [id] from stress_test A where
not exists (
select 1 from
(select 'Az' as k union all select 'Bw' union all select 'Cv') B
left join stress_test C on  C.id=A.id and B.[k]=C.[key]
where C.id is null)
GO
PRINT '第四種語句:'
select distinct a.id from stress_test a
 where not exists
 ( select * from keytb c
   where not exists
   ( select * from stress_test b
     where
       b.id = a.id
       and
       c.kf1 = b.[key]
   )
 )
GO
PRINT '第五種語句:'
SELECT distinct a.[id] FROM stress_test AS a,stress_test AS b,stress_test AS c
WHERE a.[key]='Ac' AND b.[key]='Bb' AND c.[key]='Ca'
      AND a.[id]=b.[id] AND a.[id]=c.[id]

GO
SET STATISTICS TIME  OFF
SET STATISTICS IO  OFF

--先對1百萬條/1億條記錄進行測試(選取6列)的T-SQL:
PRINT '第一種語句:'
SET STATISTICS TIME  ON
SET STATISTICS IO  ON
select a.[id] from
(select distinct [id] from stress_test where [key] = 'Az') a,
(select distinct [id] from stress_test where [key] = 'Bw') b ,
(select distinct [id] from stress_test where [key] = 'Cv') c,
(select distinct [id] from stress_test where [key] = 'Du') d,
(select distinct [id] from stress_test where [key] = 'Ex') e,
(select distinct [id] from stress_test where [key] = 'Fy') f
where a.[id] = b.[id] and a.[id] = c.[id] and a.[id] = d.[id] and a.[id] = e.[id] and a.[id] = f.[id]
GO
PRINT '第二種語句:'
select [id]
from stress_test 
where [key]='Az' or [key]='Bw' or [key]='Cv' or [Key]='Du'or [Key]='Ex'or [Key]='Fy'
group by id having(count(distinct [key])=6)
GO
PRINT '第三種語句:'
select distinct [id] from stress_test A where
not exists (
select 1 from
(select 'Az' as k union all select 'Bw' union all select 'Cv'union all select 'Du'union all select 'Ex'union all select 'Fy') B
left join stress_test C on  C.id=A.id and B.[k]=C.[key]
where C.id is null)
GO
PRINT '第四種語句:'
select distinct a.id from stress_test a
 where not exists
 ( select * from keytb c
   where not exists
   ( select * from stress_test b
     where
       b.id = a.id
       and
       c.kf1 = b.[key]
   )
 )
GO
PRINT '第五種語句:'
SELECT distinct a.[id] FROM stress_test AS a,stress_test AS b,stress_test AS c,stress_test AS d,stress_test AS e,stress_test AS f
WHERE a.[key]='Az' AND b.[key]='Bw' AND c.[key]='Cv' AND d.[key]='Du' AND e.[key]='Ex' AND f.[key]='Fy'
     and a.[id] = b.[id] and a.[id] = c.[id] and a.[id] = d.[id] and a.[id] = e.[id] and a.[id] = f.[id]

GO
SET STATISTICS TIME  OFF
SET STATISTICS IO  OFF 

我們可以先測試一下小的數據量(50000條);

大家可以下面測試腳本的:

   Select @maxgroup=500
   Select @maxLoop=100

----------------------------------------------------------------------
 |------------------語句 1----語句 2----語句 3----語句 4----語句 5----|
 | 5萬(3列)        5ms       19ms     37ms     59ms      0ms
 | 5萬(6列)        1ms       26ms     36ms     36ms     1ms
 

從測試的的數據來看,語句5的效率是最高的,幾乎沒有花費時間,而語句2的效率只能說是一般。如果測試到這裏就結束了,我們可以毫不猶豫的選擇語句 5 :-(,繼續進行下面的測試.....

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章