SQL Server 的 ORDER BY 兩三事

  • 3609
  • 0

SQL 的 ORDER BY 會執行資料的重新排序,然後輸出給上層查詢或輸出給用戶端,開發實務上 ORDER BY 的情況非常常見,就跟呼吸一樣自然,但是 ORDER BY 對一個設計不良的資料庫可以說是一個大災難,因為它排序的對象是結果集,若結果集愈大,ORDER BY 會拖慢效能,但適當的設計可以減少 ORDER BY 對效能的損耗。

本篇的緣由是這個:https://www.facebook.com/groups/DotNetUserGroupTaiwan/permalink/2502224093404027,有興趣的朋友可自行去觀看。

顧名思義,資料庫是一個用來儲存與管理資料的一個結構體,儲存 (storage) 表示裡面的資料是會依照特定的結構去寫入到磁碟機,而管理 (management) 則是提供多種方法去存取儲存在裡面的資料,以及維護整個儲存的結構,讓它是可靠且可信任的儲存方式。基本上每個資料庫引擎 (SQL Server, Oracle, MySQL, ...) 都會定義自己的資料儲存結構,並基於國際標準 (至少是 SQL-92) 提供資料儲存的管理方式,方法很多,其實最常用 (但是也有很多人不知道) 的大概就是索引 (Index) 了。

索引與 ORDER BY

索引對資料庫來說是一種讓資料存取的速度加快的一種管理方式,它就像書本後面的索引頁一樣,給它適當的設定,它會幫你建立一個對照表,這個表會記錄某些資料儲存在哪個資料分頁 (Data Page) 的位置,提供給查詢最佳化器 (Query Optimizer) 決定其查詢執行計畫 (Query Execution Plan) 之用,若沒有這個表,查詢最佳化器將無法確認對儲存區的存取方式,只能用最慢的方法,就是整個表格掃過一遍的方式來存取,這個就叫做表格掃瞄 (Table Scan),是 SQL 執行最慢的一種指令

而之前寫過 GUID 是否能當做 PK 用一文,裡面有提到叢集索引 (Clustered Index),它是將主鍵上 (主鍵預設是 Clustered Index) 的資料整理成對應表給查詢最佳化器用,它預設會使用升冪排序 (ORDER BY ASC),因此查詢最佳化器在處理它的時候都是以升冪看待,當它遇到 ORDER BY DESC 的時候,就會參照 Clustered Index 進行反向處理,但這個是建立於 ORDER BY 的指令是以 Clustered Index 的欄位為主,若今天查詢 ORDER BY 的指令所參照的欄位是沒有索引的時候,查詢最佳化器就只能即時進行排序。排序的對象會是結果集 (Resultset),也就是在前面所執行的 FROM ... WHERE ... 所取出的結果作排序,若查詢所涵蓋的範圍是數十筆可能還沒什麼關係,但若是百萬筆以上,那麼問題就大了,等於查詢要掃過百萬筆以上的資料,光是 I/O 成本就會高的嚇人了,這還沒有考慮到結果集有多大。

如果指令中沒有 ORDER BY,但存有會使查詢最佳化器強制對資料集進行排序的指令的話,排序一樣會執行,以確定查詢指令所使用的資料順序,同樣的,若查詢最佳化器使用的欄位 (一般預設是 Clustered Index,沒有就是 Table Scan) 沒有索引的話,效能數字應該會挺悲劇的....

像 TOP、GROUP BY、OFFSET ... FETCH 等指令都有可能產生強制性的排序作業。

拿空間換時間

索引在本質上是一個對照表,因此也會佔用資料庫內的可用空間,而且在資料 INSERT / DELETE 時,若指令影響到的欄位若有被索引使用到時,就會觸發索引的變更,一個資料表內若有很多索引的話,對寫入的效能也會有所影響,而且資料庫也會比平常成長更快,但是這對一些常用排序的查詢來說,是一種空間換時間的策略,空間還可以利用一些方法處理 (加硬碟、壓縮資料庫等) 釋放出來,但時間的話基本上就是老闆或 PM 要求你速度要快快快,沒什麼能討價還價的空間,所以拿空間換時間也是不得不的作法,然而也不是隨便亂設,而是要精準的打中查詢所需要的欄位,這點 SQL Server 的 Database Tuning Wizard 或是 SSMS 的查詢執行計畫處理器能夠幫上一些忙 (給予相關的建議)。

避免 ORDER BY 影響效能

前面有提過,若是要避免 ORDER BY 變成效能瓶頸的話,最好的方式是先針對資料集做篩選,也就是傳回的資料要愈少愈好,這樣排序起來才會快,也就是要盡可能的避免全部資料集的擷取,當然有時候這很難達到,然而若所有查詢都不篩選 (例如使用 EF 時直接 OrderBy() / OrderByDescending() 而沒有半個 Where(),這就是針對全部資料集做排序),只要資料集一大就會很可怕。

另一個情況是先拿回到用戶端再於用戶端排序,講真的,會做這種建議的不是不懂 DB / SQL 就是太相信網路傳輸速度,或許在應用程式與資料庫都在同一台的情況下,相對可行 (內部匯流排的速度和網路速度是天與地的差別),但現今多數的應用系統都是應用程式與資料庫分開在不同主機,網路頻寬就是很大的影響因素,即便是用 10Gbps,在多人的環境下一樣會變成效能瓶頸,系統上線初期可能還沒有影響,但資料量一大 (幾萬筆、幾十萬筆、幾百萬筆、...) 問題就會明顯的浮現出來,到時要改基本上也很難改得動了。

查詢速度 = SQL 指令回傳結果集 (包含 ORDER BY) + 網路傳輸 + 用戶端處理的時間

相信我,一個百萬筆以上資料的資料庫,在網路傳輸的大小就動輒以 MB 起跳了,何況是多人承載的系統,只要幾個人下這種查詢,資料庫大概就準備被打爆吧。