小覷資料庫(SqlServer)查詢語句執行過程

Yunanw發表於2014-04-14

 近年來,越來越多的NoSql產品不斷的以技術革命的者的身份跳出來:“你看哥是多麼的快,你們關型型資料庫真是戰五渣阿”。是的,高效能的場景下NoSql真的很出彩。而我們關係型資料庫只能在牆角哭泣"是的,沒錯,他們真的好快”。

  但是他們為啥哪麼快?用了雷政富光環了嗎?我們從瞭解資料庫引摯執行過程來了解一些基礎知識,看看我們關係型資料庫到底慢在哪?我只粗懂一些SqlServer,只能從SqlServer講起。但是SQLServer是一個非常複雜的軟體。我們將通過一個查詢的執行過程讓你對SQlServer的核心引摯和執行過程做一個簡單的瞭解。

  由於Select語句和Update語句基本一樣,但是Update涉及到修改語句,所以我們直接從一條Sql語句開講。update 碼農表 set 女友=1 where 女友=0當我們碼農寫下這樣一條Sql,小手輕按F5。 一個女朋友就產生了 一條語句在客戶端被以TDS(Tabular Data Stream)協議形式中發給SqlServer服務端的網路介面(SQL Server Network Interface)。 什麼是TDS協議這不重要。 因為TDS是一個專屬協議,最早由Sysbase公司設計用於與資料庫服務互動。你無須過多關心。SNI是一個 協議層用於在服務端和客戶端建立網路連線,自SqlServer2005以後才有。它由一系列同時使用API組成。 
這些都不是我們要講的重點,我們接著往下說。SNI收到TDS包解包後發現,喲,小子又提交Sql語句了。就將這個包標識為一個Sql命令,將此Sql命令發給命令解析器 command parser。 command Parser首先檢查語法錯誤,如果發現語法錯誤,將錯誤通過協議層發回給客戶端。如果語法驗證通過,則會生成查詢樹。

查詢樹生成以後,SqlServer要進行一個非常消耗CPU的工作,就是根據查詢樹生成查詢計劃。SqlServer 並不是簡單的將一個查詢樹翻譯成查詢計劃,而是經過不斷的比較和權衡。一條SqlServer可能有很多種查詢計劃.比如Select Name from table where a=1 如果這個表上沒有任何索引,那麼可能生成的查詢計劃就是表掃描。如果在A列上有索引,那麼可能應用A列上的索引,也可能不用。總之,SqlServer比較各種方案找到一個開銷最小的執行計劃。而這個找到最小執行計劃的過程,也是比較消耗CPU資源的。

為了更加有效的利用資源,SqlServer會對查詢計劃行快取。將之前執行過的查詢計劃儲存在記憶體中反覆使用。 這裡有個很有意思的效能優化點。因為記憶體對SqlServer來說也是非常重要的。如果快取了大量的查詢計劃也會影響到SqlServer效能。那麼不快取費CPU,快取費記憶體。我們該怎麼辦?我們應該更加高效的利用查詢計劃快取。舉個例子。剛才我們執行的語句update 碼農表 set 女友=1 where 女友=0 然後又執行 update 碼農表 set 女友=0 where 女友=1 對於SqlServer來說,它會把這當成二條完全不同的語句來處理。

這裡有一個定義:以單獨的SQL語句的形式執行的查詢就是即席查詢(Adhoc)。我們經常在程式裡拼的sql,就是這種既席查詢。

如果你的系統中有大量的既席查詢,會產生大量查詢計劃快取。所以推薦大家在程式中寫Sql時,儘量的使用引數化。update 碼農表 set 女友=1 where 女友=@女友數 既高效又防Sql注入,何樂而不為呢?

到上面為止,完成這些工作的元件都屬於Sqlserver的關係引摯部分。關係查詢可以說是SqlServer中最複雜的元件。用於確定查詢的最佳執行方案。而市面上大多NoSql資料庫沒有關係引摯這部分。語句處理比較簡單。 這就節約了一部分開銷。

而資料的訪問和管理則由儲存引擎負責。事實上我也不算說儲存引摯部分的細節。我們只談談關係型資料庫和NoSql不太一樣的部分。SqlServer和大多數NoSql資料庫一樣,都是非常依賴記憶體的。記憶體比磁碟快。這是人所共知的。 SqlServer也有將資料快取中記憶體中的機制。每次查詢請求資料時 SqlServer先在在記憶體中查詢有沒有對應的頁。 這裡的頁是指一個段連續的8KB記憶體。這段記憶體是將資料庫檔案中的頁(每個頁8KB)直接對映到記憶體中的。如果發現有查詢需要的頁將直接將對應的頁的內容打包成結果返回。如果沒有,SqlServer則先去磁碟中找到對應的頁將它載入記憶體。再將記憶體中的頁返回。而查詢結束,記憶體中的頁也不在回收。將一直儲存到記憶體中。直到SqlServer發現作業系統可用記憶體不足,才會將一些不常使用的記憶體頁還給作業系統(多聰明)。而且SqlServer會預先分配一些空白的記憶體頁。這樣等到用時就不用現分配了。 知道了上面原理,你就能明白 為什麼Page Life Expectancy (PLE) 效能記數器的數值越大越好了。Page Life Expectancy (PLE) 越大表示一個頁面在記憶體在呆的時間越長。也就是說你的記憶體壓力越小。

可是,如果要是更新時SqlServer怎麼辦?我們知道關係型資料庫對永續性的要求比好多NoSql產品都有節操多了。那資料庫是如何保證永續性(喂不要想太多)和儘量提高效能的呢? 
SqlServer使用了一種叫預寫式日誌的技術。簡單來說就是你不是叫我幹活嗎?活還沒幹呢,我先把我要乾的活寫下來。然後等我有空時再幹活。這麼做有什麼好處呢? 
  首先,我們知道磁碟的隨機寫入效能是很低的,相反,順序寫入效能要比隨機寫入高很多。如果每次使用者更新資料時,都寫入到資料庫檔案,那麼很有可能產生一個隨機讀寫。這樣可能會影響效能。而SqlServer採用的是先寫入日誌。然後只更新記憶體中的對應的資料頁。而日誌一般都是順序寫入檔案尾部的。這樣一次隨機的讀寫就被轉換成一次順序寫加一次記憶體修改。效能自然有效提升。   而一般的NoSql資料庫預設情況下並不保證寫入的永續性。有的是定時刷到硬碟,但是並沒有預寫式日誌,有的是先讓你返回,成功不成功你再來問一次。

   那SqlServer又是如何保證記憶體中的資料被回寫到硬碟呢?SqlServer和上面提到有些NoSqL有點型別。SqlServer有一個叫 Lazy Writer 執行緒,用於週期的檢測 空閒快取頁的值,如果這個值較低,他將掃描正個資料快取將較長時間沒有沒使用的頁面過期。如果他發現一個很長時間沒有被使用的髒頁,他也會將被更的頁面但還沒有回寫到硬碟的頁–也叫髒頁寫入到磁碟,並將他在記憶體中標記為空閒頁。 
   那如果SqlServer突然斷電,而記憶體中的髒頁並沒有來得及寫入到硬碟腫麼辦?SqlServer使用一種叫檢查點的機制。 
   檢查點程式確保任何和已提交事務相關的髒頁能被寫入到磁碟,也將未提交事務的髒頁寫入磁碟以確保效率。和Lazy Writer 不同,檢點查並不將頁面移出快取。他只是將髒頁刷入磁碟,然後頁面頭標識這個頁面為 Clean Page。 預設情況下,在一個忙碌的伺服器上,SQL Server 大約每一分鐘發起一個檢查點並記錄在事務日誌裡。如果SqlServer 實現或資料庫重新啟動,那麼恢復程式通過讀取事務日誌就知道那些工作是在檢查點之前做的。檢查點之前做的工作,他不用理會。注意:CheckPoint的觸發條件,是在CheckPoint期間生成日誌的大小。因此,大家見過記憶體中有很多髒頁,卻不引發CheckPoint的情況。 
   當SQL Server非正常原因關閉時,也就是在沒有走CheckPoint(會在下面提到)時關閉了資料庫,此時資料庫中資料本身可能存在不一致的問題。因此在資料庫再次啟動的時候,會去掃描日誌,找出那些未提交卻寫入持久化儲存的資料,或已提交卻未寫入持久化儲存的資料,來進行Undo和Redo來保證事務的一致性。SqlServer 會嘗試保證資料庫恢復時間小於1分鐘,但是,至少需要有10M資料寫入日誌,sQLServer才會觸發CheckPorint

到這,你也就明白了為什麼資料庫會慢於NoSql產品了。當然這也只是我所理解的一方面。可能還有很多地方NoSql有優化的地方。如果簡化掉關係引摯,去掉預寫式日誌,資料庫效能會不會也有質的飛躍呢?可這一切又值得嗎? 
另外,如果你的資料檔案和日誌如果在一個磁碟上,那個可能這個預入式日誌優勢會大打折扣。原因你一定想的到。 
其實我只是借這NoSQL火抱一下大腿。這篇貼子並沒有大多NoSql的內容。我對NoSql也不太瞭解。但是現在你不會NoSql你都當了意思和人家打招呼了。NoSql在一些對事務性要求不太高的地方大有用武之地。我打算看完硬碟裡的波多老師作品就去學習Nosql 
了。

相關文章