1. 程式人生 > >空間換時間,超大資料表的查詢效率優化。

空間換時間,超大資料表的查詢效率優化。

原文出處:http://www.cnblogs.com/wesley/archive/2012/04/23/2466982.html

在開發論壇程式的時候,我借鑑了目前一些論壇的資料規模,10年的積累大概在2000萬~5000萬左右,因此決定,最低承載力設計要求至少是 9 位數。於是在開發完第一階段的功能以後,開始折磨硬碟進行效能測試。
在論壇程式中,體積最大、同時也是效率要求最高的兩張表,便是話題 Topics 和帖子 Posts,因此本文將針對這2個例項,介紹分享我的實際經驗。

本文沒有給出任何程式碼示範, 內容可能比較理論化抽象化, 但希望能夠通俗易懂。

 

首先從查詢需求入手,分析我們的目標。

對話題表 Topics,最頻繁的查詢需求有以下幾種篩選、排序,或者它們的搭配:

  1. 通過 Id 查詢單個話題
  2. 按論壇版面 ForumId 篩選
  3. 按照發貼者篩選
  4. 按照特定過濾條件篩選,如置頂、精華、邏輯刪除等
  5. 按照最後回覆倒序
  6. 按照話題建立時間倒序


對於帖子表 Posts,需求可能略微簡單一些:

  1. 通過 Id 查詢單個帖子
  2. 按照話題 TopicId 篩選
  3. 按照發帖人篩選
  4. 按照特定過濾條件篩選,如垃圾資訊、邏輯刪除等
  5. 按照發貼時間排序


主鍵

確定資料行的唯一性,我們毋庸置疑的在這兩張表各自的 Id 列上建立了主鍵。

索引

優化查詢效率,首先想到的是索引,索引又分為聚簇索引和非聚簇索引,這個大家都懂的。(關於這些的基本知識介紹已經非常多見,這裡只做簡單介紹,瞭解相關基本知識有勞另找文章閱讀。)
聚簇索引即表資料行的物理排序,可以獲得最佳的篩選和排序效率,SQL Server 設計器在設定主鍵時會預設將主鍵設定為聚簇索引,實際上對不少情況而言,這是浪費了寶貴的資源。非聚簇索引可以建多個,可以有效的提高查詢對應列的查詢效率。索引是好東西,但是不能濫用,因為索引的儲存是磁碟開銷,索引的更新也是維護開銷。沒錯,網上大量文章都這麼說的,當然也是正確的。

而在撰寫本文之前,我沒有看到一篇文章提及聚簇索引和非聚簇索引在查詢時配合關係,我可不認為這不值得一提,也恰恰是本文理論的重點。

  1. 任何情況都肯定會用到聚簇索引,即使是整表從頭到尾掃一邊,那也是按照這個順序掃一遍
  2. 但不是任何情況都會用到你建立的非聚簇索引,因為分析器不會傻到先掃完一遍非聚簇索引,再去掃一遍聚簇索引,這麼解釋是不是很白?
  3. 基於第1條和第2條,我們很容易得出一個理解,非聚簇索引建立在物理排序(即可認為是聚簇索引,有的話)的基礎上,通過檢視 SQL Server 查詢分析器的實際執行計劃,也證明了這一點,目的有2個:
    • 減少對聚簇索引的掃描範圍,或者說對聚簇索引進行直接定位
    • 預先對資料進行邏輯排序,提高查詢時的排序效能 (對用於排序的索引,正序和倒序的設定是必須符合實際查詢情況的,否則查詢優化器將棄用索引)
  4. 那麼一次查詢會不會使用多個非聚簇索引呢?不會! 你可以使用 with index 語句來強制使用2個或以上的非聚簇索引,但是效率更差。

 

通過以上分析,至少我自己的腦子跳出了關於索引的一大堆概念理念,變得開闊明朗了,不知道您看懂沒  0.0


在 Topics 表上,將主鍵 Id 設定了唯一聚簇索引,另外增加了一個3個欄位的複合唯一索引 ForumId(asc) + LastRepliedTime(desc) + Id(desc) 和一個單列索引 UserId。
在 Posts 表上,主鍵仍然是 Id, 但不作為聚簇索引,我們把聚簇索引加在 TopicId 上,且不唯一。
這樣的索引設計,已經我們滿足了大部分查詢需求,但對於特定過濾條件的篩選,索引無能為力。我們總不能在想查詢的欄位上都加上索引吧,何況它們都是個布林欄位。

優化方法一: 標識欄位另存小表

前面我們說了非聚簇索引的兩個目的,而非聚簇索引對某些查詢場景表示無能為力,那我們只好自己想辦法解決了。我們通過以下的實際案例來解釋。

論壇有一個置頂貼的功能,就是把某幾個話題固定擺在最上面嘛。最簡單、最直接的實現途徑有2個:一是查詢按照置頂標識排序、二是把置頂貼查詢出來然後快取起 來。第一種辦法我們立馬可以放棄了,總不能為此專門給這個只有幾個列舉值甚至是 bool 型別的欄位加個索引吧。那麼我們就將它單獨查詢出來然後再快取。然後,又一個簡單、直接的查詢被想出來了,在 where 條件中加一個判斷條件。但是請記得我們正在查一個數據量龐大的表,即便面對500萬這樣的資料量,SQL Server 就會耗費幾個G的記憶體加上數分鐘時間,簡單的說,崩潰了。
面對這樣的需求,我們可以用一個更加直接、卻不太容易被第一想到的方式:找另外一個地方(表) 吧這些標記為置頂的話題 Id 單獨存起來。把置頂話題的 Id 單獨存一張小表,然後通過 inner 聯接查詢或者 in 語句去查詢 Topics 這個大表,無論哪種都是飛速的,因為通過主鍵聚簇索引的範圍查詢是 Seek 直接定位,是資料庫中最快的查詢計劃。
類似地,像精華貼等這種數量不龐大的結果集,我們可以使用同樣的辦法。最終仍然是按照原理去設計的:縮小對聚簇索引的掃描範圍。

優化方法二: 大表切成小表

通過上述優化,對於一個500萬資料量的資料庫已經是小菜一碟了。但是記得我們的設計目標是9位數,100000000,要仔細數才能數清楚0的個數的那種。即便排序索引做得再優秀,面對一個龐大的結果集進行排序,翻到第1000頁, 差不多30000行記錄往後仍然是很耗時間的,也就是為什麼大家翻到到一個巨大數目的頁碼時,被強制縮小到了一個限定範圍的頁數,不然就得崩潰了。

其他文章就介紹了一種“切”法:分表,物理切。
將不需要的資料歸檔,儲存在另外的歸檔表裡,讓被頻繁查詢的主表維持在一個可接受的大小範圍內。這種做法已經在諸多成熟程式中實現,當然是可行可靠的。但是帶來了2個比較不爽的問題,一要定時搬遷資料,二破壞了邏輯結構關係,讓筆者我這種完美癖很難接受。

因此我設計了另一種“切”的方式,邏輯切

回顧我們前面反覆在使用的一條規則,減少對聚簇索引的掃描範圍。如果我能提前知道哪些資料是不可能出現在結果集當中的,那不就可以提前排除了嗎?那問題就變成,我怎麼知道哪些資料不可能出現在結果集中呢?

建立一張冗餘表: DailyStatistics :
Date - 主鍵,儲存不包含時間部分的日期值
TopicCount - 當天的話題數量
PostCount - 當天的帖子數量
TopicIdStone - 當天最後一個話題的 Id
PostIdStone - 當天最後一個帖子的 Id

實際上是在業務邏輯層面對資料量做好了統計從而實現查詢優化。它由觸發器來維護,當對 Topics 和 Posts 表進行插入或刪除時,觸發器自動對這個統計表進行計算維護,不需要在我們的程式中進行額外的邏輯維護。
說到這裡,我想大家已經能夠明白接下來它能夠幹什麼了。日期是我們判斷話題歸檔的標識條件,通過這張統計表,我們把日期換算成了 Id 範圍,我們只需要加一句 Id between .. and ... , 再一次通過 Id 聚簇索引來縮小掃描範圍。

這張表的維護開支,只有簡單的觸發邏輯實現插入更新,並且是自動維護的,與維護一個巨大索引的開支相比簡直就是賺翻了;而這個表的資料量,10年不過3653條,毛毛雨嘛!