1. 程式人生 > >T-SQL執行內幕(1)——簡介

T-SQL執行內幕(1)——簡介

本文屬於SQL Server T-SQL執行內幕系列

前言:

    本文主體內容來自於:http://rusanu.com/2013/08/01/understanding-how-sql-server-executes-a-query/但是經常打不開,本人又在:https://www.codeproject.com/Articles/630346/Understanding-how-SQL-Server-executes-a-query 發現幾乎跟原文一模一樣的內容,並且網上大多收錄這篇而不是原文。 不過提醒一下第二篇文章中對原文進行了少量的刪減。

    一開始想直譯,但是看了內容之後,覺得略微有點難讀,加上自己對這方面也有點想法,所以打算把它作為一個引子,在不影響作者內容跟資訊準確性的前提下新增自己的看法。所以如果讀者看原文的話,會發現跟原文並不完全一樣。

    本文不適合初學者看,除非你的學習能力很好,但是不代表所謂的初學者不能看,我只是給個忠告而已,因為內容較深入,可能會影響興趣。

 

目錄:

       本系列內容較多,原文字身已經很多,加上個人新增的內容之後,篇幅過長,不便於閱讀,過長的篇幅也容易造成閱讀疲勞,所以我把它們拆分:

    SQL Server資料庫引擎有兩大核心:儲存引擎(Storage Engine)和查詢處理器(Query Processor),也叫關係引擎(Relational Engine)

  • 儲存引擎:負責在磁碟與記憶體之間以某種方式讀取資料。並且在這個過程中維護資料一致性及併發性。
  • 查詢處理器/關係引擎:接受提交到SQL Server的所有查詢,併為其選擇最佳執行計劃,然後把執行計劃執行最後返回結果。
    語句以T-SQL形式提交,而T-SQL是高階宣告性語言,只告訴資料庫引擎“要做什麼”,但是不會告訴它“要怎麼做”,而怎麼做這一步,就是查詢處理器中的查詢優化器的工作。下面來詳解。    


正文:

    如果你是一個普通資料庫開發人員,那瞭解增刪改查的T-SQL寫法基本上就能開始工作了,但是隨著時間的推移和外界的不斷變化,除非你轉到別的領域,不然遲早你要面對這個問題——為什麼我的SQL慢?這個問題衍生出另外三個有意思的問題:

  1. 怎樣發現慢查詢或者高開銷查詢?(Troubleshooting)
  2. 怎樣改進(PerformanceTuning)
  3. 監控效果(Monitoring)

    之所以說這些是“問題”,是因為大部分的人包括我自己在很多時候都知道要做這些,但是怎麼做?要回答這些越來越深入的問題,首先必須先了解根源——TSQL的執行內幕。瞭解T-SQL是如何執行的,那麼大部分的語句及其相關問題都能從中得到或多或少的啟發。才能更好地回答“為什麼我的資料庫效能差”。

    但是篇幅原因,本系列只是主要介紹SQLServer 的執行機制。閒話不說,先上圖:

    上圖是一個T-SQL從發起到得到結果的流程示意圖(最左下角是發起,最右下角是返回)。下面對一些後續要用到的術語和這個圖進行簡要介紹。圖中有幾個重要術語:請求(request)、任務(task)、工作執行緒(worker),下面將一一簡介。如果覺得概念比較亂,可以直接先跳到T-SQL執行內幕(2)——Tasks、Workers、Threads、Scheduler、Sessions、Connections、Requests一節。

Requests

    請求,SQL Server是一個客戶端-伺服器平臺,與資料庫互動的唯一方式就是通過傳送包含有命令的請求給資料庫。而客戶端與伺服器直接的通訊協議稱為TDS(Tabular Data Stream),如果讀者有興趣可以看一下MSDN的文件:https://msdn.microsoft.com/en-us/library/dd304523.aspx,伺服器的請求列表可見sys.dm_exec_requests 。

    應用程式使用諸如SqlClient、OleDB、ODBC、JDBC等驅動來實現這種協議。當應用程式需要資料庫完成任何事情時,都會通過TDS協議傳送一個請求(request)給資料庫引擎。

    簡單來說,每次對資料庫的操作都會以“請求”的形式傳送給資料庫伺服器。所以請求是T-SQL生命週期的開始。上圖左下角綠色塊。

    請求的主要分類有以下三類:Batch Request、RPC Request、Bulk Load Request

Batch Request:

    批請求,即一個請求中包含了一個或多個T-SQL文字(語句)。這種型別無引數,但可以包含本地變數。通常由SqlCommand.ExecuteReader()/ExecuteNonQuery()/ExecuteScalar()/ExecuteXmlReader ()等SqlCommand類並且引數列表為空的物件從客戶端發起。

    另外我們最常用的在SSMS中輸入一些普通SQL語句並執行的方式也是批請求。過去監控Long Running Query的時候常用的Profiler/SQL Trace工具,如果要抓批請求所使用的語句,需要選擇SQL:BatchStarting事件。這是批請求開始時的語句,也可以把SQL:BatchCompleted事件也包含進去以便計算執行時間。

    除此之外,Batch 對應的處理速度(可以在效能計數器中找到SQLServer: SQL Statistics: Batch Requests/Sec),也從側面看出整個系統的效能,是很重要的指標,不過不要單方面下定論,這個指標並不能單純指出問題,通常只能直接得知系統的“繁忙程度”而已。我不想在一開始就過於深入地討論如何優化,先沉下心來把原理搞懂了,很多問題就自然有了解決方法。

Remote Procedure Call Request:

    簡稱RCP請求,這種型別的請求包含任意數量引數的需要執行的RPC名字或Procedure ID。比如sp_execute,第一個引數是一段要執行的T-SQL文字。每個RPC是獨立的,不能混在其他SQL語句中。

    最常見的RPC請求就是在客戶端呼叫帶有引數的儲存過程。另外一個不嚴謹的區分就是RPC通常是外部應用(如Windows 服務、Web Services等發出的請求)。對這種請求的監控可以使用Profiler/Trace的RPC:Starting事件。我們可以從下圖的所屬類別大概看出,RPC事件歸到儲存過程事件中,而Batch歸到TSQL事件中,但是我覺得這個並不需要過多關注,除非你想做更深入的研究:


Bulk Load Request:

    Bulkload是一個特殊的請求,專用於bulk insert操作,比如使用bcp.exe或者SqlBulkcopy類進行大容量匯入。跟前面兩種請求不同,它是唯一一種“先執行再通過TDS協議傳輸請求”的請求。

小結

    當請求通過TDS到達伺服器之後,SQL Server會建立一個任務(Task)來處理請求,可以簡單理解為,當request到達SQL Server之後,後續操作都發生在SQL Server內部。從執行流程來說,現在已經是:Requests→TDS→Tasks

Tasks

    任務,上面說過,task(任務)是請求從開始到完成的表現方式。task可以有子任務(subtask),如果有多個task,SQL Server會使用task queue(任務佇列)來存放任務列表。如果請求是SQL 批,則task為整個批而不是單獨語句。在SQL批中的單獨語句不建立新的任務,除非語句(不是指批)以並行方式執行時會產生子任務來並行執行。一旦請求返回結果並且客戶端進行資料處理完畢(如使用SqlDataReader讀取資料),請求就算完成。

    伺服器的任務列表可見sys.dm_os_tasks。如果需要了解會話ID對應的Windows執行緒ID,可以使用下面語句檢視,得到執行緒ID之後,使用Windows效能監視器來監視執行緒的效能。這個在查詢一些服務是否有效能問題時比較有用,但是這個語句不返回正處於sleeping狀態的會話:

SELECT STasks.session_id, SThreads.os_thread_id  
  FROM sys.dm_os_tasks AS STasks  
  INNER JOIN sys.dm_os_threads AS SThreads  
    ON STasks.worker_address = SThreads.worker_address  
  WHERE STasks.session_id IS NOT NULL  
  ORDER BY STasks.session_id;  
GO
    當一個新請求到達伺服器並且建立一個對應的任務時,首先會處於PENDING(掛起狀態)。任務狀態可以有:
  • PENDING:正在等待工作執行緒,worker。
  • RUNNABLE:可執行但正在等待接收量程。
  • RUNNING:當前正在Scheduler中執行。
  • SUSPENDED:擁有worker但是正在等待某些事件。sys.dm_os_schedulers
  • DONE:已經完成。
  • SPINLOOP:陷入自旋鎖。

    在這一階段,SQL Server無法得知請求的具體內容,為了執行任務,SQL Server需要指派一個Worker用於服務這個任務。到這一步,執行流程就到了:Requests→TDS→Tasks→Workers

Workers

    工作執行緒,Worker是SQL Server執行緒池(Thread pool),當伺服器啟動時,根據SQL Server的max_worker_threads(下圖紅圈部分)按需建立。只有worker能執行程式碼,並且空閒的Workers會等待掛起(pending)的任務變成可用(Runnable)之後,worker才執行某個任務。從worker開始執行任務到任務完成之前,這個worker處於busy狀態。當沒有可用的worker時,任務(task)就會變成pending狀態等待有空閒的worker執行它為止。

 

    對於工作執行緒數,預設值為0,意味著自動配置,對絕大多數系統而言是最佳設定,但是並不代表永遠最佳,一般情況下,每個查詢會建立一個單獨的作業系統執行緒來服務請求(1:1),但是如果伺服器的連線達到數以百計的時候,為每個請求分配一個執行緒會佔用大量的系統資源,此時這個配置可以使SQL Server可以為更多的查詢請求建立一個工作執行緒(N:1),從而提高效能。官方建議如下:

CPU 數

32 位計算機

64 位計算機

<= 4 個處理器

256

512

8 個處理器

288

576

16 個處理器

352

704

32 個處理器

480

960

    當實際查詢請求數量小於這個最大工作執行緒數時,會使用1:1的方式,如果超過最大執行緒數時,SQL Server就會把工作執行緒集中線上程池(Thread Pool)中進行有效排程。

    但是注意如果所有的Worker都處於活動狀態,SQL Server可能出現停止響應的狀態,直到有工作執行緒可用為止。此時需要使用專用管理員連線(Dedicated Administrator Connection,DAC)連到SQL Server進行kill操作。

    對於SQL 批請求,worker會執行整個批而不是單獨的SQL。這就引出一個問題“SQL批中的語句能否並行執行?”答案是否定的,因為他們是序列執行,每個Thread(worker)每次只執行批中的一個SQL,完成後繼續執行批裡面的下一個SQL。對於那些使用並行執行(DOP配置要>1)的語句,會建立子任務,每個子任務使用相同的方式執行:建立一個PENDING的子任務然後等待worker(由於執行這個批的worker已經標註為佔用/繁忙,所以這裡的worker是另外一個不同的worker)執行。Worker的清單可以查詢DMV: sys.dm_os_workers。換句話說,對於一個並行執行語句,會有多個Worker來協助批處理執行,但是也是序列執行。

    當分配了Worker之後,執行流程就到了:Requests→TDS→Tasks→Workers→編譯/優化

小結:

    本節作為本系列的開篇,介紹了一些基礎概念,主要集中在從客戶端發起請求到SQL Server的過程,下一節將介紹Tasks、Workers、Threads、Scheduler、Sessions、Connections、Requests這幾個概念,以便更好地深入學習。