數據庫索引及基本優化入門
2013-7-26
一 前言
經常在面試中發現很多人工作了好多年了,項目經驗也不少,用過各種數據庫,但大都不知道這些SQL語句背后的基本原理,更別說數據庫優化了。平時做項目只知道實現功能,懶得學習,懶得思考,懶得看書(其實本人也是,不要找借口說這是China國情,項目是給boss做的,但技術和成長是你自己的)。
本篇文章主要講述數據庫索引的基本原理,及基本的數據庫優化的知識。所有知識均為本人自己學習的總結以及網絡。此篇文章主要是為公司內部人員培訓所用的,整理出來只是希望和大家分享、交流,因本人技術有限,若有遺漏、錯誤,希望多多指正、交流。
二.基礎知識
2.1 頁
數據庫文件存儲是已頁為存儲單元的,一個頁是8K(8192Byte),一個頁就可以存放N行數據。我們常用的頁類型就是數據頁和索引頁。一個頁中除了存放基本數據之外還需要存放一些其他的數據,如頁的信息、偏移量等,如下圖所示。
雖然SQLServer是以頁為單位存儲數據,但是其分配空間是以一個盤區為單位的(8個頁=64K),這樣做的目的主要是為提高I/O的性能。
2.2 B樹
B樹即二叉搜索樹,所有非葉子節點最低擁有兩個子節點,基本信息如下圖所示。都是小的元素放左邊,大的元素放右邊。比如說要查找某個元素,其時間復雜度就對應該元素的深度,如要查詢9,從根節點開始,只要比較三次就找到他了,其查詢效率是非常高的。
子節點:最多兩個子節點(指針分別指向Left和Right)
階數(節點子節點個數):2
深度:就是層數,各個葉子節點不一定一樣,如節點21的深度為4,40的深度為3
2.2 B-樹
B-樹是一中多路搜索樹,其階數可以自定義(>2),是很多數據及文件系統應用的一種索引結構,基本特征如:
1) 階數(M)>2,即孩子數量大於2個
2) 每個結點存放至少M/2-1(取上整)和至多M-1個關鍵字;(至少2個關鍵字)
3) 非葉子結點上的多個關鍵字是按照順序排列的:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
4) 所有葉子節點都位於同一層,因此葉子節點的深度都是一樣的
5) 非葉子結點的關鍵字個數=指向兒子的指針個數-1;
6) 非葉子結點的指針:P[1], P[2], …, P[M];其中P[1]指向關鍵字小於K[1]的子樹,P[M]指向關鍵字大於K[M-1]的子樹,其它P[i]指向關鍵字屬於(K[i-1], K[i])的子樹;
如下圖是一個三階的B-樹,節點[18]有兩個指針分別指向其2個子節點。
這時如果要插入一個值17,其處理步驟:
1) 從根節點進入,17小於22,進入左邊的節點[18];
2) [18]不是葉子節點,繼續向下搜索,17小於18,進入其左邊的子節點[12,16];
3) [12,16]為葉子節點,插入到該節點;
4) 節點[12,16,17]元素大於2了(3階樹的節點關鍵字數量應>3/2-1,<3-1),因此該節點需要分裂,分裂中間的元素16到父節點18中去;
5) 12,17分裂成了兩個子節點了;
分裂后的效果如下圖
以上圖片效果來自一個外國大學里面的的在線版B-樹的測試,網站:http://www.cs.usfca.edu/~galles/visualization/BTree.html ,大家可以去這個網站測試,效果很直觀,外國人就是牛。本人以前用C#+GDI實現過類似的效果,結果還是可以的,就是當樹太大的時候,布局不好處理了。
2.3 B+樹
B+樹是B-樹的變體,也是一種多路搜索樹,一棵m 階的B+樹和m 階的B-樹的差異在於:
l 非葉子節點的子節點和其關鍵字相同,即節點有三個元素(關鍵字),他就肯定有三個子節點;
l 非葉子節點的子節點P[i],指向關鍵字值屬於[K[i], K[i+1])的子樹(B-樹是開區間);
l 所有葉子節點增加一個鏈指針;
l 所有關鍵字的數據都在葉子節點中;
如下圖所示,圖片來自網絡(http://www.cnblogs.com/chjw8016/archive/2011/03/08/1976891.html)。
三 索引存儲
B+樹和B-樹是數據庫廣發應用的索引存儲結構,它可以極大的提高數據查找的效率。關於B-樹、B+樹的原理與應用的詳細可以參考文檔:http://blog.csdn.net/hguisu/article/details/7786014
前面2.1中我們講了SQLServer中使用頁為存儲單元的,那么在建立索引時,其索引節點就是頁了,然后樹的鍵值就是存放到這些頁(節點)中的。就是說表中的數據行就是存放到頁上的,一個表有多個頁構成,這些頁以樹的結構存放。
3.1 聚集索引
如下圖為聚集索引的存儲結構(圖片來自網絡)。其中可以看出頁有兩種:Index Rows(索引頁)、data rows(數據頁),所有非葉子節點都存放着索引項,數據行是存放在葉子節點中的,只有葉子節點才真實存放着表中的每一行數據,而其他非葉子節點的頁都存放着聚集索引的鍵值。因此查詢數據的時間復雜度都是一樣的,就是該樹的深度。
在4.2中有說明,聚集索引決定了表數據的存儲順序,具體可以參考4.2。若表沒有創建聚集索引,則表數據時一個無序的堆結構。
3.2 非聚集索引
與非聚集索引的存儲結構唯一不一樣的,就是非聚集索引中不存儲真正的數據行,因為在聚集索引中已經存放了數據,非聚集索引只包含一個指向數據行的指針即可。如下圖所示(圖片來源:http://www.cnblogs.com/ashou706/archive/2010/06/08/1754164.html):
四 數據庫優化
數據庫優化的一個重要參數指標就是“邏輯讀”(Logical Reads),可以使用命令SET STATISTICS IO ON來打開消息提示,如下圖所示。
邏輯讀(Logical Reads):我們在查詢數據時,數據時從緩沖區(內存)中讀取的,而不是直接從磁盤讀取數據的。數據庫會預先把數據讀取到數據緩沖區中,存放到8K字節的頁中。邏輯讀就是從緩沖區中讀取頁的頁數,這個才是真正反映查詢效率的指標,一般情況下,一個查詢的邏輯讀越小,其效率越高、速度就越快。同時,同樣的SQL查詢同樣數據集,每次的邏輯讀是一樣的。
物理讀取(Physical Reads):真正的從磁盤讀取數據到期緩沖區,在SQLServer執行查詢前,會先檢查其需要的數據是否在緩沖區中,若不在,就會從磁盤讀取數據到緩沖區。這一塊是數據庫本身的職責,我們在做查詢優化的時候不用太關注的,只要給數據庫服務器提供足夠的內存就OK了。
預讀(Read-Ahead Reads):數據庫為優化查詢,預先讀取一部分數據,這個值在優化中可以不用關注。由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分分之一,因此為了提高效率,要盡量減少磁盤I/O。為了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向后讀取一定長度的數據放入內存。由於磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有局部性的程序來說,預讀可以提高I/O效率。
數據優化的一個主要手段就是查看SQL的執行計划,通過查看具體SQL執行過程,可以看出索引的使用是否正確,了解查詢中性能問題在哪,從而解決問題。
4.1 T-SQL優化基本常識
1.在Where條件中盡量不要在=號左邊進行函數、運算符、或表達式計算,如Where DATEDIFF(DD,StartTime,GetDate())=6 ;或Where Num/2=100;
2.在Where中盡量避免出現!=或<>操作符;
3.在Where中盡量避免對字段進行null值判定;
4.使用Like關鍵字進行模糊查找時,不要使用前置百分號,如Like ‘%123%’;
5.數據庫字段的長度盡量的小(保證應用的前提下);
6.不要使用Selecte*,不要使用*號來查詢數據;
7.盡量避免使用游標,游標的效率是很差的,可以使用While循環來代替;
8.盡量避免返回大量數據(查詢數據(Select)優化,分頁處理等);
9.使用Exists代替in和not in;
4.2 聚集索引
聚集索引決定了表數據的物理存儲順序,也就是說表的物理存儲時根據聚集索引結構進行順序存儲的,因此一個表只能有一個聚集索引,SQLServer的聚集索引屬性如下圖。
該索引的的創建腳本:
/****** Object: Index [Index_KeyId] Script Date: 08/12/2013 15:25:59 ******/
CREATE UNIQUE CLUSTERED INDEX [Index_KeyId] ON [dbo].[User] ( [KeyId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
因此我們可以得出一個個結論:根據聚集索引的查找效率是比較高的。若表沒有建立聚集索引,則表的數據存儲是亂的,數據就是一個堆,沒有任何順序可言,對表的查詢經常會掃描全表,造成性能較低。一般我們在實際使用中,大多會對主鍵建立聚集索引,一般這么做就足夠了,但實際應用應該尊從一個原則就是“頻繁使用的、排序的字段上”,如果要深究的話,可以參見文章結尾的參考目錄其他同學的文章。
對應與聚集索引,所有其他的索引可以統稱為非聚集索引,非聚集索引的創建會單獨創建索引文件來存儲索引結構,因此在創建其他索引的時候也要注意硬盤空間。比如一個表的容量是2000w行,大概有800Mb,創建的一個非聚集索引可能數據立馬增加好幾個G。具體如下4.3、4.4所述。
4.3 覆蓋索引
覆蓋索引就是在原本索引的基礎上,把Select中需要的字段放到索引包含列中,這樣就不需要再到數據表中讀取數據了,這個就叫做覆蓋索引了。 比如,我們查詢User表中的字段UserName、Age,其中UserName上創建了非聚集索引,查詢語句及索引腳本如下:
Select UserName,Age from [User] where UserName ='Ryan'
--UserName的索引
CREATE UNIQUE NONCLUSTERED INDEX [Index_UserName] ON [dbo].[User] ( [UserName] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
通過UserName條件查詢到數據后,還需要Age字段的值,該非聚集索引沒有她的數據,還要到數據頁中取其Age數據(就是圖中的鍵查找),這樣會造成額外的開銷,查詢計划如下圖。
若把Age放到該索引的包含列中,該索引就會包含Age的值,查詢的時候就可以直接返回UserName、Age的值了,UserName、Age的覆蓋索引腳本及SQLServer的管理視圖如下:
CREATE UNIQUE NONCLUSTERED INDEX [Index_UserName] ON [dbo].[User] ( [UserName] ASC ) INCLUDE ( [Age]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
創建了覆蓋索引后的查詢效率明顯高了,執行計划如下圖,其中就沒有了循環鍵查找了:
關於覆蓋索引更詳細的文章,可以參考文章:SQL Server 查詢性能優化——覆蓋索引
4.4 復合索引
在上面的索引例子中都只是在一個鍵上建立索引,但實際情況中往往一個查詢會有多個查詢條件,如下的sql語句中,多了條件Password:
Select UserName,Age from [User] where UserName ='Ryan' and Password='123456'
該索引中是沒有關於Password字段的任何信息,因此查詢也會引發鍵查找,查詢計划如下圖
對於這種情況,復合索引的用途就來了,簡單來所,復合索引就是在多個列上建立索引。Sql腳本及SqlServer的索引屬性視圖如下:
CREATE UNIQUE NONCLUSTERED INDEX [Index_UserName] ON [dbo].[User] ( [UserName] ASC, [Password] ASC ) INCLUDE ( [Age]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
創建復合索引后再次執行剛的查詢,觀看查詢計划,查詢有明顯的改善:
在使用復合索引時,應注意多個索引鍵的順序問題,這個是會影響查詢效率的,一般的原則是唯一性高的放前面,還有就是SQl語句中Where條件的順序應該和索引順序一致。
4.5 頁填充因子
通過前面的了解我們知道數據時存放到樹上的頁中,當插入數據時,如果該頁已經存儲滿了,就要進行頁的拆分,頻繁的拆分,會產生較多的索引碎片,影響修改和查詢數據的效率。
填充因子就是用來描述這種頁中填充數據的一個比例,一般默認是100%填充的。如果我們修改填充因子為80%,那么頁在存儲數據時,就會剩余20%的剩余空間,這樣在下次插入的時候就不會拆分頁了。 那么是不是我們可以把填充因子設置第一點,留更多的剩余空間,不是很好嘛?當然也不好,填充因子設置的低,會需要分配更多的存儲空間,葉子節點的深度會增加,這樣是會影響查詢效率的,因此,這是要根據實際情況而定的。那么一般我們是怎么設置填充因子的呢,主要根據表的讀寫比例而定的。如果讀的多,填充因子可以設置高一點,如100%,讀寫各一半,可以80~90%;更改多可以設置50~70%。SQlServer的的索引屬性中有一個設置填充因子的項,如下圖。
更詳細的信息可以參考:http://www.cnblogs.com/cxd4321/archive/2010/08/16/1800677.html
4.6 索引碎片
我們在前面了解到索引的結構就是B樹,當樹在增加、刪除的時候,會觸發頁的拆分或合並,這種頻繁的操作會產生索引碎片,造成索引不連續,當索引碎片曾多時,是會影響查詢效率的。因此,訪問使用的是隨機的i/o,而不是有順序的i/o,這樣訪問索引頁會變得更慢。因此要定期的清理索引碎片,一般的方法就是重建索引。關於索引碎片的整理,可參考:http://www.cnblogs.com/mywebname/archive/2007/11/13/958463.html
4.7 索引優化注意事項
Ø 建立索引的字段盡量的小,最好是數值;
Ø 盡量在唯一性高的字段上創建索引,不要在性別這種唯一性很低的字段上創建索引;
附錄A:參考文檔
1.一個b-樹的在線演示例子,很直觀很不錯的額:
http://www.cs.usfca.edu/~galles/visualization/BTree.html
2. SQLSERVER聚集索引與非聚集索引的再次研究
http://www.cnblogs.com/lyhabc/p/3196479.html
3. SQL Server 查詢性能優化——覆蓋索引:
http://www.cnblogs.com/chillsrc/archive/2012/09/04/2671092.html
出處:http://www.cnblogs.com/anding/p/3254674.html
Ps:這文章格式太難弄了,圖片上傳也很繁瑣(本來是用word寫好了的)