在數據庫部分,對數據庫應用性能改進來說,需要重點關注應用程序,在查詢設計和索引策略等方面進行優化,甚至可以把數據庫查詢效率提高數百倍,在其他方面的優化努力,其效果就沒有這么明顯(見下圖)。本文重點描述在應用程序中進行數據庫查詢時,在設計和使用索引、設計查詢語句等方面的注意事項,以取得良好的數據庫查詢性能。
一、索引設計和使用策略
使用索引是數據庫減少磁盤I/O最有效的方法。除了在表中數據量非常少和需要返回表中大部分行的情況,正確使用索引的查詢性能比全表掃描要高得多,有時簡單新建一個合適的索引,就能把查詢性能提高幾十倍,在查詢語句出現性能問題時,通常第一時間都會檢查是否正確設計和使用了索引。
但是,事物都是有兩面性的,索引的建立和維護需要消耗額外的CPU時間、額外的內存和存儲空間,在對數據庫表進行insert、update、delete操作時,都需要同時維護該表的所有索引,如果索引設計不當,不但不會提高性能,反而會降低性能。
遵循以下原則,可以讓我們充分利用索引帶來的性能方面的提升。
序號 |
索引設計和使用原則 |
解釋說明 |
1 |
提高索引的區別能力,在數據基數大、重復值少的列上建索引,只要有可能,就使用create unique Index語句定義主鍵和唯一鍵 |
區分度越高,查找越精確,效率越高,盡量避免在有很多重復值(包括空值)的列上建索引 |
2 |
盡量選擇唯一、最小、不可Null、容易獲得、不常變更的列作為主鍵 |
|
3 |
在where子句中最頻繁用到的列上建索引 |
|
4 |
在有distinct、min、max、order by、group by操作的列和join連接列上建索引 |
可降低查找成本和排序成本 |
5 |
避免創建冗余的類似索引,如存在a,b,c三列上的索引,那么a,b兩列上的索引就是冗余的 |
冗余索引沒有作用,只會增加額外的開銷 |
6 |
索引中列的排列順序需要考慮列唯一性,列寬度,列數據類型和查詢使用頻度。具有唯一性、列寬度小的、整型的、查詢中引用最多的列排在前面 |
唯一性提高區分度,列寬小減少空間占用,列數據類型影響排序效率 |
7 |
盡量避免在一個索引中使用多於5個的列 |
|
8 |
對一個表,不要創建太多索引。通常,對聯機事務處理環境(OLTP),不超過3個;對聯機分析處理(OLAP)只讀查詢環境,可超過5個;混合查詢和OLTP環境,2~5個為宜 |
|
9 |
盡量使用完全索引操作,即查詢的所有列都在索引列中,如果需要的字段與排序無關,但查詢時經常用到,可在create index時使用include子句將該字段包含在索引頁中,但不作為索引字段使用 |
完全索引操作只訪問索引,不需要訪問數據表,可大幅度降低查詢時間 |
10 |
在外鍵上建索引可提高父表update和delete操作的效率 |
|
11 |
在需要批量處理一定范圍的數據或讀取事先排序好的數據時,可使用聚簇索引(Cluster Index) |
批量處理操作時比較適用 |
12 |
不要在頻繁更新的列、寬度較大的列上建立聚簇索引 |
所有非聚簇索引將以聚簇鍵作為定位器,這兩種情況將導致非聚簇索引更新效率下降 |
13 |
不要在有大量並行順序插入操作的情況下使用聚簇索引,或使用其他列上的索引打亂插入順序,將插入操作分布到整個表 |
如果按聚簇鍵批量插入,插入操作集中在表的最后一個頁面,將形成“熱點”,I/O效率非常低 |
14 |
首先創建聚簇索引,再創建非聚簇索引,避免因建立聚簇索引導致所有非聚簇索引重建的情況 |
|
15 |
重建聚簇索引時使用Drop_Existing選項,避免重建聚簇索引導致所有非聚簇索引重建的情況 |
適用於SQL Server、MySQL |
16 |
在對超大字段進行查找,或對大量數據使用like ‘%...%’進行模糊查詢時,使用全文索引 |
|
二、SQL語句編寫的注意事項
數據庫系統中,索引、碎片、統計信息和鎖等很多因素都會影響性能,對這些特性進行優化都可能提高查詢語句的執行效率。但是,如果SQL語句編寫不合理,就可能大幅度增加本可避免的開銷。
以下是編寫SQL語句需要注意的一些事項:
- 建立並遵循SQL編碼規范,SQL語句應簡潔、易讀、便於維護
- 為提高查詢性能,應限制操作的數據量,盡量在小結果集上操作,減少行數和列數
² 不要使用“select * from ……”返回所有列,只檢索需要的列,可避免后續因表結構變化導致的不必要的程序修改,還可降低額外消耗的資源
² 不要檢索已知的列,如select cust_no, cust_name from CustInfo where cust_no = ‘10000050’
² 使用高選擇性的where子句,讓數據庫返回必須的數據集,而不是返回大量數據給應用程序,再由應用程序進行篩選,返回大數據集的代價很高
- 有效地使用索引:where子句引用的列決定了索引的使用,需要重點關注
² Where子句中列的順序與需使用的索引順序保持一致,不是所有數據庫的優化器都能對此順序進行優化,保持良好編程習慣
² 使用可參數化的搜索條件,如=, >, >=, <, <=, between, in, is null以及like ‘<literal>%’
² 盡量不要使用非參數化的負向查詢,這將導致無法使用索引,如<>, !=, !>, !<, not in, not like, not exists, not between, is not null, like ‘%<literal>’
² 不要在where子句中對字段進行運算,如where amount / 2 > 100,即使amount字段有索引,也無法使用,改成where amount > 100 * 2就可使用amount列上的索引
² 不要在where子句中對字段使用函數,如where substring( Lastname, 1, 1) = ‘F’就無法使用Lastname列上的索引,而where Lastname like ‘F%’或者where Lastname >= ‘F’ and Lastname < ‘G’就可以
² 小心使用or操作,and操作中任何一個子句可使用索引都會提高查詢性能,但是or條件中任何一個不能使用索引,都將導致查詢性能下降,如where member_no = 1 or provider_no = 1,在member_no或provider_no任何一個字段上沒有索引,都將導致表掃描或聚簇索引掃描
² Between一般比in/or高效得多,如果能在between和in/or條件中選擇,那么始終選擇between條件,並用>=和<=條件組合替代between子句,因為不是所有數據庫的優化器都能把between子句改寫為>=和<=條件組合,如果不能改寫將導致無法使用索引
- 分頁顯示的處理:在有大量滿足條件的結果集,但界面上需要分頁展示的情況,最好的方法是限制數據庫每次返回的記錄數,如每頁展示50條記錄,可使用select …… fetch first 50 rows only
- 避免不必要的資源開銷:
² 盡量用相同數據類型進行比較,避免發生數據類型轉換,如select ‘5678’+3
² 在有min、max、distinct、order by、group by操作的列上建索引,避免額外的排序開銷
² 當需要驗證是否有符合條件的記錄時,使用exists,不要使用count(*),前者在第一個匹配記錄處返回,后者需要遍歷所有匹配記錄
² 如果參與union子句的select結果集互斥或者允許結果集中有重復記錄,可使用union all代替union,可避免檢測和刪除重復記錄的開銷
² 盡量使用單條語句完成操作,如多條單筆insert語句可以合並為一條多筆insert語句,使用case子句等
² 當不需要更新數據時,使用select……for read only或fetch read only,可以提高查詢性能
- 降低事務開銷
² 使用明確的事務控制語句來控制事務的開始和commit/rollback,將原子性擴展到整個事務,否則,每個語句都是一個原子操作,日志開銷大
² 及時commit,如果有insert、update和delete操作沒有commit,數據上的鎖會一直保持,及時commit有利於盡快釋放鎖,可提高並發能力,尤其是在批量操作時,分段commit還有利於出錯時的重新處理,可從最后一次commit操作繼續處理,如果一直沒有commit,就必須從頭開始了
² 盡量保持短事務,盡量不要在事務中處理與數據庫操作無關的工作,如群發郵件等
² 使用insert into……select …… from ……等塊操作方式,可大幅度降低日志開銷
² 根據業務邏輯使用最低級別的隔離級別,降低鎖開銷
- 使用select……for update保護后續update語句中可能被修改的那些行,可有效避免最常見的死鎖情況
- 使用數據庫的參照完整性約束,如Not Null約束和主/外鍵約束,它們不僅能保證數據完整性,還能幫助優化器生成更高效的執行計划
- 調整join操作順序以使性能最優,join操作是自頂向下的,如select a.col1, b.col2, c.col3 from a left join b on a.col4 = b.col5
Join c on a.col4 = c.col6
操作順序是a和b進行left join,其結果再與c進行join,如果a與c進行join的結果集比較小,則應調整上述語句順序,先進行a與c的join操作,再與b進行left join,可提高查詢性能
三、其他注意事項
除了有效使用索引,編寫高效SQL語句外,在提高查詢性能方面,從應用程序編寫角度還有以下內容需要注意:
- SQL語句是一種處理結果集(多行、多列)的高效語句,而游標(cursor)是每次處理一行的技術,效率要低得多,如非必要,不要使用游標
- 如果使用游標,使用代價最低的游標,並盡快釋放
- 多使用動態SQL:靜態SQL語句的執行計划是在應用程序編譯時生成的,即使后續數據庫數據發生了重大變化,原有執行計划變得非常低效,只要應用程序沒有重新編譯,數據庫就不會調整、優化其執行計划。而動態SQL語句雖然需要在每次執行時編譯生成執行計划,但每次都是根據最新統計數據生成的優化執行計划,可得到較好的查詢性能
- 使用存儲過程:對於不需要在用戶界面上展示,但需要與數據庫進行多次、大量數據交互的情況,最好的選擇就是使用存儲過程,可以減少網絡通訊次數,降低網絡流量,而存儲過程已經過預編譯,其執行速度也很快
- 在高並發環境下,使用數據庫連接池,可大幅度降低建立連接和關閉連接的開銷
實際上,要想得到一個高性能的數據庫應用,不是一蹴而就、一勞永逸的,除了在數據庫設計之初就要充分考慮業務特性和數據特性,做好物理設計(硬件選擇,尤其是存儲系統的選擇和設計)和邏輯設計,編制高效的應用程序外,還需要與數據庫管理員充分配合,在運行過程中,持續跟蹤數據庫狀況,不定期對索引和數據碎片整理(ReOrg),對統計信息進行更新,對應用進行持續優化,才能讓數據庫應用系統一直保持良好的性能。