海量資料處理的SQL效能優化
1 設計階段的優化
1.1 表設計
1.1.1 正規化化
資料庫設計三正規化定義:
1. 第一正規化:每個欄位只包含最小的資訊屬性。
例如常見的學號:入學年份+班級+編號,是不符合第一正規化的,需要將其拆解為:入學年份、班級、編號。
2. 第二正規化:(在滿足第一正規化基礎上)模型含有主鍵,非主鍵欄位依賴主鍵。
3. 第三正規化:(在滿足第二正規化基礎上)模型非主鍵欄位不能相互依賴。
例如訂單表,一般來說訂單表的主鍵是訂單號。在此表中,欄位下單時間、客戶ID是符合第二正規化的,而客戶姓名這個欄位就不滿足第二正規化,應當放入客戶表內,組成客戶ID客戶姓名。
正規化化的設計能有效降低資料冗餘,更新方便快速,降低了資料不一致的風險。故常見於聯機交易型的資料庫。
1.1.2 反正規化化
有意不符合正規化化的設計,常見於反第二第三正規化。
符合三正規化的設計在降低冗餘的同時也帶來了問題。如果需要對資料進行加工處理(例如具有訂單表、客戶表,需要統計某個年齡的客戶的訂單總金額)的時候,需要不斷進行關聯操作。當訂單數量極為龐大的時候,這個關聯操作所需要消耗的資源將會相當巨大,導致查詢效能低下。因此在資料倉庫的海量資料的處理中,常使用反正規化化的方式進行設計來提高效能,用空間換取時間。例如在訂單表內新增上下訂單的客戶的生日,則只需直接執行篩選即可。
反正規化化的設計並沒有定勢,需要視具體的業務而定,通常是將查詢較多或者查詢時候關聯消耗過大的欄位新增進來作為冗餘欄位。
1.2 索引的設計
1.2.1 索引的原理
索引是一種建立與表外部用於加快表查詢速度的資料結構。商業資料庫索引的實現常用B-樹/B+樹的變種,它通常有三層:根、中間層、葉子。其中每一層內包含的節點塊都是按照順序排列的(對於數字按照大小,字串則從左往右的字元順序依次進行比較)。每個節點塊內包含著指向實際資料地址的指標。
當我們需要進行查詢時,會先在索引的樹內進行查詢,如果是數字,如29,從根節點起往下,尋找最後一個小於他的區間B1,然後再往下L1,R2。在R2內含有指向實際資料地址的指標,資料庫再到實際地址獲取資料。
如果是字串,則從左往右,例如ABCDE。從根節點起往下,尋找最後一個小於他的區間B1(A),然後再往下L1(B),R2(CDE)。在R2內含有指向實際資料地址的指標,資料庫再到實際地址獲取資料。
如果在到實際指標的時候無法定位到具體的哪行,則會對指標指示區域進行行掃描。
組合索引會將欄位從左到右拼接起來再進行建立索引。
在我們需要更新資料的時候,資料庫在更新表的同時,需要同步更新索引,這樣才能保證索引的可用性。
1.2.2 索引的選擇原則
由於上述原因,我們在設計表選擇索引的時候,首先考慮的是幾點:
1. 索引要建立在常用的欄位上,以增加使用頻率。
2. 索引的建立不是越多越好,過多的索引會佔用大量空間,同時更新緩慢。一般3-5個為宜。
3. 索引的欄位選擇區分度較高的欄位。(例如,訂單表中訂單時間就比訂單日期作為索引效率更高)因為分割槽度較大的索引能夠使得索引查詢完畢後得到的指標更為精確,不需要繼續對錶進行一行行掃描。
4. 由於對於組合索引是將欄位由左到右進行拼接的。因此,請將區分度較高的欄位放置於左側。(例如性別+客戶姓名這個索引效率就比客戶姓名+性別低)因為在部分情況下當查詢到資料超出前面的欄位的限制的時候,那麼資料庫會自動停止匹配下一個欄位,節約索引匹配時間。
1.2.3 索引的維護
同樣是根據上述原因當表變化過大的時候,需要重建索引,不然樹可能會不平衡,深度過大,降低查詢效率。
1.3 分割槽鍵
1.3.1 分割槽鍵原理
部分資料庫具有分割槽特性,每個分割槽可以並行執行增加資料處理速度,可以對錶進行分割槽操作,根據分割槽鍵內的資料通過特有的雜湊函式,將資料置於不同的分割槽內。
雜湊函式進行資料分割槽是由一個內建的雜湊函式決定的,其接受定義為分割槽鍵的列內的值,返回資料庫的分割槽號。
1.3.2 分割槽鍵的選擇原則
在選擇分割槽鍵的時候,必須遵循一定原則,較差的分割槽鍵設計有可能不僅沒有提升資料庫效能,反而會由於資料分佈不均,頻繁跨區操作,大幅降低效能。
1. 列越少越。
2. 效率:整型>字元>小數。
3. 選擇分佈較為均勻的分割槽鍵組合。例如性別、出生月份,像所屬省份、出生年份這類就是不適合的。
4. 選擇較為常用的欄位組合。這個一般資料庫會限制分割槽鍵必須是主鍵,比較容易做到。
2 執行階段的優化
前面講的是設計的時候的要求,在平時對海量資料進行操作的時候,SQL語句的優化也極為重要。
2.1 語句
很多人喜歡使用一條語句將所有查詢和關聯動作執行完畢,但是這是不好的習慣,過於複雜的語句會導致優化器在進行優化的時候根據資料庫情況自行選擇執行計劃,有時候會出現較為低效的執行計劃;同時代碼的可讀性也會較差。因此請將複雜語句拆解開來,每一步之間使用臨時表進行儲存。
2.2 匹配條件
2.2.1 匹配時候不能對欄位使用函式
例如:WHERELENGTH(NAME)=5在作為匹配條件的時候會導致索引失效,觸發全表掃描。
2.2.2 避免使用子查詢匹配
很多人在編寫SQL語句的時候經常使用子查詢,但是很多時候在資料庫SQL優化器解析的時候,會將子查詢查詢到的每一行結果提取出來轉換成次查詢,類似於IN語句。在海量資料處理中,當子查詢的結果資料量較大的時候,會導致整個執行語句轉換為成千上萬次查詢,效率極低。
因此在遇到使用子查詢的時候,請將它轉換為JOIN語句。
2.3 關聯
由於JOIN可以使用表中的索引分割槽鍵,效率較高,因此JOIN在大資料量處理的時候較常用到。如果執行效率過低,需要檢查資料庫的執行計劃,檢視資料庫優化器是否選擇了不恰當的關聯方式,根據執行計劃修改SQL語句。我們常見的關聯方式有幾種:
2.3.1 Nested-Loop Join(NL Join)巢狀迴圈連線
對於被連線的資料子集較小的情況,巢狀迴圈連線是個較好的選擇。在巢狀迴圈中,內表被外表驅動,外表返回的每一行都要在內表中檢索找到與它匹配的行,因此整個查詢返回的結果集不能太大,要把返回子集較小表的作為外表,而且在內表的連線欄位上一定要有索引。
此時,對於被選擇為外表的表來說,外表掃描1遍,內表會被掃描N遍。
2.3.2 Merge-Scan Join(MS Join)排序合併連線
通常情況下雜湊連線的效果都比排序合併連線要好,然而如果行源已經被排過序,在執行排序合併連線時不需要再排序了,這時排序合併連線的效能會優於雜湊連線。
2.3.3 Hash Join 雜湊連線
雜湊連線是做大資料集連線時的常用方式,優化器使用兩個表中較小的表(或資料來源)利用連線鍵在記憶體中建立散列表,然後掃描較大的表並探測散列表,找出與散列表匹配的行。
這種方式適用於較小的表完全可以放於記憶體中的情況,這樣總成本就是訪問兩個表的成本之和。但是在表很大的情況下並不能完全放入記憶體,這時優化器會將它分割成若干不同的分割槽,不能放入記憶體的部分就把該分割槽寫入磁碟的臨時段,此時要有較大的臨時段從而儘量提高I/O 的效能。
但是Hash連線也有特有的問題,首先連線欄位長度一般要求相同,然後不能使用於多個謂詞,而且對於排序堆、臨時表空間消耗較大。
一般來說會先掃描內表,生成Hash資料,再掃描外表。
2.4 執行順序
由於越在前面的語句執行成本越低,因此我們儘可能遵循幾個策略排列我們的語句
1. 降低資料量越大的操作放在越前面。
2. 越是複雜查詢成本越大的語句放在越後面。