1. 程式人生 > >十五週翻譯《Pro SQL Server Internals, 2nd edition》 CHAPTER 7

十五週翻譯《Pro SQL Server Internals, 2nd edition》 CHAPTER 7

文章來自《Pro SQL Server Internals, 2nd edition》的CHAPTER 7 Designing and Tuning the Indexes中的Clustered Index Design Considerations

原作者:Dmitri Korotkevitch

 

第七章  設計和調整索引

無法定義可在任何地方使用的索引策略。 每個系統都是獨特的,需要它自己的索引方法基於工作量,業務需求和其他一些因素。但是,有幾個設計考慮因素和指南可以應用於每個系統。

當我們優化現有系統時也是如此。 雖然優化是一個迭代過程在每種情況下都是獨一無二的,有一套技術可用於檢測每種情況下的低效率資料庫系統。

     在本章中,我們將介紹在設計時需要牢記的幾個重要因素新索引和優化現有系統。

聚集索引設計注意事項

     每次更改聚簇索引鍵的值時,都會發生兩件事。首先,SQL Server移動了行到聚簇索引頁鏈和資料檔案中的不同位置。 其次,它更新了行id,這是聚集索引鍵。 儲存了行id,需要在所有非聚簇索引中更新。就I / O而言,這可能是昂貴的,特別是在批量更新的情況下。 而且,它可以增加聚簇索引的碎片,以及在行ID大小增加的情況下,非聚簇索引的碎片。 從而,最好有一個靜態聚簇索引,其中鍵值不會改變。

    所有非聚簇索引都使用聚簇索引鍵作為row-id。 一個太寬的聚簇索引鍵增加非聚簇索引行的大小,並需要更多空間來儲存它們。 結果,SQL Server需要在索引或範圍掃描操作期間處理更多資料頁,這使索引更少。

     在非唯一非聚簇索引的情況下,row-id也儲存在非葉索引級別,反過來,減少了每頁索引記錄的數量,並可能導致索引中的額外中間級別。儘管非葉索引級別通常快取在記憶體中,但這會引入額外的邏輯讀取每次SQL Server遍歷非聚集索引B-Tree時。

最後,較大的非聚簇索引在緩衝池中使用更多空間並引入更多開銷在索引維護期間。 顯然,不可能提供定義的通用閾值可應用於任何表的金鑰的最大可接受大小。 但是,作為一般規則,最好是具有窄聚簇索引鍵,索引鍵儘可能小。

定義唯一的聚簇索引也是有好處的。 這個原因重要但不明顯。 考慮到一種情況,表沒有唯一的聚簇索引,你希望在執行計劃中執行使用非叢集索引查詢的查詢。 在這種情況下,如果非聚簇索引中的行編號得到的就不是唯一的,SQL Server就不知道在鍵查詢操作期間要選擇哪個聚簇索引行。

SQL Server通過向非唯一聚簇索引中新增另一個名為uniquifier的可空整數列的操作來解決此類問題。對於第一次出現的鍵值,SQL Server用NULL填充uniquifiers,並對插入到表中的後續重複值進行自動遞增操作。 

注意:每個聚簇索引鍵值的可能重複項數量受整數域值的限制。使用相同的聚集索引鍵的行不能超過2,147,483,648。 這是理論上的限制,建立一個選擇性這麼差的索引顯然是個不好的方式。

讓我們看看在非唯一聚簇索引中引入uniquifiers的開銷。列表7-1所示的程式碼建立了三個相同結構的不同表,每個表都填充了65,536行。dbo表。表dbo.UniqueCI是唯一定義了唯一聚簇索引的表。表NonUniqueCINoDups沒有任何重複的鍵值。最後一個表dbo.NonUniqueCDups在索引中有大量重複項。

清單7-1 非唯一聚簇索引:表建立

create table dbo.UniqueCI

(

 KeyValue int not null,

 ID int not null,

 Data char(986) null,

 VarData varchar(32) not null

 constraint DEF_UniqueCI_VarData

 default 'Data'

);

create unique clustered index IDX_UniqueCI_KeyValue

on dbo.UniqueCI(KeyValue);

create table dbo.NonUniqueCINoDups

(

 KeyValue int not null,

 ID int not null,

 Data char(986) null,

 VarData varchar(32) not null

 constraint DEF_NonUniqueCINoDups_VarData

 default 'Data'

);

create /*unique*/ clustered index IDX_NonUniqueCINoDups_KeyValue

on dbo.NonUniqueCINoDups(KeyValue);

create table dbo.NonUniqueCIDups

(

 KeyValue int not null,

 ID int not null,

 Data char(986) null,

 VarData varchar(32) not null

 constraint DEF_NonUniqueCIDups_VarData

 default 'Data'

);

create /*unique*/ clustered index IDX_NonUniqueCIDups_KeyValue

on dbo.NonUniqueCIDups(KeyValue);

-- Populating data

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N5)

insert into dbo.UniqueCI(KeyValue, ID)

 select ID, ID from IDs;

insert into dbo.NonUniqueCINoDups(KeyValue, ID)

 select KeyValue, ID from dbo.UniqueCI;

insert into dbo.NonUniqueCIDups(KeyValue, ID)

 select KeyValue % 10, ID from dbo.UniqueCI;

  

現在,讓我們看一下每個表的聚簇索引的物理統計資訊。 其程式碼如下所示

清單7-2,結果如圖7-1所示。

 

  清單7-2 非唯一聚簇索引:檢查聚簇索引的行大小

select index_level, page_count, min_record_size_in_bytes as [min row size]

 ,max_record_size_in_bytes as [max row size]

 ,avg_record_size_in_bytes as [avg row size]

from

 sys.dm_db_index_physical_stats(db_id(), object_id(N'dbo.UniqueCI'), 1, null ,'DETAILED');

select index_level, page_count, min_record_size_in_bytes as [min row size]

 ,max_record_size_in_bytes as [max row size]

 , avg_record_size_in_bytes as [avg row size]

from

 sys. dm_db_index_physical_stats(db_id(), object_id(N'dbo.NonUniqueCINoDups'), 1, null

,'DETAILED');

select index_level, page_count, min_record_size_in_bytes as [min row size]

 ,max_record_size_in_bytes as [max row size]

 ,avg_record_size_in_bytes as [avg row size]

from

 sys. dm_db_index_physical_stats(db_id(), object_id(N'dbo.NonUniqueCIDups'), 1, null

,'DETAILED');  

圖7-1 非唯一聚簇索引:聚簇索引的行大小

 

即使dbo.NonUniqueCINoDups表中沒有重複的鍵值,該行仍然會新增兩個額外的位元組。 SQL Server將一個uniquifier儲存在資料的可變長度部分中,這兩個位元組就被新增到可變長度資料偏移陣列中的另一個條目中。

這種情況下,當聚簇索引具有重複值時,uniquifiers將再新增4個位元組,這就造成總共6個位元組的開銷。

值得一提的是,在某些邊緣情況下,uniquifier使用的額外儲存空間可以減少可以放入資料頁面的行數。我們的例子說明了這種情況。如你所見,dbo.UniqueCI使用的資料頁數比其他兩個表少15%。

現在,讓我們看看uniquifier如何影響非聚簇索引。列表7-3所示的程式碼在所有三個表中建立了非聚簇索引。圖7-2顯示了這些索引的物理統計資訊。

列表7-3 非唯一聚集索引:檢查非聚集索引的行大小

create nonclustered index IDX_UniqueCI_ID

on dbo.UniqueCI(ID);

create nonclustered index IDX_NonUniqueCINoDups_ID

on dbo.NonUniqueCINoDups(ID);

create nonclustered index IDX_NonUniqueCIDups_ID

on dbo.NonUniqueCIDups(ID);

select index_level, page_count, min_record_size_in_bytes as [min row size]

 ,max_record_size_in_bytes as [max row size]

 ,avg_record_size_in_bytes as [avg row size]

from

 sys. dm_db_index_physical_stats(db_id(), object_id(N'dbo.UniqueCI'), 2, null

,'DETAILED');

select index_level, page_count, min_record_size_in_bytes as [min row size]

 ,max_record_size_in_bytes as [max row size]

 ,avg_record_size_in_bytes as [avg row size]

from

 sys. dm_db_index_physical_stats(db_id(), object_id(N'dbo.NonUniqueCINoDups'), 2, null

,'DETAILED');

select index_level, page_count, min_record_size_in_bytes as [min row size]

 ,max_record_size_in_bytes as [max row size]

 ,avg_record_size_in_bytes as [avg row size]

from

 sys. dm_db_index_physical_stats(db_id(), object_id(N'dbo.NonUniqueCIDups'), 2, null

,'DETAILED');

 

  

圖7-2  非唯一聚簇索引:非聚簇索引的行大小

dbo.NonUniqueCINoDups表中的非聚集索引沒有開銷。 你應該還記得,SQL Server不會將偏移量資訊儲存在NULL資料的尾隨列的可變長度偏移陣列中。儘管如此,uniquifier在dbo.NonUniqueCIDups表中也引入了8個位元組的開銷。 這八個位元組由一個4位元組的unquifier值,一個雙位元組的可變長度資料偏移陣列條目和一個儲存行中可變長度列數的雙位元組條目組成。

我們可以通過以下方式總結uniquifier的儲存開銷。 對於具有uniquifier但為NULL的行,如果索引中至少有一個儲存NOT NULL值的可變長度列,那就會產生兩個位元組的開銷。 該開銷來自uniquifier列的可變長度偏移陣列條目。否則沒有開銷。

在填充uniquifier的情況下,如果存在儲存非空值的可變長度列,則開銷為6個位元組。否則,開銷是8個位元組。

提示:如果預計聚簇索引值中存在大量重複項,可以將整數標識列作為最右邊的列新增到索引中,從而實現唯一性。 與uniquifiers引入的最多8位元組的不可預測儲存開銷相比,這將為每行增加一個4位元組的可預測儲存開銷。當你通過其該行的聚簇索引列使用該行時,這還可以提高單個查詢操作的效能。

用最小化插入新行引起索引碎片化的方式設計聚簇索引是有好處的。實現這一點的方法之一是不斷增加聚簇索引值。 標識列上的索引就是一個這樣的例子。 另一個示例是使用插入時的當前系統時間填充的datetime列。

然而,索引不斷增加會存在兩個潛在問題。第一個與統計有關。就像你在第3章中瞭解到的,當直方圖中沒有引數值時,SQL Server中的遺留基數估計器會低估基數。你應該將這種行為考慮到系統的統計維護策略中,除非你使用新的SQL Server 2014-2016基數估計值,該估計值假定在直方圖之外的資料具有類似於表中其他資料的分佈。

下一個問題更復雜。 隨著索引的不斷增加,資料總是插入到索引的末尾。 一方面,它可以防止頁面拆分並減少碎片。 另一方面,它可能導致熱點,即當多個會話試圖修改相同的資料頁和/或分配新的頁或區段時發生的序列化延遲。SQL Server不允許多個會話更新相同的資料結構,而是通過序列化執行這些操作。

除非系統以非常高的速率收集資料並且索引每秒處理數百個插入,否則熱點通常不是問題。 我們將在第27章“系統故障排除”中討論如何檢測此類問題。

最後,如果系統有一組頻繁執行且重要的查詢,就要考慮到聚簇索引這時候是有好處的,它會優化它們。這解決了昂貴的金鑰查詢操作花銷並提高了系統的效能。

即使可以使用覆蓋非聚簇索引來優化此類查詢,但它並不總是理想的解決方案。 在某些情況下,它需要你建立非常大的非聚簇索引,這將佔用磁碟和緩衝池中的大量儲存空間。

另一個重要因素是修改列的頻率。將經常修改的列新增到非聚簇索引上需要SQL Server在多個位置更改資料,這會對系統的更新效能產生負面影響,並增加阻塞。

儘管如此,並不總能設計滿足所有這些準則的聚簇索引。 此外,你不應將這些指南視為絕對要求。應該分析系統,業務需求,工作負載和查詢,並選擇適當的聚簇索引,即使它們違反了某些準則。

 

身份,序列和唯一識別符號

人們通常選擇身份,序列和唯一識別符號作為聚簇索引鍵。 與前面的一樣,這種方法有其自身的優缺點。

在此類上定義的聚簇索引是唯一的,靜態的和窄的。此外,身份和序列不斷增加的同時也減少了索引碎片。一個理想的用例是目錄實體表。作為示例,你可能會考慮到儲存客戶,文章或裝置列表的表。 這些表儲存數千甚至數百萬行,但是資料相對靜態,因此熱點不是問題。 此外,這些表通常由外來鍵引用並用於連線。 integer或bigint列上的索引非常緊湊和高效,這將提高查詢的效能。

 

注意:我們將在第8章“約束”中更詳細地討論外來鍵約束。

 

在事務性表中,身份或序列列上的聚集索引效率較低,事務性表以非常高的速率收集大量資料,這是由於它們引入的潛在熱點造成的。

 

另一方面,對於聚集索引和非聚集索引,uniqueidentifier很少是一個好的選擇。使用NEWID()函式生成的隨機值極大地增加了索引碎片。此外,uniqueidentifiers上的索引會降低批處理操作的效能。我們看一個示例並建立兩個表:一個表在標識列上具有聚簇索引,另一個表在uniqueidentifier列上具有聚簇索引。下一步,我們將在兩個表中插入65,536行。你可以在列表7-4中看到執行此操作的程式碼。

 

列表7-4 Uniqueidentifiers:表建立

create table dbo.IdentityCI

(

 ID int not null identity(1,1),

 Val int not null,

 Placeholder char(100) null

);

create unique clustered index IDX_IdentityCI_ID

on dbo.IdentityCI(ID);

create table dbo.UniqueidentifierCI

(

 ID uniqueidentifier not null

 constraint DEF_UniqueidentifierCI_ID

 default newid(),

 Val int not null,

 Placeholder char(100) null,

);

create unique clustered index IDX_UniqueidentifierCI_ID

on dbo.UniqueidentifierCI(ID)

go

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N5)

insert into dbo.IdentityCI(Val)

 select ID from IDs;

;with N1(C) as (select 0 union all select 0) -- 2 rows

,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows

,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows

,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows

,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows

,IDs(ID) as (select row_number() over (order by (select null)) from N5)

insert into dbo.UniqueidentifierCI(Val)

 select ID from IDs;

  我的計算機上的執行時間和讀取次數如表7-1所示。 如圖7-3所示

兩個查詢的執行計劃。

  圖7-3。 將資料插入表中:執行計劃

  表7-1。 將資料插入表:執行統計

 如您所見,uniqueidentifier列上的索引有另一個排序運算子。SQL Server在插入之前對隨機生成的uniqueidentifier值進行排序,從而減少了查詢的效能。讓我們在表中插入另一批行並檢查索引碎片。 這樣做的程式碼

如清單7-5所示。 圖7-4顯示了查詢的結果。

;with N1(C) as (select 0 union all select 0) -- 2 rows
,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows
,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows
,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows
,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows
,IDs(ID) as (select row_number() over (order by (select null)) from N5)
insert into dbo.IdentityCI(Val)
 select ID from IDs;
;with N1(C) as (select 0 union all select 0) -- 2 rows
,N2(C) as (select 0 from N1 as T1 cross join N1 as T2) -- 4 rows
,N3(C) as (select 0 from N2 as T1 cross join N2 as T2) -- 16 rows
,N4(C) as (select 0 from N3 as T1 cross join N3 as T2) -- 256 rows
,N5(C) as (select 0 from N4 as T1 cross join N4 as T2) -- 65,536 rows
,IDs(ID) as (select row_number() over (order by (select null)) from N5) 
insert into dbo.UniqueidentifierCI(Val)
 select ID from IDs;
select page_count, avg_page_space_used_in_percent, avg_fragmentation_in_percent
from sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.IdentityCI'),1,null,'DETAILED');
select page_count, avg_page_space_used_in_percent, avg_fragmentation_in_percent
from sys.dm_db_index_physical_stats(db_id(),object_id(N'dbo.UniqueidentifierCI'),1,null
,'DETAILED'); 

  

圖7-4。 索引碎片

正如你所看到的,uniqueidentifier列上的索引嚴重碎片化,與標識列上的索引相比,它使用的資料頁大約多40%。

在uniqueidentifier列的索引中的批量插入會變成在資料檔案的不同位置插入資料,當表太大這會出現繁重的隨機物理I / O. 這可能會降低操作效能。

前段時間,我參與了一個系統的優化,該系統具有250 GB的表,其中包含一個聚簇索引和三個非聚簇索引。 其中一個非聚簇索引是uniqueidentifier列上的索引。通過刪除此索引,我們能夠將50,000行的批量插入從45秒加速到7秒。

當你想要在uniqueidentifier列上建立索引時,有兩種常見方法。 第一個是支援跨多個數據庫的值保證唯一性。其中行可以插入到分散式系統的每個資料庫中。開發人員經常使用uniqueidentifiers來確保每個鍵值在系統範圍內都是唯一的。

像這種實現的關鍵元素是如何生成鍵值。就像你已經看到的,NEWID()函式或客戶機程式碼中生成的隨機值會對系統性能產生負面影響。但是,你可以使用NEWSEQUENTIALID()函式,該函式會生成遞增的唯一的值(SQL Server不時重置它們的基值)。使用NEWSEQUENTIALID()函式生成的uniqueidentifier列上的索引類似於標識列和序列列上的索引;但是,你要記住,uniqueidentifier資料型別使用16位元組的儲存空間,而4位元組的int或8位元組的bigint資料型別。 

作為替代解決方案,你也可以建立兩列的複合索引(InstallationId,Unique_Id_Within_Installation)。 兩列的組合保證了多個安裝和資料庫的唯一性,並且比獨特識別符號使用更少的儲存空間。 你可以使用整數標識或序列來生成Unique_Id_Within_Installation值,這將減少索引的碎片。 

在需要跨資料庫後所有實體生成唯一鍵值的情況下,可以考慮跨所有實體使用單個sequence物件。這種方法可以滿足需求,但是使用到比唯一識別符號更小的資料型別。

另一個常見方法是安全性,其中uniqueidentifier值用作安全性令牌或隨機物件編號。 不幸的是,你無法在此方案中使用NEWSEQUENTIALID()函式,因為可以猜測該函式返回的下一個值。

在這種情況下,一種可能的改進是不在uniqueidentifier列上建立索引,而是使用CHECKSUM()函式建立計算列,然後對其進行索引。 程式碼如列表7-6所示。

  列表7-6 使用CHECKSUM():表結構

create table dbo.Articles
(
 ArticleId int not null identity(1,1),
 ExternalId uniqueidentifier not null
 constraint DEF_Articles_ExternalId
 default newid(),
 ExternalIdCheckSum as checksum(ExternalId),
 /* Other Columns */
);
create unique clustered index IDX_Articles_ArticleId
on dbo.Articles(ArticleId);
create nonclustered index IDX_Articles_ExternalIdCheckSum
on dbo.Articles(ExternalIdCheckSum); 

  提示:您可以索引計算列而不保留它。

儘管IDX_Articles_ExternalIdCheckSum索引非常分散,但與uniqueidentifier列上的索引(4位元組金鑰與16位元組)相比,它更緊湊。 它還提高了批處理操作的效能,因為更快的排序,所以也需要更小的記憶體來進行。

你必須記住的一件事是CHECKSUM()函式的結果不保證是唯一的。你應該在查詢中包含兩個謂詞,如列表7-7所示

列表7-7 使用CHECKSUM():選擇資料

select ArticleId /* Other Columns */
from dbo.Articles
where checksum(@ExternalId) = ExternalIdCheckSum and ExternalId = @ExternalId   

提示     如果需要索引大於的字串列,則可以使用相同的技術900/1700位元組,這是非聚簇索引鍵的最大大小。 即使這樣的指數不會
支援範圍掃描操作,它可以用於點查詢。