標籤:
查詢執行的總圖:
根據總圖的流程,詳細說明每個部分:
1. 請求(Request)
SQL Server是C/S架構的平台。與它互動的唯一方式就是發送包含資料庫命令的請求。應用程式和資料庫之前的通訊協定叫做TDS(Tabular Data Stream)協議。應用程式可以使用以下幾種實現了TDS協議的用戶端:
- The CLR managed SqlClient,
- OleDB,
- ODBC,
- JDBC,
- PHP Driver for SQL Server
- 開源的FreeTDS
TDS的請求分為以下幾類:
這種請求只包括T-SQL文本,不包含參數,但是可以包含本地變數。在SqlClient中帶有空參數列表的SqlCommand對象上執行SqlCommand.ExecuteReader(), ExecuteNonQuery(), ExecuteScalar()或者ExecuteXmlReader(),就會是批請求。通過Profile會觀察到SQL:BatchStarting事件。
- RPC請求(Remote Procedure Call Request)
它包含過程標識符(Procedure Identifier,用於)和任意數理的參數。不同的過程標識符代表了不同的系統預存程序。執行帶有不允許為 NULL 的參數列表的SqlCommand對象時,就是這種請求類型。通過Profile可以觀察到RPC:Starting事件。
- 批量載入請求(Bulk Load Request)
批量載入是批量插入操作所使用的一種特別的請求類型。例如BCP工具、OleDB的IRowsetFastLoad介面和SqlBulkcopy類。它是唯一一種在TDS協議中不需要完成包發送就可以開始執行的請求。開始執行後,就可以使用資料流中的資料進行插入操作了。
2. 任務(Task)
當完整的請求到達資料庫引擎,SQL Server會建立一個Task去處理此請求。可以通過sys.dm_exec_requests觀察請求情況。一個任務代表一個完整的請求,而不會是請求的一部分語句。同樣,對於請求中的部分語句,也不會建立新的任務。有些請求的中語句會並存執行,而Task會產生sub-task處理並行。當用戶端取走所有請求返回的結果集中的資料後,Task就完成了。
可以通過sys.dm_os_tasks觀察Task情況。
3. 背景工作處理序(Workers)
根據新請求所建立的Task,初始狀態是PENDING。這個階段,SQL Server並不知道請求的內容。Task須要執行這個請求,引擎就會分配worker去執行。(是分配,不是建立)
Workers是SQL Server的線程池。在SQL Server啟動過程中會初始化一定數量的Workers。可以通過Max_Worker_Threads參數,按需要配置最大線程數。只有Worker才執行代碼。當沒有閒置worker時,task變成Pending狀態。worker完成task後,變成可用狀態,才會去選擇Pending狀態的task執行。
在SQL批請求中,worker選擇task後,執行批請求中的每一個語句。很明顯,批請求中語句,是串列執行的。前一個完成,才會開始下一個。批中的某些語句會並存執行,這個並行是Task建立sub-task來完成的。而每一個sub-task會經曆和task一樣處理過程(如等待可用的worker來選取它並執行)。
可以通過sys.dm_os_workers查看worker及其狀態。
4. 文法解析和編譯(Parsing & Compilation)
當task開始執行,首先它要弄明白請求的具體的內容。這個階段SQL Server對請求中的T-SQL文本進行文法解析並產生表示請求的抽象文法樹(abstract syntax tree)。整個請求會被解析和編譯。如果在這個階段產生錯誤,則會返回編譯錯誤,並結果任務並釋放task和worker。
編譯T-SQL不會產生像本地CPU指令一樣的可執行代碼,也產生類似於位元組碼的東西。它產生查詢計劃(Query Plan)。查詢計劃描述了資料訪問的路徑和訪問對象的方法。
5. 最佳化(Optimization)
最佳化是從很多個查詢計劃中選擇出最優的一個。SQL Server採用基於成本的最佳化器。它會估算所有可能(大多數)的查詢計劃的成本,並選擇出成本最低的一個。成本主要通過計算查詢計劃需要讀取的資料大小(data size)。為了知道資料大小,SQL Server需要知道每個表的大小和列值的分布情況(通過統計資訊資料)。成本還會考慮CPU和記憶體使用量量。再通過一個公式將這些資料綜合個成本值,然後選取出成本值最小的那個執行計畫。
最佳化過程需要消耗時間和CPU,所以一當查詢計劃最終產生,則會被緩衝到計畫快取中,以備重用。
6. 執行(Execution)
一旦最佳化器選定了執行計畫,請求就可以開始執行了。執行計畫會被轉換成實際的執行樹。樹中的每個節點是一個操作符。所有操作符都實現三個抽象介面:open()、next()和close()。迴圈執行包括調用根節點的open(),然後逐級調用next()直到返回false,再調用close()。
葉級節點通常是一些物理資料訪問操作符(訪問實際的資料和索引),中間節點通常是一些實現資料過濾、排序和串連等資料操作的操作符。並存執行有一個專門的操作符:Exchange操作符。Exchange操作符發出多個線程,每個線程執行一個查詢計劃的子樹,然後再使用multiple-producers-one-consumer 方式彙總所有子線程的輸出。
資料修改操作也適用於這個執行方式。
有些操作符非常簡單,如TOP(N)。當調用它的next(),它會去調用子節點的next()並記錄資料。當重複執行N次後,它就返回false,並終止對子節點的調用和對相應分支子樹的迭代執行。
有些操作符非常複雜,如nested loop操作符。這需要跟蹤內外子節點迴圈迭代的位置,調用外節點的next(),值重繞(rewind)內節點並不斷調用內節點的next()直到找到匹配的值。
有些操作符需要等到擷取到它的所有子節點的輸出資料時,才能產生自己的輸出資料。這種行為方式也叫stop-and-go。如sort操作符,它第一次調用netxt(),不會返回資料,需要等到所有的資料被返回並排序,這才能返回資料。
HASH JOIN是一個非常複雜並且又是stop-and-go類型的操作符。為了構造hash表,它要調用構建側(build side)節點的next(),直到返回false。然後再調用探測側(probe side)的next(),直到找到在hash表中找到匹配的值,然後返回。重複探測側的操作,直到next()返回false。
7. 返回結果(Results)
查詢一旦開始執行就可以開始返回資料給用戶端程式。當執行樹開始產生返回資料後,最頂端的操作符會負責把資料寫入網路緩衝並發送給用戶端。執行中產生的返回結果,不會被緩衝到任何地方,一但產生就開始返回給用戶端。
顯然,通過網路返回資料給用戶端會受到網路流量控制協議的約束。如果用戶端不能及時地取走返回的資料,最終會阻塞資料發送方的發送行為,並使得查詢執行被掛起。當用戶端的資料接收能力正常後,發送方的發送行為和查詢執行會被重設,正常產生返回結果資料。
OUTPUT參數的輸出值,只能在執行計畫完成後,才能被寫入到資料流中。所以它也只能在所有返回結果被用戶端取走後,才能被讀取到。
總結:
1. 這是一篇譯文,計劃分為3部分。學習之用,非逐字翻譯,很多是結合自己的理解譯的,與原文內容相比,有一些增和刪。
2. 原文地址:Understanding how SQL Server executes a query
理解SQL Server是如何執行查詢的 (1/3)