當確定了應用性能問題可以歸結到某一個,或者幾個耗時資源的語句后,對這些語句進行調優,就是數據庫管理員或者數據庫應用程序開發者當仁不讓的職責了。語句調優是和數據庫打交道的必備基本功之一。
當你面對一個“有問題”的語句時,應該怎么分析它的問題所在,最后達到優化語句的目的呢?首先要想一想,“有問題”的語句“問題”究竟在那里?也就是說,你要優化的目標是什么。常見的需求有:
1) 語句需要訪問大量的數據頁面,造成內在壓力、磁盤繁忙等。
對於這類問題,所關心的是為什么語句要執行要訪問這么多數據頁面?是語句的結果集本身就比較大;還是SQL SERVER沒有辦法有效地seek,而是像大炮打蒼蠅一樣從大量的原始數據里找出需要返回的結果;還是因為數據頁面里有很多碎片,導致SQL SERVER讀了很多頁面,但是每個頁面里的數據量不多。這些都是要考慮的因素。
2) 在內存沒有壓力的前提下(語句所訪問的頁面都事先緩存在內存里),語句運行的時間還是很長。
語句的運行時間一般會主要花在這3步上:語句編譯、語句執行和結果集返回。結果集返回的速度和SQL SERVER自身沒有太大關系,所以一般不會在語句調優的時候來考慮。語句調優時要搞清楚編譯和執行各花了多少時間,哪 一段時間有優化的空間,以及怎么來優化。
3) 單個語句執行時間可以接受,但是苦CPU使用量比較大,多個語句並發執行會造成SQL SERVER CPU高。
有些語句單句執行可能一兩秒鍾就能執行完畢,對用戶來講還在可接受的范圍。但是它的CPU間可能也是在一兩秒,甚至更長。如果同時有十幾個用戶在跑同樣的語句,SQL SERVER 就會滿負荷了。語句的CPU時間也分編譯階段和執行階段。優化者要先搞清楚這兩個階段各用了多少CPU資源,然后再看看有沒有優化降低CPU使用量的可能。
4) 語句單獨執行看不出有大問題,但是並發執行就容易遇到阻塞和死鎖。
這個也是語句調優的一個重要任務。很多語句執行速度很快,使用資源量SQL SERVER也能夠承受,但是就是容易引起阻塞和死鎖。這種現象往往是由於應用在某個表或者索引上的並發度特別高,而問題語句申請的鎖數量比較大造成的。當然有時候可以使用Query Hint 來強制 SQL SERVER使用粒度比較小的鎖。但是這往往不是最好的解決辦法,也可能解決不了問題。最理想的方法,是通過調整語句運行方式,引導它申請盡可能少的、粒度盡可能小的鎖。這里也要做語句調優。
在做這些調優的時候,首先要對目標語句做估算,看看它優化的空間有多大。有些語句本身比較簡單,可以通過調整索引的方法迅速提高性能,這樣的調優是很值得做的。有些語句非常復雜,或者返回的結果集很大,通過調整SQL SERVER這里的設置,提高性能的空間往往不大。這個時候就要考慮,語句本身是不是能夠換一種方法實現。很多時候改一下語句,把一條大的語句拆分成若干條小的語句,或者去掉一些不必要的邏輯,會達到事半功倍的效果
在談論如何做語句調優的具體方法之前,必須先介紹一下最必需的背景知識。不了解這些知識 ,做語句調優就只能基本靠猜。所需要的背景知識主要包括理解索引和統計信息,理解什么是統計和重編譯,並且能夠基本讀懂語句的執行計划。以下為例子,借助MS示例數據庫AdventureWordks來介紹。
--測試用例 USE AdventureWorks2008 GO IF OBJECT_ID ('SalesOrderHeader_TEST') IS NOT NULL DROP TABLE dbo.SalesOrderHeader_TEST GO IF OBJECT_ID ('dbo.SalesOrderDetail_TEST') IS NOT NULL DROP TABLE dbo.SalesOrderDetail_TEST GO -- (31465 行受影響) SELECT * INTO dbo.SalesOrderHeader_TEST FROM Sales.SalesOrderHeader -- (121317 行受影響) SELECT * INTO dbo.SalesOrderDetail_TEST FROM Sales.SalesOrderDetail -- 建立聚集索引 CREATE CLUSTERED INDEX SalesOrderHeader_TEST_CL ON dbo.SalesOrderHeader_TEST(SalesOrderID) -- 建立非聚集索引 CREATE NONCLUSTERED INDEX SalesOrderDetail_TEST_NCL ON dbo.SalesOrderDetail_test(SalesOrderID) go
SalesOrderHeader_TEST 里存放的是每一張訂單的頭信息,包括訂單創建日期、客戶編號、合同編號、銷售員編號等,每個訂單都有一個單獨的訂單號。在訂單號這個字段上,有一個聚集索引。
SalesOrderDetail_TEST 里存放的是訂單的詳細內容。一張訂單可以銷售多個產品給同一個客戶,所以SalesOrderHeader_TEST 和SalesOrderDetail_TEST是一對多的關系。每每詳細內容包括它所屬的訂單編號,它自己在表格里的唯一編號(SalesOrderDetailID)、產品編號、單價、以及銷售數量等。在這里,先只在SalesOrderDetailID 上建立一個非聚集索引。
按照AdventureWorks里原先的數據, header_test 里面有3萬多條訂單信息,detail里有12萬多條訂單詳細記錄,基本上一條訂單有3-5條詳細記錄。這是一個正常的分布。
下面再在 header_test 里面加入9條訂單記錄,他們的編號是從75124 到75132這是9張特殊的訂單,每張有12萬多條詳細記錄。也就是說 deatil_test里會有90%的數據屬於這9張訂單。
declare @i int set @i = 1 while @i < 10 begin INSERT INTO [AdventureWorks2008].[dbo].[SalesOrderHeader_TEST] ([RevisionNumber] ,[OrderDate] ,[DueDate] ,[ShipDate] ,[Status] ,[OnlineOrderFlag] ,[SalesOrderNumber] ,[PurchaseOrderNumber] ,[AccountNumber] ,[CustomerID] ,[SalesPersonID] ,[TerritoryID] ,[BillToAddressID] ,[ShipToAddressID] ,[ShipMethodID] ,[CreditCardID] ,[CreditCardApprovalCode] ,[CurrencyRateID] ,[SubTotal] ,[TaxAmt] ,[Freight] ,[TotalDue] ,[Comment] ,[rowguid] ,[ModifiedDate]) SELECT [RevisionNumber] ,[OrderDate] ,[DueDate] ,[ShipDate] ,[Status] ,[OnlineOrderFlag] ,[SalesOrderNumber] ,[PurchaseOrderNumber] ,[AccountNumber] ,[CustomerID] ,[SalesPersonID] ,[TerritoryID] ,[BillToAddressID] ,[ShipToAddressID] ,[ShipMethodID] ,[CreditCardID] ,[CreditCardApprovalCode] ,[CurrencyRateID] ,[SubTotal] ,[TaxAmt] ,[Freight] ,[TotalDue] ,[Comment] ,[rowguid] ,[ModifiedDate] FROM [SalesOrderHeader_TEST] WHERE SalesOrderID = 75123 INSERT INTO [AdventureWorks2008].[dbo].[SalesOrderDetail_TEST] ([SalesOrderID] ,[CarrierTrackingNumber] ,[OrderQty] ,[ProductID] ,[SpecialOfferID] ,[UnitPrice] ,[UnitPriceDiscount] ,[LineTotal] ,[rowguid] ,[ModifiedDate]) SELECT 75123 + @i ,[CarrierTrackingNumber] ,[OrderQty] ,[ProductID] ,[SpecialOfferID] ,[UnitPrice] ,[UnitPriceDiscount] ,[LineTotal] ,[rowguid] ,GETDATE() FROM Sales.SalesOrderDetail SET @i = @i + 1 END GO
