摘要:本文介紹了在客戶機上處理 Microsoft SQL Server 查詢的方式,各種客戶機與 SQL Server 的交互方式,以及 SQL Server 在處理客戶機程序的請求時需要完成的工作。(打印共 26 頁)
簡介
Microsoft(R) SQL Server(TM) 內部機制和結構是一個非常大的主題,因此本文僅限於程序開發人員感興趣的問題,集中研究其他源中沒有徹底討論的問題。在討論 SQL Server 的結構時,我們主要觀察客戶機的處理過程,研究不同的客戶機程序與 SQL Server 的交互方式,以及 SQL Server 如何處理客戶機的請求。還有一些討論 SQL Server 其他方面的信息源,特別是 Microsoft PRess 出版的 Inside SQL Server 7.0,作者是 Ron Soukup 和 Kalen Delaney,這本書非常詳細地討論了 SQL Server 存儲引擎的內部機制和處理方法,不過對查詢處理器的討論不夠深入。本文正填補了這個空白。
我們期望本文有助於讀者編寫出更好的應用程序。通過本文,讀者會在提高程序性能方面得到新的啟發,產生新的理解。
SQL Server 是一種客戶機/服務器系統
多年來,SQL Server 一直被認為是一種客戶機/服務器系統。事實上,Sybase DataServer(以此為基礎開發了原始的 SQL Server)正是第一個作為客戶機/服務器系統開發的商用關系數據庫系統。那這又說明了什麼呢?這不只意味著 SQL Server 是一個雙層系統。從傳統上看,雙層系統意味著客戶機應用程序運行在一台機器上,向另一台計算機上的服務器發送請求。而對於 SQL Server,客戶機/服務器意味著 SQL Server 的組成部分,即客戶機 API 部分,駐留在處理結構中的遠端,與服務器組件本身是分開的。
在典型的雙層模型中,客戶機程序部分駐留在台式機上,具有大量客戶機應用程序邏輯和業務邏輯,並且會直接向數據庫系統發出請求。然後,客戶機得到服務器響應這些請求所返回的數據。
三層系統也采用了同樣的模型。多年以來,SQL Server 一直用在事務處理監視系統中,例如 BEA 的 Tuxedo 以及 Compaq 的 ACMSxp,這些系統早在二、三十年前就采用了典型的三層模型。三層模型在今天基於 Web 的應用系統中占據了支配地位,這類系統以 Microsoft 的 MTS 以及新的 COM+ 1.0 為代表。從 SQL Server 的角度看,三層解決方案中的客戶機程序是放在中間層的。中間層直接與數據庫交互。實際的桌面,或瘦客戶機(Thin ClIEnt),使用其他機制並通常直接與中間層交互,而不是直接與數據庫系統交互。圖 1 描述了這種結構。
圖 1. 三層系統模型
客戶機結構
從結構的角度看,SQL Server 關系服務器組件本身並不真正關心客戶機程序運行的位置。事實上,就 SQL Server 而言,即使在運行 SQL Server 的同一台機器上運行應用程序,仍然還是客戶機/服務器模型。服務器運行一個單獨的多線程進程,為來自客戶機的請求提供服務,不管客戶機的位置在哪裡。客戶機程序代碼本身是單獨的運行在客戶機應用程序內部的 DLL,與 SQL Server 的實際接口是在客戶機和服務器之間對話的“表格數據流”(Tabular Data Stream, TDS) 協議。
一個常見的問題是“什麼是 SQL Server 的本機接口呢?”很長時間以來,很多開發人員一直都不願意使用 ODBC 這樣的接口,因為他們認為由 Sybase 開發的客戶機 API,也就是 DB-Library,是 SQL Server 的本機接口。實際上,SQL Server 關系服務器本身並沒有本機 API,它的接口就是在客戶機和服務器之間的通信流協議 TDS。TDS 把客戶機發送給服務器的 SQL 語句封裝起來,也把服務器返回給客戶機的處理結果封裝起來。任何直接處理 TDS 的 API 都是 SQL Server 的本機接口。
讓我們來看一下客戶機的組件,如圖 2 所示。客戶機結構中的某些部分就不在這裡討論了,因為它們不屬於 SQL Server 的范疇。但如果您在編寫應用程序的話,就必須了解這些部分。大家知道得最多的應該是各種對象模型,如果您正在編寫 ASP 或 Microsoft Visual Basic(R) 應用程序,就需要通過 ADO 與數據庫系統交互,而不是直接調用底層的 API,例如 ODBC 或 OLE-DB。ADO 映射到 OLE-DB,而 RDO 映射到 ODBC。因此,作為這種最常用的編程模型的對象模型,並不是 SQL Server 客戶機結構中的嚴格意義上的組件。此外,還有另外一些組件可以插接到 SQL Server 基礎結構上面的這一層。OLE-DB 的“會話池服務提供程序 (session Pooling Service Provider)”就是這種組件的一個例子。
圖 2. 客戶機結構
客戶機接口
SQL Server 有兩個接口可以認為是 SQL Server 7.0 的本機接口,即 OLE-DB 和 ODBC。DB-Library 接口也是本機的,它與 TDS 通信,但是 DB-Library 使用的是 TDS 較老的版本,需要在服務器上進行一些轉換。現有的 DB-Library 應用程序仍然可以繼續與 SQL Server 7.0 協同使用,但是很多新的功能和性能提高等好處只能通過 ODBC 和 OLE DB 才能利用。更新 DB-Library 使其支持 SQL Server 7.0 的新能力,將會導致與現有應用程序的很多不兼容性,因此需要修改應用程序。ODBC 在五年之前就替代了 DB-Library,是新的 SQL Server 應用程序更理想的 API,因此引入不兼容的 DB-Library 新版本並不明智。
從圖 2 可以看到,所有這些客戶機 API 都有三個部分。最上面的部分實現 API 的細節,例如行集和游標應該是什麼樣等等。TDS 格式化程序負責處理實際請求,例如 SQL 語句,並將其封裝成 TDS 消息包,發送給 SQL Server,獲得返回的結果,然後再把結果反饋到接口實現。
還有一些供所有提供程序使用的公共庫代碼。例如,BCP 設備就是 ODBC 和 OLE-DB 都可以調用的庫。DTC 也是這樣。第三個例子是 ODBC 規范的 SQL 語法,即帶有參數標記的 CALL 語法,這些對於所有提供程序都是通用的。
除了我們在前面已經提到的局限性,即 DB-Library 仍然只能使用 SQL Server 6.5 版,TDS 協議對於所有 API 都是相同的。ODBC 和 OLE-DB 在與 SQL Server 7.0 通信時使用 SQL Server 7.0 版,但也能夠與 6.5 或 6.0 服務器通信。另一個是 Net-Library,這是一個抽象層,客戶機和服務器都在此層上同網絡抽象接口通信,不必為 ipX 還是 TCP/IP 困擾。在這裡我們將不討論 Net-Library 的工作細節;只要知道它們的工作基本上是將來自的網絡通信底層的細節隱藏起來不讓軟件的其他部分看到就可以了。
從客戶機的角度看服務器
前面已經提到過,客戶機與 SQL Server 通信的主要方法就是通過使用 TDS 消息。TDS 是一種簡單協議。當 SQL Server 接收到一條消息時,可以認為是發生了一個事件。首先,客戶機在一個連接上發送登錄消息(或事件),並得到返回的成功或失敗的響應。當您希望發送 SQL 語句時,客戶機可以把 SQL 語言消息打包發送給 SQL Server。另外,當您希望調用存儲過程、系統過程或虛擬系統存儲過程(我們後面還要詳細討論)時,客戶機可以發送 RPC 消息,這種消息相當於 SQL Server 上的一個 RPC 事件。對於上面的後兩種情況,服務器會以數據令牌流的形式送回結果。Microsoft 沒有把實際的 TDS 消息寫入文檔中,因為這被認為是 SQL Server 組件之間的私用契約。
目錄存儲過程是另一類關鍵的客戶機/服務器的交互部分。這些存儲過程首先在 ODBC 的 SQL Server 6.0 中出現, 包括諸如 sp_tables 和 sp_columns 等存儲過程。ODBC 和 OLE-DB API 定義了描述有關數據庫對象的元數據的標准方法,這些標准需要適用於所有類型的 RDBMS 服務器,而不必調整為 SQL Server 自己的系統表。不是客戶機向服務器發送對系統表的多個查詢,並在客戶機端建立標准的元數據視圖,而是創建一組存儲在服務器上的系統存儲過程,並對 API 返回適當格式的信息。這種方法使得通過一次通信就可以完成很多重要的元數據請求。
為 ODBC 編寫的過程已經寫入文檔,通常適合需要從系統表中獲取信息但其他機制沒有提供這種方法的情況。這使得 Transact-SQL 過程和 DB-Library 應用程序可以訪問元數據,而不需要編寫對 SQL Server 系統表的復雜查詢,並且使應用程序不受今後 Microsoft 修改系統表的影響。
OLE DB 定義了一組架構行集,它們類似於 ODBC 的元數據,但又和它不同。它創建了一組新的目錄存儲過程,以更有效地為這些架構行集植入數據。但是,這組新的存儲過程沒有寫入文檔,因為這些存儲過程重復了早先提供的功能。通過現有的若干種方法都可以得到元數據,因此 SQL Server 開發組決定不顯露這些並沒有為編程模型增加新內容的對象。
客戶機與服務器的交互還有第三個方面。它最初出現在 SQL Server 6.0 中,但是沒有得到普遍使用。這就是虛擬系統存儲過程的概念;在 SQL Server 7.0 中起很重要的作用。當第一次為 SQL Server 6.0 開發服務器端游標時,開發人員就需要選擇采取什麼方法管理客戶機/服務器的交互。游標並不特別適合現有的 TDS 消息,因為這些消息允許逐行返回數據,不需要客戶機指定額外的 SQL 語句。開發人員本來可以向 TDS 協議添加更多的消息,但是需要修改太多的其他組件。SQL Server 6.0 中的 TDS 版本還需要向 Sybase 版本靠攏,以便確保兩者的可互操作性,於是開發人員選擇了另外的處理機制。他們開發了外表看起來像是系統存儲過程的新功能(服務器端游標),實際上是指向 SQL Server 代碼的入口存儲過程。它們被客戶機應用程序使用標准的 RPC TDS 消息來調用。它們被稱為虛擬系統存儲過程,因為在客戶機上,它們像其他存儲過程那樣被調用,和其他存儲過程不同的是,它們並不是由簡單的 SQL 語句組成。大多數虛擬系統存儲過程都是私用的,並且沒有寫入文檔。對於游標過程,所有 API 都顯露其自有的一組游標 API 模型和它們自己的游標操作函數,因此沒有必要為存儲過程本身編寫文檔。即使是在 Transact-SQL 語言中,也有顯露游標的語法,可以使用 DECLARE、OPEN、FETCH 等,所以完全沒有必要為虛擬系統存儲過程編寫文檔,例如 sp_cursor,因為這些過程只在內部使用。
ODBC 和 OLE DB 中出現了帶參數的查詢和准備/執行模型的概念。在 SQL Server 7.0 以前的版本中,這些概念是由客戶機 API 中的代碼實現的。在 SQL Server 7.0 中,Microsoft 為這些概念添加了對“關系服務器”的支持,並且通過新的虛擬系統存儲過程顯露了這種支持。本文後面還要介紹這些功能,以及服務器如何支持這些功能。通過 sp_executesql 過程對帶參數的查詢的支持,被認為對直接 Transact-SQL 和 DB-Library 的使用特別有用,所以將其寫入了文檔。准備/ 執行的過程,被 ODBC 驅動程序和 OLE DB 提供程序專用。
這樣,可以與 SQL Server 通信的所有客戶機程序,都建立在這三組功能之上:TDS 協議、目錄存儲過程和虛擬系統存儲過程。
服務器結構
SQL Server,或更確切一點地說,是“SQL Server 關系服務器”,經常被說成是由兩個主要部分組成,即關系引擎和存儲引擎。正如前面提到過的那樣,已經有很多文獻介紹存儲引擎的細節了,所以本文主要介紹關系引擎的功能。圖 3 給出了 SQL Server 關系引擎部分的主要組件。所給出的組件可以分為三組子系統。左邊的組件編譯查詢,包括查詢優化器。查詢優化器是所有關系數據庫引擎中的最神秘的部分之一,從性能的角度看也是最重要的部分。查詢優化器負責提取 SQL 語句中的非過程請求部分,並將其翻譯成一組磁盤 I/O、過濾以及其他能夠高效地滿足該請求的過程邏輯。圖中右側是執行基礎結構。這裡實際上只有很少的功能。當編譯組件的工作完成之後,所產生的結果只需用很少幾個服務即可直接執行。
圖 3. 服務器結構
圖的中間是稱為 SQL Manager 的部分。SQL Manager 控制著 SQL Server 內部的所有數據的流動。SQL Manager 控制著 RPC 消息,在 SQL Server 7.0 中,絕大多數來自客戶機的功能調用都是通過 RPC 消息進行的。上一節中介紹的虛擬系統存儲過程邏輯上也是 SQL Manager 的一部分。通常,作為 TDS SQL 語言消息的 SQL 語句直接在編譯一端執行,與早期版本相比,SQL Server 7.0 較少使用這種方法,但還算是比較常見的。執行結果由稱為 ODS 的執行引擎中的組件格式化為 TDS 執行結果消息。
絕大多數輸出都來自圖中的執行端,而且輸出結果也真正出自表達式服務。“表達式服務”庫是進行數據轉換、謂詞評估(過濾)以及算法計算的組件。它還利用了 ODS 層,把輸出結果格式化為 TDS 消息。
還有幾個組件,我們只是在這裡簡單地提一下,這些組件在關系引擎內部提供附加服務。這些組件中的一個是目錄服務組件,用於數據定義語句,例如 CREATE TABLE、CREATE VIEW 等。目錄服務組件主要放在關系引擎中,但是實際上大約有三分之一的目錄服務組件是在存儲引擎中運行的,所以可以看作是共享組件。
關系引擎中的另一種組件是“用戶模式調度程序 (UMS)”,這是 SQL Server 自己內部的纖程和線程規劃器。把任務分配給纖程或線程是一種非常復雜的內部機制,取決於對服務器如何配置,以及在 SMP 系統中允許 SQL Server 進行處理器之間的適當的負載平衡。UMS 還可以避免 SQL Server 由於同時運行太多的線程而導致性能過低。最後,還有大家熟悉的系統過程,邏輯上它們也屬於關系引擎的一部分。這些組件肯定不是服務器代碼,因為可以很容易地使用 sp_helptext 檢查定義這些過程的 Transact-SQL 代碼。但是,系統過程被作為服務器的一部分來對待,因為系統過程的用途是顯露重要的服務器能力,像系統表一樣,以供應用程序在更高的層次上和更適當的層次上使用。如果應用程序開發人員將較高層次的系統過程 — 更容易使用 — 作為一種接口,即使隨著版本的更新,原始層次上的系統表發生變化時,應用程序仍然可以繼續使用。
處理 SQL 語句時的客戶機/服務器交互
下面我們將討論當客戶機應用程序與 SQL Server 交互時客戶機的動作。以下是一個 ODBC 調用的例子:
SQLExecDirect(hstmt, "SELECT * FROM parts where partid = 7",
SQL_NTS)
(OLE-DB 也有一個與這個調用幾乎直接等價的調用,此處不再討論這個調用,因為這個調用實際上與 ODBC 調用相同。)該 ODBC 調用取一個 SQL 語句,然後將其發送給 SQL Server 來執行。
在這個具體的查詢語句中,我們從零件表中提取具有特定零件標識號的所有行。這是特定 SQL 的一個典型例子。在 SQL Server 7.0 以前的版本中,特定的 SQL 與存儲過程的一個顯著差別是,查詢優化器所生成的計劃從不緩存。查詢語句要被讀入、編譯、執行,然後再拋棄計劃。在 SQL Server 7.0 中,正如稍後還要討論的,實際上提供了可以緩存特定查詢語句的計劃的機制。
在這條語句被送往 SQL Server 之前,還必須要問幾個問題。所有客戶機程序都要提供某種游標說明,所以客戶機程序在內部必須詢問的一個問題是,程序員請求的是什麼樣的結果集或什麼樣的游標。最快的類型是在文檔中被稱為默認結果集的游標。這種游標由於歷史上的原因被稱為消防站游標,有時甚至根本不把它作為游標看待。當 SQL 請求被送到服務器之後,服務器開始把結果返回給客戶機,這個返回結果的過程持續進行,直到把全部數據集發送完畢為止。這就像一個將數據抽給客戶機的大型消防站。
一旦客戶機程序確定了這是默認結果集,則下一步就是確定是否有參數標記。使用這個 ODBC SQLExecDirect(以及 OLE-DB 中等價的調用)調用的選項之一是,不是在 WHERE 從句中給出像 7 這樣的具體值,而是可以用一個問號來傳遞參數標記,如下所示:
SQLExecDirect(hstmt, "SELECT * FROM parts where partid = ?",
SQL_NTS)
請注意,您必須分別提供實際的參數值。
客戶機需要知道 SQL 語句中是否有參數標記,或者它是否為真正特定的非參數化 SQL。這將影響到客戶機將用這個語句在內部做什麼,並確定將什麼作為消息真正發送給 SQL Server。在沒有問號時,很明顯,客戶機只想將這個請求作為 SQL Language TDS 消息發送,然後客戶機將位於流水的末端,並將結果返回。然後客戶機能將結果返回給基於應用程序參數的應用程序。客戶機的內部處理選擇會模糊一點,這取決於您通過 ODBC 或 OLE DB API 請求什麼。例如,應用程序不直接請求默認結果集。相反,在 ODBC 中,如果請求一個只讀的、只向前的且每次只給出一行的游標,那麼對於客戶機內部運行來說,這就是在定義流水游標(默認結果集)。
流水游標有一個主要問題。除非客戶機已將所有的行全部接收完畢,客戶機不能將任何其他 SQL 語句向下發送給服務器。因為結果集可能有很多行,所以有些應用程序使用流水游標時不能順利運行。後面將要描述的只向前的快速游標,是 SQL Server 7.0 版的一個新特點,尤其適合於處理這種情況。
在 SQL Server 7.0 版之前,SQLExecDirect 調用在很大程度上是以相同方式處理的,而不管是否用參數標記來代替常數。如果您定義一個參數標記,客戶機將實際取您通過不同調用提供的值(本節的開始示例中的值“7”),並將它插入問號處。然後,使用代替值的新語句被向下發送,作為一個特定的 SQL 語句。在服務器上使用參數化的 SQL 沒有任何好處。
然而,在 SQL Server 7.0 版中,如果 SQLExecDirect 使用了參數標記,向下發送給 SQL Server 的 TDS 消息便不是 SQL 語言消息。相反,它被下發給使用 sp_executesql 過程的服務器,所以,就 TDS 協議來說,它是 RPC。在客戶機上,結果基本上相同。客戶機將取回數據流水。
如果您不想取回這個數據流水,則可以始終使用塊游標或可滾動游標。在這種情況下,數據流變得大不相同。調用是對通過 SQL 文本中的 sp_cursoropen 輸入點(這些虛擬存儲過程之一)進行的。該 sp_cursoropen 利用 SQL 來增加附加邏輯,以使其滾動,它潛在地將某些結果重定向到一個臨時表,然後用句柄給游標一個響應,表明游標現在是打開的。仍然在程序員的控制之外,客戶機調用 sp_cursorfetch,將一行或多行轉到客戶機上,然後返回到用戶應用程序。客戶機還可使用 sp_cursor 來重新配置游標,或改變某些統計數字。在您處理完游標之後,客戶機將調用 sp_cursorclose。
讓我們看一個簡單的情況,即只返回一行給客戶機。至於默認的結果集,需要從客戶機到服務器往返發送一次消息。 SQL 消息(或 sp_executesql)向下發往服務器,然後結果返回來。在同一行(非流水)的游標情況下,您會看到傳統情況下能用 SQL Server 看見的東西。一個往返行程用於打開,一個往返行程用於取得數據,一個往返行程用於關閉。這個過程使用消息的次數是默認結果集使用的三倍。在 SQL Server 7.0 中,有一種所謂只向前的快速游標, 它使用同樣的游標結構。它與流水的表現不一樣,因為在發送任何附加 SQL 消息之前,它不需要您處理全部結果行。所以,如果您帶回 5 行,還有更多的數據,您仍能將更新向下發送給服務器。
一個只向前的快速游標在服務器上比常規游標更快,它讓您指定兩個附加選項。一個稱為自動取數,另一個稱為自動關閉。自動取數將返回第一個行集合,作為打開的響應消息的一部分。自動關閉在讀完最後一行後自動關閉游標。因為它是只向前的和只讀的,所以不能回滾。SQL Server 只傳回一個帶有說明游標已關閉的最後數據集的消息。如果您正在使用只向前的快速游標,則在行數少的消息裡,您可向下與同一往返行程通信。如果您有很多行,則您至少還要對每一行塊支付附加開銷。如果您使用只向前的快速游標,那麼游標處理會更加接近默認的結果集。
SQLExecDirect 模型流程如圖 4 所示。
圖 4. 客戶機/服務器交互
准備/執行模型
除了執行直接模型(在 ODBC 中用 SQLExecDirect 調用)外,在 ODBC 和 OLE-DB 中,還有一種執行模型,稱為准備/ 執行模型。定義要執行的 SQL,是作為一個獨立於實際執行 SQL 的步驟來完成的。以下是 ODBC 中的一個例子:
SQLPrepare(hstmt, "SELECT * FROM parts where partid = ?", SQL_NTS)
SQLExecute(hstmt)
在 SQL Server 7.0 版本之前,准備/執行從來都不是 SQL Server 的本機模式。如今在 7.0 版本中,有兩個提供本機接口的虛擬系統存儲過程。對於准備調用,我們要再次研究游標的類型,然後調用 sp_prepare 或 sp_cursorprepare。這些過程會完成 SQL 或存儲過程的編譯,但不會實際執行計劃。相反,虛擬系統存儲過程只是返回該計劃的句柄。現在,應用程序可以反復地執行 SQL 了,例如傳入不同的參數值,而不需要重新編譯。
在 SQL Server 6.5 中,由於沒有本機接口,需要模擬准備和執行兩個階段。可以通過下面的兩種方法做到這一點。在第一種方法中,不會真正出現准備階段。只有執行部分返回元數據(有一些選項可以做到這一點),所以 SQL Server 可以把結果的格式描述返回給應用程序。在第二種方法中,SQL Server 實際上創建一個特定存儲過程,這個過程是單個用戶私用的,不能共享計劃。這第二種方法可能會占滿 tempdb 數據庫的空間,因此大多數應用程序開發人員都通過 ODBC 配置對話框中的復選框,關閉此選項,以使用第二種方法。
在 SQL Server 7.0 中,准備/執行方法是 SQL Server 的本機功能。准備好 SQL 語句之後,才會執行它。至於默認的結果集,應用程序只需要調用 sp_execute,提供准備操作生成的句柄,語句就會被執行。對於游標,與其他游標處理過程看起來很相似,事實上,它也具有相同的特性,包括如果游標是快速只前向型,還可以使用 autofetch 和 toclose。