1. 程式人生 > 其它 >【轉】ASP.NET站點效能提升-加速SqlServer資料庫訪問 ASP.NET站點效能提升-加速資料庫訪問

【轉】ASP.NET站點效能提升-加速SqlServer資料庫訪問 ASP.NET站點效能提升-加速資料庫訪問

轉自:http://www.cnblogs.com/ntwo/archive/2010/12/15/1907040.html

ASP.NET站點效能提升-加速資料庫訪問

SQL Server本身就是個很大的題目。這裡不會涉及到SQL Server資料庫訪問的方方面面,而是重點關注於可能獲得最大效能提升的領域。

查明瓶頸

缺少索引和昂貴查詢

可以通過減少查詢執行的讀操作極大地提高查詢效能。執行的讀操作越多,對磁碟、CPU和記憶體的壓力就可能越大。第二,進行讀操作的查詢可能阻塞其它進行更新的查詢。如果更新查詢在持有鎖時必須進行等待,它可能會延遲一系列其它查詢。最後,除非整個資料庫都在記憶體中,每次從磁碟上讀取資料,都需要從記憶體中刪除其它資料。如果被刪除的資料後來被用到,需要重新從磁碟上讀取。

減少讀操作最有效的方法是在表上建立有效索引。SQL Server索引允許查詢不需要掃描整個表,而只讀取需要的部分。但是,索引會有額外的開銷並減慢更新操作,所以必須小心使用。

缺少索引

SQL Server允許在表字段上加索引,提高對這些欄位進行操作的WHERE和JOIN語句的速度。當查詢優化器優化查詢時,會儲存似乎應該有但沒有的索引的資訊。可能使用Dynamic Management View(DVM)訪問這些資訊:

select d.name AS DatabaseName, mid.*
from sys.dm_db_missing_index_details mid
join sys.databases d ON mid.database_id=d.database_id

這個查詢返回的最重要列是:

描述
DatabaseName 這一行屬於的資料庫
equality_columns 具有等於操作符的逗號分割的列的列表,例如:
column=value
inequality_columns 具有比較操作符的逗號分割的列的列表,例如:
column>value
included_columns 如果包括在索引中可能會有收益的逗號分割的列的列表
statement 缺失索引的表名

這些資訊會在重啟伺服器後清空。

另一個方法是使用SQL Server 2008包含的Data Engine Tuning Advisor。這個工具會分析資料操作跟蹤資料,根據所有查詢識別出最佳的索引集。它甚至給出了建立識別出的缺失索引的SQL語句。

第一步是得到一段時間內的資料庫操作的跟蹤資料。在資料庫最繁忙的時間段內,開啟跟蹤:

  1. 開啟SQL Profiler。選擇Start | Programs | Mircrosoft SQL Server 2008 | Performance Tools | SQL Server Profiler。
  2. 在SQL Profiler中,選擇File | New Trace。
  3. 選擇Events Selection選項頁。
  4. 只保留SQL:BatchCompleted和RPC:Completed事件。確保選擇了事件的TextData列。
  5. 單擊Column Filters按鈕。選擇Database Name列,展開Like,輸入需要監控的資料庫名稱。
  6. 選擇ApplicationName,過濾需要監控的程式。
  7. 單擊Runt按鍵,監控結束後,儲存到檔案。
  8. 儲存為模板,下次不用再建立。選擇File | Save As | Trace Template。下次建立新跟蹤時,可能從Use the template下拉框選擇模板。
    傳送這些事件到螢幕會佔用很多伺服器資源。解決方法是儲存跟蹤為指令碼,然後使用指令碼進行後臺跟蹤。
  9. 選擇File | Export | Script Trace Definition | For SQL Server 2005-2008。現在可以關閉SQL Server Profiler,這會關閉跟蹤。
  10. 在SQL Server Management Studio中,開啟剛才建立的.sql檔案。搜尋字串“InsertFileNameHere”,替換成你想要日誌儲存的檔案的完整路徑。儲存。
  11. 開始跟蹤,F5執行.sql檔案。它會顯示跟蹤的trace ID。
  12. 檢視系統中的跟蹤狀態,在查詢視窗執行命令:
    select * from ::fn_trace_getinfo(default)

    查詢執行的trace ID行中property列為5的行。如果這行的value列值是1,跟蹤正在執行。trace ID為1的跟蹤的系統跟蹤。
  13. 跟蹤一段時間後,假設trace ID是2,執行以下命令停止跟蹤:
    exec sp_trace_setstatus 2,0
    重啟跟蹤,執行:
    exec sp_trace_setstatus 2,1
  14. 停止並關閉跟蹤,這樣才能訪問跟蹤檔案,執行:
    exec sp_trace_setstatus 2,0
    exec sp_trace_setstatus 2,2

執行Database Engine Tuning Advisor:

  1. 選擇Start | Programs | Microsoft SQL Server 2008 | Performance Tools | Database Engine Tuning Advisor。

  2. 在Workload區域,選擇trace檔案。在Database for workload analysis下拉框,選擇需要分析的第一個資料庫。

  3. 在Select databases and table to tune,選擇需要索引建議的資料庫。

  4. 如果跟蹤檔案很大,Database Engine Tuning Advisor會花費很長時間進行分析。在Tuning Options選項頁,可以選擇何時停止分析。

  5. 點選Start Analysis按鍵開始分析。

 

注意Database Engine Tuning Advisor只是個程式。可以考慮這些建議,但自己做決定。確保在典型時段進行跟蹤,否則這些建議可能會使事情更糟。例如,如果提供晚上抓取的跟蹤檔案,那時只處理少量事務,但生成大量報表,那麼這些建議就會去優化報表而不是事務。

昂貴查詢

如果使用SQL Server 2008或更高版本,可以使用活動監視器查詢最近執行的昂貴查詢。在SSMS中,右擊資料庫伺服器,選擇Activity Monitor。

可以通過使用DMV dm_exec_query_stats可以獲取更多的資訊。當查詢優化器為查詢建立執行計劃時,它會快取計劃進行重用。每次使用查詢計劃執行查詢時,效能統計會被保留。可以使用dm_exec_query_stats檢視這些統計。

SELECT
  est.text AS batchtext,
  SUBSTRING(est.text, (eqs.statement_start_offset/2)+1,  
    (CASE eqs.statement_end_offset WHEN -1 
    THEN DATALENGTH(est.text) 
    ELSE eqs.statement_end_offset END - 
    ((eqs.statement_start_offset/2) + 1))) AS querytext,
  eqs.creation_time, eqs.last_execution_time, eqs.execution_count, 
  eqs.total_worker_time, eqs.last_worker_time, 
  eqs.min_worker_time, eqs.max_worker_time, 
  eqs.total_physical_reads, eqs.last_physical_reads, 
  eqs.min_physical_reads, eqs.max_physical_reads, 
  eqs.total_elapsed_time, eqs.last_elapsed_time, 
  eqs.min_elapsed_time, eqs.max_elapsed_time, 
  eqs.total_logical_writes, eqs.last_logical_writes, 
  eqs.min_logical_writes, eqs.max_logical_writes,
  eqs.query_plan_hash 
FROM
  sys.dm_exec_query_stats AS eqs
  CROSS APPLY sys.dm_exec_sql_text(eqs.sql_handle) AS est
ORDER BY eqs.total_physical_reads DESC

DMV有一個限制:當執行它時,自從上次伺服器重啟後執行的查詢在快取不是都有查詢計劃。一些計劃因為使用次數少會過期。那些生成開銷很小,但執行開銷又不夠小的計劃,根本就不會儲存。如果計劃被重新編譯了,那統計資料是從上次重新編譯開始的。

另一個限制是查詢只適用於儲存過程。如果使用臨時查詢,引數是嵌入在查詢中的。這會導致查詢優化器為每一組引數生成一個計劃,除非查詢已經是引數化的。這會在查詢計劃重用部分進一步討論。

為了解決這個問題,dm_exec_query_stats返回query_plan_hash列,如果查詢的執行計劃是相同的,這列的值就是相同的。通過使用GROUP BY聚合這一列,可以得到使用相同邏輯的查詢的總效能資料。

這個查詢返回下資訊:

描述
batchtext Text of the entire batch or stored procedure containing the query.
querytext Text of the actual query.
creation_time Time that the execution plan was created.
last_execution_time Last time the plan was executed.
execution_count Number of times the plan was executed after it was created. This is not the number of times the query itself was executed; its plan may have been recompiled at some statge.
total_worker_time Total amount of CPU time in microseconds that was consumed by executions of this plan since it was created.
last_worker_time CPU time in microseconds that was consumed the last time the plan was executed.
min_worker_time Minimum CPU time in microseconds that this plan has ever consumed during a single execution.
max_worker_time Maximum CPU time in microseconds that this plan has ever consumed during a single execution.
total_physical_reads Total number of physical reads performed by executions of this plan since it was compiled.
last_physical_reads Number of physical reads performed the last time the plan was executed.
min_physical_reads Minimum number of physical reads that this plan has ever performed during a single execution.
max_physical_reads Maximum number of physical reads that this plan has ever performed during a single execution.
total_logical_writes Total number of logical writes performed by executions of this plan since it was compiled.
last_logical_writes Number of logical writes performed the last time the plan was executed.
min_logical_writes Minimum number of logical writes that this plan has ever performed during a single execution.
max_logical_writes Maximum number of logical writes that this plan has ever performed during a single execution.
total_elapsed_time Total elapsed time in microseconds for completed executions of this plan.
last_elapsed_time Elapsed time in microseconds for the most recently completed execution of this plan.
min_elapsed_time Minimum elapsed time in microseconds for any completed execution of this plan.
max_elapsed_time Maximum elapsed time in microseconds for any completed execution of this plan.

另一種方法是分析SQL Server Profiler生成的跟蹤檔案。

為了更好的分析,可以將跟蹤檔案儲存為表格:

  1. 開啟SQL Profiler。
  2. 開啟跟蹤檔案:File | Open | Trace File。
  3. 儲存跟蹤為表格:File | Save As | Trace Table。

還可以使用fn_trace_gettable:

SELECT * INTO newtracetable FROM ::fn_trace_gettable('c:\trace.trc', default)

找到最昂貴查詢或儲存過程的最容易的方法是使用GROUP BY,根據查詢或儲存過程聚合效能資料。但是,如果查看錶中的TextData列,就會發現所有的查詢或儲存過程呼叫都包括引數值。如果想要聚合它們,必須過濾掉這些引數。

如果呼叫的是儲存過程,刪除引數還不是很難,因為它們總是是儲存過程名後面。

如果呼叫的是臨時查詢,刪除引數就比較困難了,因為它們在每個查詢中的位置都是不一樣的。有一些工具可以是工作變得容易些:

一旦定位了最昂貴的查詢,就可以判斷新增索引是否可以加速執行:

  1. 在SMMS中開啟一個查詢視窗。
  2. 在“Query”選單,選擇“Include Actual Execution Plan”或者使用快捷鍵Ctrl+M。
  3. 複製昂貴查詢到查詢視窗並執行,開啟“Execution plan”選項頁。
  4. 如果查詢優化器發現缺失索引,就會顯示綠色的訊息。
  5. 查詢更多資訊,可以右鍵單擊執行計劃視窗,選擇“顯示查詢計劃XML”。在XML中,查詢MissingIndexes元素。

如果發現是缺失索引,參考修復瓶頸節的缺失索引子節。

如果發現昂貴查詢,參考修復瓶頸節的昂貴查詢子節。

未使用索引

索引的一個缺點是當資料更新時,它們也需要更新,這就導致了延遲。它們也會佔用磁碟空間。如果一個索引拖慢更新,並且幾乎不使用,最好刪除它。

使用DMV dm_db_index_usage_stats可以獲得每個索引的使用資訊:

SELECT d.name, t.name, i.name, ius.*
FROM sys.dm_db_index_usage_stats ius
JOIN sys.databases d ON d.database_id = ius.database_id
JOIN sys.tables t ON t.object_id = ius.object_id
JOIN sys.indexes i ON i.object_id = ius.object_id AND i.index_id = 
ius.index_id 
ORDER BY user_updates DESC

 

這個查詢會顯示每個上次伺服器啟動後有活動的索引的名稱、表和資料庫,和自從上次伺服器啟動後更新和讀的數量。

user_updates列,顯示由INSERT、UPDATE和DELETE操作引起的更新的數量。如果這個數字相對於讀的數量很高,考慮刪除這個索引:

DROP INDEX IX_TITLE ON dbo.Book

如果資料庫有很多查詢在同時執行,一些查詢會訪問相同的資源,例如一張表或索引。在一個查詢更新資源時,另一個查詢是不能讀的;否則會導致不一致的結果。

為了阻止查詢訪問資源,SQL Server會鎖資源。等待鎖釋放的查詢會有一些延遲。如果想要確定這些延遲是否過度,在資料庫伺服器上使用perfmon檢查以下計數器:

分類:SQL Server:Latches

Total Latch Wait Time (ms): Total wait time in milliseconds for latches in the last second.
Lock Timeouts/sec: Number of lock requests per second that timed out. This includes requests for NOWAIT locks.
Lock Wait Time (ms): Total wait time in milliseconds for locks in the last second.
Number of Deadlocks/sec: Number of lock requests per second that resulted in a deadlock.

如果Total Latch Wait Time (ms)的數值很高,表示SQL Server使用它自己的同步機制等待的時間很長。通常情況下,Lock Timeouts/sec應該為0,Lock Wait Time (ms)應該很低。如果不是,查詢等待鎖釋放的時間太長了。

最後,Number of Deadlocks/sec應該為0。否則,存在查詢互相等待對方釋放鎖,阻止它們訪問資源。SQL Server最後會檢測到這個情況,通過回滾其中一個查詢,但是這會浪費時間和已經完成的工作。

如果發現鎖的問題,參考修復瓶頸節的鎖子節,查找出哪些查詢導致過長的鎖等待時間。

執行計劃重用

在執行查詢時,SQL Server查詢優化器會編譯一個成本最低的查詢計劃。這會使用很多CPU週期,所以,SQL Server會在記憶體中快取查詢計劃。當接收查詢時,會試圖與快取的查詢計劃進行匹配。

效能計數器

分類:Processor(_Total)

% Processor Time: The percentage of elapsed time that the processor is busy.
型別:SQL Server: SQL Statistics

SQL Compilations/sec: Number of batch compiles and statement compiles per second. Expected to be very high initially after server startup.
SQL Re-Compilations/sec: Number of recompiles per second.

這些計數器在伺服器剛啟動時,會顯示很高的數值,因為每一個收到的查詢都需要編譯。執行計劃快取是在記憶體中的,所以每次重啟都需要重新編譯。在正常情況下,每秒編譯次數應該小於100,重新編譯次數應該接近0。

dm_exec_query_optimizer_info

另一種方法是檢視伺服器優化查詢花費的時間。因為查詢優化是CPU密集型操作,所以幾乎所有的CPU時間都花費在這上面了。

Dynamic Management View (DMV) sys.dm_exec_query_optimizer_info顯示上次伺服器重啟後查詢優化次數和平均時間(單位秒)。

SELECT 
  occurrence AS [Query optimizations since server restart],
  value AS [Avg time per optimization in seconds],
  occurrence * value AS [Time spend optimizing since server  
    restart in seconds]
FROM sys.dm_exec_query_optimizer_info
WHERE counter='elapsed time'

執行這個查詢,等待一段時間,再運行了一次。這樣就能得到這段時間優化查詢的時間。

sys.dm_exec_cached_plans

DMV sys.dm_exec_cached_plans提供計劃快取中所有執行計劃的資訊。可能與DMV sys.dm_exec_sql_text組合使用找出給定查詢的查詢計劃的重用頻率。如果一個查詢或儲存過程的執行計劃的重用比率很低,那麼從計劃快取中可以得到的好處也是很有限的。

SELECT ecp.objtype, ecp.usecounts, ecp.size_in_bytes, 
  REPLACE(REPLACE(est.text, char(13), ''), char(10), ' ') AS querytext
FROM sys.dm_exec_cached_plans ecp
cross apply sys.dm_exec_sql_text(ecp.plan_handle) est
WHERE cacheobjtype='Compiled Plan'

objtype列的值是Proc表示是儲存過程,Adhoc表示是臨時查詢,usecounts顯示計劃的使用次數。

碎片

資料庫中資料和索引在磁碟中是以8KB頁的大小組織的。頁是SQL Server與磁碟中交換資料的最小單位。

當插入或更新資料時,一個頁的空間可能不夠了,SQL Server建立一個新頁,將原來頁上的一半內容移動到新頁上。這使新頁和老頁上都留下了空閒空間。這樣,如果在老頁上不停地插入和更新資料,就不會持續得分割資料。

這樣,在很多次更新、插入和刪除資料後,就會有很多半滿的頁。這會佔用更多的磁碟空間,更重要的是會拖慢資料讀取。這些頁和物理順序與SQL Server需要讀取的邏輯順序也可能不一樣。結果,SQL Server需要等待磁碟頭到達下一頁,而不是順序讀取,這樣,就會有更多的延遲。

使用dm_db_index_physical_stats DMV查詢表和索引的碎片程度:

DECLARE @DatabaseName sysname
SET @DatabaseName = 'mydatabase' --use your own database name
SELECT o.name AS TableName, i.name AS IndexName, ips.index_type_desc,
  ips.avg_fragmentation_in_percent, ips.page_count, ips.fragment_count, 
  ips.avg_page_space_used_in_percent
FROM sys.dm_db_index_physical_stats(
  DB_ID(@DatabaseName),
  NULL, NULL, NULL, 'Sampled') ips
JOIN sys.objects o ON ips.object_id = o.object_id
JOIN sys.indexes i ON (ips.object_id = i.object_id) AND (ips.index_id = i.index_id)
WHERE (ips.page_count >= 7) AND (ips.avg_fragmentation_in_percent > 20)
ORDER BY o.name, i.name

這會統計所有使用超過7個頁並且這些頁的碎片超過20%的表和索引。

如果索引型別是CLUSTERED INDEX,實際上指的是表,因為表是聚集索引的一部分。索引型別HEAP指的是沒有聚集索引的表。

記憶體

在perfmon中使用以下資料器查詢是否記憶體不中拖慢資料庫伺服器:

分類:Memory

Pages/sec: When the server runs out of memory, it stores information temporarily on disk, and then later reads it back when needed, which is very expensive. This counter indicates how often this happens.

分類:SQL Server: Buffer Manager

Page Life Expectancy: Number of seconds a page will stay in the buffer pool without being used. The greater the life expenctancy, the greater the change that SQL Server will be able to get a page from memory instead of having to read it from disk.
Buffer cache hit ratio: Percentage of pages that were found in the buffer pool, without having to read from disk.

如果Pages/sec一直很高或者Page Life Expectancy一直很低,低於300,或Buffer cache hit ratio一直很低,低於90%,SQL Server可能沒有足夠的記憶體。這會導致過度的磁碟I/O,造成更大的CPU和磁碟壓力。

磁碟

如果在上一節發現記憶體問題,首先修復它,因為記憶體不足會導致更多的磁碟使用。否則,檢查以下計數器:

分類:PhysicalDisk and LogicalDisk

% Disk Time: Percentage of elapsed time that the selected disk was busy reading or writing.
Avg. Disk Queue Length: Average number of read and write requests queued during the sample interval.
Current Disk Queue Length: Current number of request queued.

如果Disk Time持續保持在85%以上,磁碟系統的壓力就比較大了。

Avg. Disk Queue Length和Current Disk Queue Length指磁碟控制器排隊的任務數和正在處理的任務數。正常的數字應該是2以下。如果使用了磁碟陣列,控制器被附加到多個磁碟,計數器值是磁碟數量的兩倍或更少。

CPU

如果發現記憶體或磁碟問題,先解決它們,因為它們會增加磁碟的壓力。CPU計數器:

分類:Processor

% Processor Time: Proportion of time that the processor is busy.

分類:System

Processor Queue Length: Number of threads waiting to be processed.

如果% Processor Time持續高於75%,或Processor Queue Length持續高於2,CPU可能壓力過大。

修復瓶頸

缺失索引

聚集索引

考察這張表:

CREATE TABLE [dbo].[Book](
  [BookId] [int] IDENTITY(1,1) NOT NULL,
  [Title] [nvarchar](50) NULL,
  [Author] [nvarchar](50) NULL,
  [Price] [decimal](4, 2) NULL)

 

因為這張表沒有聚集索引,所以稱為堆表。它的記錄是無序的。如果需要查詢標題包含某一關鍵字的所有書籍,必須讀取所有的記錄。這張表的結構非常簡單:

我們可以測試在這張表中定位一條記錄需要多少時間,然後與有索引的表進行對比。

告訴SQL Server顯示I/O和計算查詢的時間:

SET STATISTICS IO ON
SET STATISTICS TIME ON

清空記憶體快取:

CHECKPOINT
DBCC DROPCLEANBUFFERS

在有一百萬條記錄的表中執行查詢:

SELECT Title, Author, Price FROM dbo.Book WHERE BookId = 5000

測試機器上的結果是:9564, CPU time: 109 ms, elapsed time: 808 ms。

SQL Server使用8KB的頁儲存所有資料。結果顯示讀取了9564個頁,也就是整張表。

現在,加入聚集索引:

ALTER TABLE Book ADD CONSTRAINT [PK_Book] PRIMARY KEY CLUSTERED ([BookId] ASC)

這會在列BookId上建立一個索引,使得BookId上的WHERE和JOIN語句更快。索引會根據BookId排序表,並增加一個稱為B-樹的結構加速訪問:

現在,運行同樣的查詢,結果是:

reads: 2, CPU time: 0 ms, elapsed time: 32 ms。

非聚集索引

現在,我們使用Title替代BookId進行查詢:

SELECT Title, Author FROM dbo.Book WHERE Title = 'Don Quixote'

結果是:reads: 9146, CPU time: 156 ms, elapsed time: 1653 ms。

這和堆表的結果差不多。

解決方法是在Title列中建立索引。然而,因為聚集索引會使得表也按照索引欄位排序,所以只能有一個聚集索引。因為已經在BookId上建立了聚集索引,所以只能建立非聚集索引。

非聚集索引建立了表記錄的備份,這次是按照Title排序的。為了節省空間,SQL Server排除了聚集索引欄位以外的其它列。一張表上可以建立249個非聚集索引。

因為我們需要在查詢中訪問其它欄位,我們需要聚集索引可以連結到表記錄。方法是在非聚集索引記錄中加入BookId。因為BookId有聚集索引,一旦通過非聚集索引找到BookId,就可以使用聚集索引得到真正的表記錄。這個方法中的第二步驟稱作鍵查詢。

為什麼要通過聚集索引,而不在非聚集索引中使用表記錄的實體地址?因為當更新表記錄時,記錄可能會變大,SQL Server需要移動後面的記錄騰出空間。如果非聚集索引包括實體地址,每次移動記錄時,都需要更新地址。在更慢的更新和更慢的讀之間有一個平衡。如果沒有聚集索引或聚集索引沒有唯一約束,非聚集索引記錄就包括實體地址。

檢視非聚集索引的效果,首先建立它:

CREATE NONCLUSTERED INDEX [IX_Title] ON [dbo].[Book]([Title] ASC)

 

執行查詢:

SELECT Title, Author FROM dbo.Book WHERE Title = 'Don Quixote'

結果是: reads: 4, CPU time: 0 ms, elapsed time: 46 ms。

包含列

再一次檢查測試查詢,它只是返回Title和Author。Title已經在非聚集索引記錄中了。如果在索引中加入Author,就不需要等待SQL Server訪問表記錄了,跳過了鍵查詢步驟。

可以通過在非聚集索引中包括Author:

CREATE NONCLUSTERED INDEX [IX_Title] ON [dbo].[Book]([Title] ASC) INCLUDE(Author) WITH drop_existing

 

現在再執行查詢:

SELECT Title, Author FROM dbo.Book WHERE Title = 'Don Quixote'

結果:reads: 2, CPU time: 0 ms, elapsed time: 26 ms。

讀從4降到了2,使用時間從46ms降到了26ms,有50%的提升。從絕對值來看,提升不多,但如果查詢執行非常頻繁,這樣做還是很有意義的。但也不做過了頭,聚集索引記錄越大,8KB頁上儲存的記錄就越少,SQL Server就需要讀更多的頁。

選擇合適的列建立索引

因為建立和維護索引都需要開銷,所以必須選擇合適的列建立索引。

  • 在列上建立主鍵,預設會在這些列上建立聚集索引。
  • 在一列上建立索引,會影響使用這張表的所有查詢。所以不要只關注一個查詢。
  • 在資料庫上建立索引前,必須先做測試,確定這樣做真會改善效能。

何時使用索引

當選擇建立索引時,可以使用以下決策過程:

  • 找出最昂貴查詢。可以看到Database Engine Tuning Advisor生成的索引建議。
  • 在每個JOIN的至少一列上建立一個索引。
  • 考慮ORDER BY和GROUP BY子句中使用的列。
  • 考慮使用WHERE子句中的列,特別是如果WHERE選擇的記錄數較少。但是,需要注意:
    • 使用函式的WHERE子句不能使用索引,因為函式輸出不在索引中。例如:
      SELECT Title, Author FROM dbo.Book WHERE LEFT(Title, 3) = 'Don'
      在Title列中放置索引不會使這個查詢更快。
    • 如果在WHERE子句中使用查詢字串開關是萬用字元的LIKE語句,SQL Server不會使用索引:
      SELECT Title, Author FROM dbo.Book WHERE Title LIKE '%Quixote'
      但是如果查詢字串是以文字常量開頭的,能夠使用索引:
      SELECT Title, Author FROM dbo.Book WHERE Title LIKE 'Don%'
  • 考慮使用有唯一約束的列。這會有助於檢查新值是否是唯一的。
  • MIN和MAX函式可以從索引中獲益,因為值是排序的,就不需要查詢整張表確定最大或最小值。
  • 使用佔用很多空間的欄位建立索引要三思。如果是非聚集索引,列值會在索引中重複儲存。如果是聚集索引,列值會在所有非聚集索引中重複儲存。這會增加索引記錄的大小,這樣,在8KB頁上只能存放更少的索引記錄,這會使得SQL Server讀取更多的頁。

何時不使用索引

實際上建立過多的索引會降低效能,不在列上放置索引的主要原因:

  • 列經常更新。
  • 列低特殊性,也就是列上的值有很多重複。

列經常更新

當更新沒有索引的列時,如果沒有頁分割,SQL Server需要向磁碟寫入一個8KB的頁。

但是,如果列上有一個非聚集索引,或者包含上非聚集索引中,SQL Server也需要更新索引。所以至少需要寫入至少一個額外頁。它還需要更新索引使用的B-樹結構,潛在地需要更新更多的頁。

如果更新了聚集索引的列,使用了舊值的非聚集索引記錄也需要更新,因為非聚集索引中使用聚集索引鍵,導航到真正的資料庫記錄。第二,資料庫記錄也是根據聚集索引排序的,如果更新導致排序順序改變了,就需要更多的寫。最後,聚集索引需要B-樹。

低特殊性

即使列上有索引,查詢優化器也不是使用它。每一次SQL Server通過索引訪問一條記錄,必須使用索引結構。在非聚集索引中,可能還需要進行鍵查詢。例如,如果選擇所有價格為20元的書,恰好也有很多書是這個價格,有可能簡單地讀取所有書記錄反而更快一點。在這種情況下,20元價格就是低特殊性。

可以使用一個簡單的查詢計算一列中的值的平均選擇性。例如,計算Book表中的Price列的平均選擇性:

SELECT
  COUNT(DISTINCT Price) AS 'Unique prices',
  COUNT(*) AS 'Number of rows',
  CAST((100 * COUNT(DISTINCT Price) / CAST(COUNT(*) AS REAL)) 
    AS nvarchar(10)) + '%'  AS 'Selectivity'
FROM Book

 

如果每本書都有不同的價格,選擇性就是100%。如果選擇性低於85%,索引增加開銷會比節省的開銷更大。

有一些價格會比其它價格出現的次數更多。檢視每一個價格的選擇性,執行:

DECLARE @c real
SELECT @c = CAST(COUNT(*) AS real) FROM Book
SELECT 
  Price, 
  COUNT(BookId) AS 'Number of rows',
  CAST((1 - (100 * COUNT(BookId) / @c)) 
    AS nvarchar(20)) + '%'  AS 'Selectivity'
FROM Book
GROUP BY Price
ORDER BY COUNT(BookId)

查詢優化器不太可能使用選擇性低於85%的索引。

何時使用聚集索引

聚集索引和非聚集索引特性的比較:

特性 聚集索引與非聚集索引對比
更快。因為不需要鍵查詢。如果需要的列包含中非聚集索引中,沒有區別。
更新 更慢。不僅是表記錄,所有非聚集索引也需要更新。
插入/刪除 更快。對於非聚集索引,在表中插入新記錄意味著在非聚集索引中也要插入新記錄。對於聚集索引,表就是索引的一部分,所以不需要二次插入。刪除記錄也是一樣的。
另一方面,當記錄不是插入表的非常後部,插入可能導致頁分割,這樣頁的一半內容就需要轉移到另一個頁上。非聚集索引上的頁分割可能性更低,因為它們的記錄更小。
當記錄插入到表的後部,不需要進行頁分割。
列大小 需要保持短小和快速。因為每一個非聚集索引都包含聚集索引值,進行鍵查詢。使用int型別要比使用nvarchar(50)要好得多。

如果多個列需要索引,最好將聚集索引改在主鍵上:

  • 讀:主鍵會包含在很多JOIN子句中,使得讀效能很重要。
  • 更新:主鍵不應該或很少更新,否則就需要更新外來鍵。
  • 插入/刪除:大多數情況上,會將主鍵設為標識列,這樣,每條記錄就分配了一個唯一的,自增長的數字。這樣,如果在主鍵上建立聚集索引,新記錄始終加到表的結尾。當記錄加到有聚集索引的表結尾,並且當前頁沒有空間時,新記錄儲存到新頁上,當前頁的資料依然儲存在當前頁上。這樣,就避免了昂貴的頁分割。
  • 大小:大多數情況下,主鍵是int型別。它是短而快的。

實際上,如果在SSMS表設計器中設定一列為主鍵,SSMS預設設定這列為聚集索引,除非其它列已經設定了聚集索引。

維護索引

以下方法可以保持索引效率:

  • 索引碎片整理。不斷地更新導致索引和表碎片增多,降低了效能。測量碎片程式,參考查明瓶頸節的碎片子節。
  • 保持統計更新。SQL Server維護統計資料以決定針對一個查詢是否使用索引。這些統計資料一般情況是自動更新的,但這個功能可以關閉。如果關閉了,確保統計資料是最新的。
  • 刪除未使用的索引。索引加速讀訪問,但減慢了更新。辨別未使用的索引,參考查明瓶頸節的缺失索引和昂貴查詢。

昂貴查詢

快取聚集查詢

聚集語句,例如COUNT和AVG是很昂貴的,因為它們需要訪問很多記錄。如果一個網頁需要聚集資料,考慮在一個表中快取聚集結果,而不是每一次頁面請求都重新查詢一次。例如,以下程式碼在Aggreates表中儲存一個COUNT聚集資料:

DECLARE @n int
SELECT @n = COUNT(*) FROM dbo.Book
UPDATE Aggregates SET BookCount = @n

當底層資料改變時,可以使用觸發器或儲存過程更新聚集資料。也可以使用SQL Server作業重新計算聚集結果。建立作業,參考:How to: Create a Transact-SQL Job Step (SQL Server Management Studio)  http://msdn.microsoft.com/en-us/library/ms187910.aspx

保持記錄短小

減少表記錄佔用的空間可以加速訪問。記錄在磁碟上儲存在8KB的頁中。一個頁中儲存的記錄越多,SQL Server獲取給定結果集需要讀取的頁就越少。

保持記錄短小的方法:

  • 使用短資料型別。如果值能放在1個位元組的TinyInt中,不要使用四個位元組的Int。如果只是儲存ASCII字元,使用varchar(n),每個字元使用一個位元組,不要使用nvarchar(n),使用兩個位元組儲存一個字元。如果儲存固定長度的字串,使用char(n)或nchar(n),不要使用varchar(n)或nvarchar(n),節省兩個位元組的長度空間。
  • 考慮使用行外儲存大的,很少使用的列。大物件欄位,例如nvarchar(max),varchar(max),varbinary(max),和XML欄位如果小於8000位元組通常存在行中,如果大於8000位元組就會中行中儲存一個2位元組的指標,指向行外區域。在行外儲存意味著訪問這個欄位至少需要讀取兩次,而不是一次。對於小得多的記錄,如果這個列很少訪問,也可以將它存放到行外。強制表中的大物件始終儲存在行外:使用
    EXEC sp_tableoption 'mytable', 'large value types out of row', '1'
  • 考慮垂直分割槽。如果表中一些列訪問的比其它列頻繁地多,把很少使用的列存放在另一張表中。訪問頻繁訪問的列會更快,代價是當訪問不經常訪問的列時,需要使用JOIN。
  • 避免重複列。例如,不要這樣做:

    這個方案不僅建立了長記錄,也使得更新書名很困難,一個作者也不能有多於兩本的書。將書名儲存在一張單獨的book表中,其中包括AuthorId列人心如向書的作者。
  • 避免重複值。例如,不要這樣做:

    使用者名稱和國家重複了。除了長記錄,更新作者資訊需要更新多個記錄,並且增加了不一致的風險。在單獨的表中儲存使用作者和書籍,在書記錄中儲存作者的主鍵。

考慮反規範化

反規範化是上節兩個觀點的對立—避免重複列和重複值。

問題是這些建議提升了更新速度,一致性和記錄大小,但使得資料分散在不同的表中,這意味著更多的JOIN。

例如,假設有100個地址分散在50個地址中,城市儲存在一個單獨的表中。這使得地址記錄更短,並且更新城市名稱更容易,但這也意味著每一次獲取地址都需要JOIN。如果城市名稱不太可能改變,並且獲取城市時,都會獲取地址的其它部分。那麼,在地址記錄中包括城市名稱會更好。這個解決方案包含了重複內容(城市名稱),但是,會少一次JOIN。

小心觸發器

觸發器是非常方便的。但它們隱藏在開發者的視野外,所以開發者可能沒有意識到觸發器的額外開銷。

保持觸發器的短小。它們執行中觸發它們的事務中。所以當觸發器執行時,持有鎖的那個事務會一直持有鎖。注意,即使沒有使用BIGIN TRAN顯式建立事務,每一個INSERT,UPDATE,或DELETE在操作期間建立它們自己的事務。

在決定使用哪些索引時,不要忘記和儲存過程和函式一樣,檢視觸發器。

對小而且臨時的結果集使用表變數

考慮在儲存過程中使用表變數代替臨時表。例如,不要這樣寫:

CREATE TABLE #temp (Id INT, Name nvarchar(100))
INSERT INTO #temp
...

可以這樣寫:

DECLARE @temp TABLE(Id INT, Name nvarchar(100))
INSERT INTO @temp
...

相對於臨時表,表變數有這些好處:

  • SQL Server更可能把它們儲存在記憶體中,而不是tempdb中。這意味著更少的通訊量和對tempdb的鎖。
  • 沒有事務日誌開銷。
  • 更少地儲存過程重編譯。

然而,它們也有缺點:

  • 在表變數建立後,不能加索引或約束。如果需要加索引,必須作為DECLARE語句的一部分:
    DECLARE @temp TABLE(Id INT primary key, Name nvarchar(100))
  • 當超過100條記錄後,表變數的效率要比臨時表低,因為不會為表變數建立統計資訊。使得查詢優化器建立優化的執行計劃更困難。

使用全文搜尋代替LIKE

你可能使用LIKE在文字列中搜索子串:

SELECT Title, Author FROM dbo.Book WHERE Title LIKE '%Quixote'

但是,除非查詢字串以常量文字開關,SQL Server不能使用列上的索引,就需要做全表掃描。

考慮使用SQL Server全文搜尋。這會為文字列中的所有單詞建立一個索引,這樣搜尋就會更快。使用全文搜尋,參考:

使用基於集合的程式碼代替遊標

考慮使用基於集合的程式碼代替遊標,這樣效能提高1000倍也是很常見的。基於集合的程式碼使用的內部演算法相比遊標,被極大的優化了。

更多資訊,訪問:

最小化SQL伺服器到Web伺服器的流量

不要使用SELECT *。這會返回所有的行。只返回需要的列。

如果網站只需要長文字的一部分,只發送這部分。例如:

SELECT LEFT(longtext, 100) AS excerpt FROM Articles WHERE ...

物件命名

儲存過程名不要以sp_開頭。SQL Server假設以sp_開頭的是系統儲存過程,即使以應用資料庫開頭,也會在master資料庫中查詢這些儲存過程。

物件名應該以schema所有都開頭。這樣會節省SQL Server辨別物件的時間,提高執行計劃重新性。例如:

SELECT Title, Author FROM dbo.Book

而不要使用

SELECT Title, Author FROM Book

使用SET NOCOUNT ON

在儲存過程和觸發器的開發加入命令SET NOCOUNT ON。會這禁止SQL Server在每個SQL語句後傳送影響的行數。

對超過1M的值使用FILESTREAM

在FILESTRAM列中儲存超過1M的BLOB型別的值。這會直接使用NTFS檔案系統儲存物件,而不使用資料庫資料檔案。實現方法:

WHERE子句中的列避免使用函式

WHERE子句中的列使用函式會使得SQL Server不使用這個列上的索引。

例如,下面的查詢:

SELECT Title, Author FROM dbo.Book WHERE LEFT(Title, 1)='D'

SQL Server不知道LEFT函式返回的值,所以只能掃描整張表,對Title列的每一個值執行LEFT函式。

但是,它知道如何處理LIKE。重寫查詢:

SELECT Title, Author FROM dbo.Book WHERE Title LIKE 'D%'

SQL Server現在可以使用Title上的索引,因為LIKE字串以文字常量開頭。

使用UNION ALL替代UNION

UNION子句合併兩個SELECT語句的結果集,從最終結果集中去除重複資料。這個操作很昂貴,它使用一張工作表,執行DISTINCT選擇實現這個功能。

如果不介意重複,或者知道不會有重複,使用UNION ALL。

如果優化器檢查到不會有重複,它會選擇UNION ALL,即使使用了UNION。例如,下面的語句永遠不會有重複的記錄,優化器會使用UNION替代UNION ALL:

SELECT BookId, Title, Author FROM dbo.Book WHERE Author LIKE 'J%'
UNION
SELECT BookId, Title, Author FROM dbo.Book WHERE Author LIKE 'M%'

使用EXISTS替代COUNT查詢重複記錄

如果需要檢查結果集中是否有記錄,不要使用COUNT:

DECLARE @n int
SELECT @n = COUNT(*) FROM dbo.Book
IF @n > 0
  print 'Records found'

這會讀整張表獲取記錄數量。使用EXISTS:

IF EXISTS(SELECT * FROM dbo.Book)
  print 'Records found'

這樣,SQL Server找到一條記錄後,就會停止讀取。

組合SELECT和UPDATE

有時候,需要SELECT和UPDATE同一條記錄。例如,需要在訪問記錄時,更新“LastAccessed”列。可以使用SELECT和UPDATE:

UPDATE dbo.Book
SET LastAccess = GETDATE()
WHERE BookId=@BookId
SELECT Title, Author
FROM dbo.Book
WHERE BookId=@BookId

但是,也可以組合SELECT到UPDATE中:

DECLARE @title nvarchar(50)
DECLARE @author nvarchar(50)
UPDATE dbo.Book
SET LastAccess = GETDATE(),
  @title = Title,
  @author = Author
WHERE BookId=@BookId
SELECT @title, @author

這可以節省一些時間,並且減少記錄持有的鎖的時間。

收集鎖詳細資訊

可以通過跟蹤SQL Server Profiler的“Blocked process report”事件查詢哪些查詢包含在嚴重的鎖延遲中。

這個事件的觸發發件是查詢的鎖等待時間超過“鎖程序閾值”。使用以下查詢設定這個閾值:

EXEC sp_configure 'show advanced options', 1
RECONFIGURE
EXEC sp_configure 'blocked process threshold', 30
RECONFIGURE

然後,在Profiler中開啟跟蹤:

  1. 開啟SQL Profiler。
  2. 在SQL Profiler,點選File | New Trace。
  3. 點選Events Selection選項卡。
  4. 選擇Show all events checkbox檢視所有事件。選擇Show all columns檢視所有資料列。
  5. 在主視窗中,展開Errors and Warnings,並選擇Blocked process report事件,確保TextData列中的複選框被選中。
  6. 如果需要跟蹤死鎖,展開Locks,選擇Deadlock graph事件。如果要得到死鎖的額外資訊,讓SQL Server將每個死鎖的資訊寫入到它的錯誤日誌中,執行:
    DBCC TRACEON(1222,-1)
  7. 反選其它選擇事件。
  8. 點選Run啟動跟蹤。
  9. 儲存模板,這樣下次就不需要重新建立。點選File | Save As | Trace Template。
  10. 一旦捕捉到資料後,點選File | Save儲存跟蹤資料到跟蹤檔案中,用於今後的分析。可以點選File | Open載入一個跟蹤檔案。

當在Profiler中點選一個Block process report時,可以在下面的視窗中看到事件的資訊,包括加鎖的查詢和被阻塞的查詢。使用同樣的方式可以得到死鎖圖的詳細資訊。

檢查SQL Server死鎖事件的錯誤日誌:

  1. 在SSMS中展開資料庫伺服器,展開Management並展開SQL Server Logs。雙擊一條日誌。
  2. 在日誌文件檢視器中,點選視窗頂部的Search,查詢“deadlock-list”。在死鎖列表事件後,可以找到死鎖包含的查詢語句的更多資訊。

減少鎖延遲

最有效的減少鎖延遲的方法是減少持有鎖的時間:

  • 優化查詢。查詢時間超短,持有鎖的時間超短。
  • 儲存過程優於臨時查詢。減少編譯執行計劃的時間和在網路上傳輸單個查詢的時間。
  • 如果必須使用遊標,頻繁提交更新。遊標處理要比集合處理慢得多。
  • 在持有鎖的時候不要處理長操作,例如傳送郵件。在開啟事務時,不要等待使用者輸入。使用樂觀鎖:

第二個減少鎖等待時間的方法是減少鎖住的資源:

  • 不要在頻繁更新的列上放置聚集索引。這會要求聚集索引和非聚集索引上都要鎖,因為它們的行上包含需要更新的值。
  • 考慮在非聚集索引上包括列。這會防止查詢讀表記錄,所以它不會阻塞其它查詢更新同一條記錄上不相關的列。
  • 考慮使用行版本控制。SQL Server的這個特性防止讀資料錶行的查詢阻塞更新相同行的查詢,或相反。更新相同行的查詢仍然相互阻塞。

    行版本控制在更新前將行儲存在臨時區域(tempdb資料庫),所以讀操作可以在進行更新的同時訪問臨時儲存的版本。這會產生額外的開銷用來維護行版本,在使用進行測試。並且,如果設定了事務的隔離級別,行版本控制只能工作在讀提交隔離模式下,這個模式是預設的模式。

    實現行版本控制,設定READ_COMMITTED_SNAPSHOT選項。當進行設定時,只能有一個連線開啟。可以通過將資料庫切換到單使用者模式。

    ALTER DATABASE mydatabase SET SINGLE_USER WITH ROLLBACK  
      IMMEDIATE;
    ALTER DATABASE mydatabase SET READ_COMMITTED_SNAPSHOT ON;
    ALTER DATABASE mydatabase SET MULTI_USER;

    檢查資料庫是否開啟了行版本控制,執行:

    select is_read_committed_snapshot_on
    from sys.databases 
    where name='mydatabase'

    最後,可以設定鎖超時時間。例如,中止等待時間超過5秒的語句:

    SET LOCK_TIMEOUT 5000

    使用1無限制等待。使用0不等待。

減少死鎖

死鎖是兩個事務都在等待對方釋放一個鎖。事務1有一個資源A的鎖,試圖獲得資源B的鎖,同時,事務2有一個資源B的鎖,試圖獲得資源A的鎖。現在,兩個事務都不能繼續。

一個減少死鎖的方法是減少鎖延遲,上節已經論述了。這會減少死鎖可能發生的時間窗。

第二個方法是始終以相同的順序鎖資源。如果事務2與事務1以相同的順序鎖資源(先A後B),那麼,事務2就不會在等待資源A前鎖住資源B,也就不會阻塞事務1了。

最後,小心使用HOLDLOCK或Repeatable Read或Serializable Read隔離級別。例如,以下程式碼:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN 
  SELECT Title FROM dbo.Book
  UPDATE dbo.Book SET Author='Charles Dickens' 
  WHERE Title='Oliver Twist'
COMMIT

假如有兩個事務同時執行這段程式碼。當它們執行SELECT時,都獲得了Book表中的行的選擇鎖。因為Repeatable Read隔離級別,它們都會持有鎖。現在,兩者都試圖請求Book表中的一行的更新鎖,以執行UPDATE。每一個事務現在都被另一個事務持有的選擇鎖阻塞了。

在SELECT語句中使用UPDLOCK防止這種情況。這會使得SELECT獲得更新鎖,這樣,只有一個事務可以執行SELECT。獲得鎖的事務可以執行UPDATE,然後釋放鎖,另一個事務也可以執行了。程式碼如下:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN 
  SELECT Title FROM dbo.Book WITH(UPDLOCK)
  UPDATE dbo.Book SET Author='Charles Dickens'  
  WHERE Title='Oliver Twist'
COMMIT

執行計劃重用

臨時查詢

考慮這個臨時查詢:

SELECT b.Title, a.AuthorName 
FROM dbo.Book b JOIN dbo.Author a ON b.LeadAuthorId=a.Authorid 
WHERE BookId=5

當SQL Server第一次接收到這個查詢時,它會編譯一個執行計劃,在計劃快取中儲存計劃,然後執行計劃。

如果SQL Server再一次接收到查詢,重用它執行計劃的條件是執行計劃還在計劃快取中,並且:

  • 查詢中的物件引用至少使用schema名稱限定。使用dbo.Book,不要使用Book。加上資料庫會更好。
  • 查詢文字精確匹配。匹配時是區分大小寫的,任何空白字元都會影響精確匹配。

作為第二個規則的結果,如果使用了相同的查詢,但不同的BookId,也不能匹配:

SELECT b.Title, a.AuthorName 
FROM dbo.Book b JOIN dbo.Author a ON b.LeadAuthorId=a.Authorid 
WHERE BookId=9 -- Doesn't match query above, uses 9 instead of 5

簡單引數化

為了使得臨時查詢重用快取的執行計劃容易一些,SQL Server支援簡單引數化。它會自動辨別查詢中的變數。因為這個功能很難做對,但是很容易做錯,SQL Server只會對單張表的非常簡單的查詢使用,例如

SELECT Title, Author FROM dbo.Book WHERE BookId=5

它可以使用以下查詢生成的執行計劃:

SELECT Title, Author FROM dbo.Book WHERE BookId=9

sp_executesql

為了不讓SQL Server去猜測查詢的哪一部分可以轉換為引數,可以使用系統儲存過程sp_executesql告訴SQL Server。呼叫sp_executesql的方式:

sp_executesql @query, @parameter_definitions, @parameter1, @parameter2, ...

例如:

EXEC sp_executesql 
  N'SELECT b.Title, a.AuthorName
  FROM dbo.Book b JOIN dbo.Author a ON b.LeadAuthorId=a.Authorid
  WHERE BookId=@BookId',
  N'@BookId int',
  @BookId=5

注意sp_executesql接收的前兩個引數是nvarchar值,所以需要使用以N為字首的字串。

儲存過程

除了向資料庫傳送單個的查詢,還可以將它們打包在一個儲存過程中,永久地儲存在資料庫中。這有以下好處:

  • 和sp_executesql一樣,儲存過程也允許顯式地定義引數,使得SQL Server更容易地重用執行計劃。
  • 儲存過程可以包含一系列查詢和T-SQL控制語句,例如IF…THEN。使用者只需要傳送儲存過程名和引數到伺服器,不需要傳送單獨的查詢語句,節省了網路開銷。
  • 儲存過程對網站程式碼隔離了資料庫細節。當表定義變化了,只需要更新一個或多個儲存過程,不需要修改網站。
  • 只允許通過儲存過程訪問資料庫,可以實現更好的安全性。這樣,可以允許使用者只訪問他們需要的資訊,而不能做計劃之外的操作。

建立儲存過程的程式碼:

CREATE PROCEDURE GetBook
  @BookId int
AS
BEGIN
  SET NOCOUNT ON;
  SELECT Title, Author FROM dbo.Book WHERE BookId=@BookId
END
GO

在開頭加入SET NOCOUNT ON,可以通過阻止SQL Server傳送儲存過程影響的行數的訊息,提高效能。

在查詢視窗執行儲存過程:

EXEC dbo.GetBook @BookId=5

或者,更簡單的方式

EXEC dbo.GetBook 5

在C#程式碼中使用儲存過程也很簡單:

string connectionString = "...";
using (SqlConnection connection = new SqlConnection(connectionString))
{
    string sql = "dbo.GetBook";
    using (SqlCommand cmd = new SqlCommand(sql, connection))
    {
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.Add(new SqlParameter("@BookId", bookId));
        connection.Open();
        // Execute database command ...
    }
}

阻止重用

有時,我們不想重用一個執行計劃。當編譯儲存過程的執行計劃時,計劃是基於那時使用的引數的。當計劃使用不同的引數重用時,使用第一組引數生成的計劃,在使用第二組引數時進行了重用。但是,我們並不希望這樣。

例如,考慮以下查詢:

SELECT SupplierName FROM dbo.Supplier WHERE City=@City

假設Supplier表在City列上有一個索引。假設Supplier中的一半記錄的City欄位值是“New York”。對於“New York”進行優化的執行計劃會使用全表掃描。但是,如果“San Diego”只有幾條記錄,對於“San Diego”的優化查詢計劃應該使用索引。對於一個引數好的計劃可能對於另一個計劃就是一個壞的計劃。如果使用次優查詢計劃的代價要比重新編譯查詢的代價高,最好是告訴SQL Server為每個查詢生成一個新計劃。

當建立儲存過程時,可以使用WITH RECOMPILE選項告訴SQL Server不要快取執行計劃。

CREATE PROCEDURE dbo.GetSupplierByCity
  @City nvarchar(100)
  WITH RECOMPILE
AS
BEGIN
...
END

也可以對於特定的執行過程生成一個新的計劃:

EXEC dbo.GetSupplierByCity 'New York' WITH RECOMPILE

最後,可以設定儲存過程在下次呼叫時重新編譯:

EXEC sp_recompile 'dbo.GetSupplierByCity'

設定使用某張表的儲存過程在下次呼叫時都重新編譯:

EXEC sp_recompile 'dbo.Book'

碎片

SQL Server提供兩種方法對錶和索引進行碎片整理:重建(rebuild)和重組(reorganize)。

索引重建

重建索引是對索引或表進行碎片整理最有效的方法。

ALTER INDEX myindex ON mytable REBUILD

這會使用更新頁方式物理地重建索引,最大限度地減少碎片。

如果重建的是聚集索引,實際上重建的是資料表,因為表是聚集索引的一部分。

重新一張表的所有索引:

ALTER INDEX ALL ON mytable REBUILD

索引重建有一個缺點是會阻塞所有訪問表和它的索引的查詢。它也可能阻塞所有正在訪問的查詢。可以使用ONLINE選擇減少這種情況:

ALTER INDEX myindex ON mytable REBUILD WITH (ONLINE=ON)

 

但是,這會導致重新時間更長。

另一個問題是重建是一個原子操作。如果有它完成前停止,所有已經完成的碎片整理工作都會丟失。

索引重組

與索引重建不同,索引重組不會阻塞表和它的索引,並且當它中途停止後,已完成的工作也不會丟失。但是,這是以降低效果為代價的。

重組索引,使用命令:

ALTER INDEX myindex ON mytable REORGANIZE

使用LOB_COMPACTION選項壓縮大物件(Large Object, LOB)資料,例如image、text、ntext、varchar(max)、nvarchar(max)、varbinary(max)和xml:

ALTER INDEX myindex ON mytable REORGANIZE WITH (LOB_COMPACTION=ON)

在一個繁忙的系統中,索引重組要比索引重建好更好。它不是原子性的,所以如果操作失敗了,不會導致所有的工作丟失。當它執行時,它只需要少量的持續較短的時間的鎖,而不是鎖住整張表和它的索引。如果發現一個頁正在使用,它只是跳過這個頁,並不再重試。

索引重組的缺點是它的效果更差,因為它會跳過頁,而且它不會建立新頁以達到更好地物理組織表或索引的目的。

堆表碎片整理

堆表是沒有聚集索引的表,因為沒有聚集索引,所以不能使用ALTER INDEX REBUILD或ALTER INDEX REORGANIZE進行碎片整理。

堆表碎片不是個大問題,因為表中的記錄根本就是無序的。當插入記錄時,SQL Server檢查表中是否還有空間,如果有,在那兒插入記錄。如果總是插入記錄,而更新或刪除記錄,所有記錄都會寫在表的結尾。如果更新或刪除記錄,堆表中就依然可能有間隙。

因為堆表碎片整理通常不是個問題,所以這裡不討論。但是,也有一些方法:

  • 建立一個聚集索引,然後刪除它。
  • 將堆表中的記錄插入到一個新表中。
  • 匯出資料,truncate表,再導回資料到那張表中。

記憶體

緩解記憶體壓力最常用的方法:

  • 增加實體記憶體。
  • 增加分配給SQL Server的記憶體。檢視當前分配的記憶體,執行:
    EXEC sp_configure 'show advanced option', '1'
    RECONFIGURE
    EXEC sp_configure 'max server memory (MB)'

    如果伺服器上的實體記憶體更多,增加分配。例如,增加到3000MB,執行:

    EXEC sp_configure 'show advanced option', '1'
    RECONFIGURE
    EXEC sp_configure 'max server memory (MB)', 3000
    RECONFIGURE WITH OVERRIDE

    不要分配所有的實體記憶體。留幾百MB給作業系統和其它軟體。

  • 減少從磁碟讀取的資料。從磁碟讀取的每一頁都需要儲存在記憶體中,並在記憶體中處理。全表掃描、聚集查詢和表連線都會讀取大量的資料。參考查明瓶頸的索引缺失和昂貴查詢小節,減少從磁碟讀取的資料。

  • 儘量重用執行計劃,減少計劃快取需要的記憶體。參考查明瓶頸的執行計劃重用小節。

磁碟

一些減少磁碟系統壓力的常用方法:

  • 優化查詢處理。
  • 將日誌檔案移動到一個專用的物理磁碟上。
  • 減少NTFS檔案系統的碎片。
  • 移動tempdb資料庫到專用磁碟上。
  • 將資料分散到兩個或多個磁碟上,分散負載。
  • 移動負載大的資料庫物件到另一個磁碟上。
  • 使用正確的RAID配置

優化查詢處理

確保正確的索引和優化最昂貴的查詢。

將日誌檔案移動到一個專用的物理磁碟上

移動磁碟的讀/寫頭是相同較慢的過程。日誌檔案是順序寫的,它本身需要很少的磁碟頭移動。但是如果日誌檔案和資料檔案在同一磁碟上,是沒有用的,因為磁碟頭必須在日誌文字和資料檔案間移動。

如果將日誌檔案放在它自己的磁碟上,那個磁碟上磁碟頭移動會很小,結果是更快的訪問日誌檔案。修改操作,例如更新、插入和刪除等修改操作會更快。

移動一個已存在的資料庫的日誌檔案到另一個磁碟,首先分離資料庫,移動日誌檔案到專用磁碟。然後重新附加資料庫,指定日誌檔案的新位置。

減少NTFS檔案系統的碎片

如果NTFS資料庫檔案有碎片了,磁碟頭在讀檔案時,必須不停地移動磁碟頭。為了減少碎片,為資料庫和日誌檔案設定一個比較大的初始檔案大小和較大的增長大小。設定足夠大,保證檔案不會增長到那麼大,會更好。這樣做的目的就是避免檔案增長和收縮。

如果需要增長和收縮資料庫或日誌檔案,考慮使用64-KB的NTFS簇大小,以匹配SQL Server讀的模式。

移動tempdb資料庫到專用磁碟上

tempdb用來排序、子查詢、臨時表、聚集、遊標等。它可能非常繁忙。這使得將它移動到它專屬的磁碟或不是很忙的磁碟會比較好。檢查伺服器上的tempdb和其它資料庫的資料庫和日誌檔案的活動資訊,使用 dm_io_virtual_file_stats DMV:

SELECT d.name, mf.physical_name, mf.type_desc, vfs.*
FROM sys.dm_io_virtual_file_stats(NULL,NULL) vfs
JOIN sys.databases d ON vfs.database_id = d.database_id 
JOIN sys.master_files mf ON mf.database_id=vfs.database_id AND 
mf.file_id=vfs.file_id

移動tempdb資料和日誌檔案到G:盤,設定它們大小為10MB和1MB,執行並重啟伺服器:

ALTER DATABASE tempdb MODIFY FILE (NAME = tempdev, FILENAME = 'G:\
tempdb.mdf', SIZE = 10MB) 
GO
ALTER DATABASE tempdb MODIFY FILE (NAME = templog, FILENAME = 'G:\
templog.ldf', SIZE = 1MB) 
GO

為了減少碎片,防止tempdb資料和日誌增長和收縮,可以給它們可能需要的最大空間。

將資料分散到兩個或多個磁碟上

增加檔案到資料庫的PRIMARY檔案組。SQL Server會將資料分散到已存在的和新檔案。將新檔案放到新磁碟或負載不是很大的磁碟。如果可以,設定初始大小足夠大,這會減少碎片。

例如,為資料uneUp資料在G:盤上增加一個初始大小為20GB的檔案,執行:

ALTER DATABASE TuneUp 
ADD FILE (NAME = TuneUp_2, FILENAME = N'G:\TuneUp_2.ndf', SIZE = 20GB)

注意副檔名.ndf,這是推薦的第二檔案的副檔名。

移動負載大的資料庫物件到另一個磁碟上

你可以移動負載大的資料物件,例如索引,到一個新磁碟,或不太繁忙的磁碟上。使用查明瓶頸的索引缺失和昂貴查詢小節介紹的dm_db_index_usage_stats DMV可以檢視每個索引上執行的讀和寫的數量。

如果伺服器有多個磁碟,在查明瓶頸的磁碟小節有度量磁碟使用情況的方法。使用這個資訊決定物件移動到哪個磁碟。

將索引移動到另一個磁碟,首先建立一個新的使用者自定義檔案組。例如,以下語句建立檔案組FG2:

ALTER DATABASE TuneUp ADD FILEGROUP FG2

然後,在檔案組中加入檔案:

ALTER DATABASE TuneUp 
ADD FILE (NAME = TuneUp_Fg2, FILENAME = N'G:\TuneUp_Fg2.ndf', SIZE = 200MB)
TO FILEGROUP FG2

最後,移動物件到檔案組中,例如,將表Book的Title列上的非聚集索引IX_Title移動到檔案組FG2中:

CREATE NONCLUSTERED INDEX [IX_Title] ON [dbo].[Book]([Title] ASC) WITH DROP_EXISTING ON FG2

可以分配多個物件給一個檔案組。可以在一個檔案組中加入多個檔案,這將允許將一個非常繁忙的表或索引分散到多個磁碟。

將表和它們的非聚集索引到不同的磁碟,這樣一個任務可以讀索引,另一個任務可以在表中進行鍵查詢。

使用正確的RAID配置

為了提高效能和容錯性,很多資料庫伺服器使用RAID子系統替代單獨的驅動器。RAID子系統有不同的配置。為資料檔案、日誌檔案和tempdb檔案選擇正確的配置可能極大地影響效能。

最常用的RAID配置是:

RAID配置 描述
RAID 0 Each fle is spread ("striped") over each disk in the array. When reading or writing a fle, all disks are accessed in parallel, leading to high transfer rates.
RAID 5 Each file is striped over all disks. Parity information for each disk is stored on the other disks, providing fault tolerance. File writes are slow—a single fle write requires 1 data read + 1 parity read + 1 data write + 1 parity write = 4 accesses.
RAID 10 Each fle is striped over half the disks. Those disks are mirrored by the other half, providing excellent fault tolerance. A fle write requires 1 data write to a main disk + 1 data write to a mirror disk.
RAID 1 This is RAID 10 but with just 2 disks, a main disk and a mirror disk. That gives you fault tolerance but no striping.

下表是RAID與單個磁碟效能比較,N表示磁碟陣列中的磁碟個數:

  讀速度 寫速度 容錯
單個磁碟 1 1 no
RAID 0 N N no
RAID 5 N N/4 yes
RAID 10 N N/2 yes
RAID 1 2 1 yes

下表是對tempdb、資料和日誌檔案合理的RAID配置:

檔案 效能相關屬性 建議的RAID配置
tempdb Requires good read and write performance for random access. Relatively small. Losing temporary data may be acceptable. RAID 0, RAID 1, RAID 10
log Requires very good write performance, and fault tolerance. Uses sequential access, so striping is no beneft. RAID 1, RAID 10

data (writes make up less than 10 percent of accesses)

Requires fault tolerance. Random access means striping is benefcial. Large data volume. RAID 5, RAID 10

data (writes make up over 10 percent of accesses)

Same as above, plus good write performance. RAID 10

有電池後備電源快取的RAID控制器能很大地提高寫效能,因為這允許SQL Server將寫請求交付給快取,需要等待物理磁碟訪問完成。控制器在後臺執行快取的寫請求。

CPU

解決處理器瓶頸的一般方法包括:

  • 優化CPU密集查詢。在查明瓶頸的索引缺失和昂貴查詢小節中可以找到最昂貴查詢的方法。DMV可以列出每個查詢的CPU使用率。
  • 建立查詢計劃是高CPU密集的。提高執行計劃重用。
  • 安裝更多更快的處理器、L2/L3快取,或更有效的驅動器。

轉自:https://www.cnblogs.com/ntwo/archive/2010/12/15/1907040.html

SQL Server本身就是個很大的題目。這裡不會涉及到SQL Server資料庫訪問的方方面面,而是重點關注於可能獲得最大效能提升的領域。

查明瓶頸

缺少索引和昂貴查詢

可以通過減少查詢執行的讀操作極大地提高查詢效能。執行的讀操作越多,對磁碟、CPU和記憶體的壓力就可能越大。第二,進行讀操作的查詢可能阻塞其它進行更新的查詢。如果更新查詢在持有鎖時必須進行等待,它可能會延遲一系列其它查詢。最後,除非整個資料庫都在記憶體中,每次從磁碟上讀取資料,都需要從記憶體中刪除其它資料。如果被刪除的資料後來被用到,需要重新從磁碟上讀取。

減少讀操作最有效的方法是在表上建立有效索引。SQL Server索引允許查詢不需要掃描整個表,而只讀取需要的部分。但是,索引會有額外的開銷並減慢更新操作,所以必須小心使用。

缺少索引

SQL Server允許在表字段上加索引,提高對這些欄位進行操作的WHERE和JOIN語句的速度。當查詢優化器優化查詢時,會儲存似乎應該有但沒有的索引的資訊。可能使用Dynamic Management View(DVM)訪問這些資訊:

select d.name AS DatabaseName, mid.*
from sys.dm_db_missing_index_details mid
join sys.databases d ON mid.database_id=d.database_id

這個查詢返回的最重要列是:

描述
DatabaseName 這一行屬於的資料庫
equality_columns 具有等於操作符的逗號分割的列的列表,例如:
column=value
inequality_columns 具有比較操作符的逗號分割的列的列表,例如:
column>value
included_columns 如果包括在索引中可能會有收益的逗號分割的列的列表
statement 缺失索引的表名

這些資訊會在重啟伺服器後清空。

另一個方法是使用SQL Server 2008包含的Data Engine Tuning Advisor。這個工具會分析資料操作跟蹤資料,根據所有查詢識別出最佳的索引集。它甚至給出了建立識別出的缺失索引的SQL語句。

第一步是得到一段時間內的資料庫操作的跟蹤資料。在資料庫最繁忙的時間段內,開啟跟蹤:

  1. 開啟SQL Profiler。選擇Start | Programs | Mircrosoft SQL Server 2008 | Performance Tools | SQL Server Profiler。
  2. 在SQL Profiler中,選擇File | New Trace。
  3. 選擇Events Selection選項頁。
  4. 只保留SQL:BatchCompleted和RPC:Completed事件。確保選擇了事件的TextData列。
  5. 單擊Column Filters按鈕。選擇Database Name列,展開Like,輸入需要監控的資料庫名稱。
  6. 選擇ApplicationName,過濾需要監控的程式。
  7. 單擊Runt按鍵,監控結束後,儲存到檔案。
  8. 儲存為模板,下次不用再建立。選擇File | Save As | Trace Template。下次建立新跟蹤時,可能從Use the template下拉框選擇模板。
    傳送這些事件到螢幕會佔用很多伺服器資源。解決方法是儲存跟蹤為指令碼,然後使用指令碼進行後臺跟蹤。
  9. 選擇File | Export | Script Trace Definition | For SQL Server 2005-2008。現在可以關閉SQL Server Profiler,這會關閉跟蹤。
  10. 在SQL Server Management Studio中,開啟剛才建立的.sql檔案。搜尋字串“InsertFileNameHere”,替換成你想要日誌儲存的檔案的完整路徑。儲存。
  11. 開始跟蹤,F5執行.sql檔案。它會顯示跟蹤的trace ID。
  12. 檢視系統中的跟蹤狀態,在查詢視窗執行命令:
    select * from ::fn_trace_getinfo(default)

    查詢執行的trace ID行中property列為5的行。如果這行的value列值是1,跟蹤正在執行。trace ID為1的跟蹤的系統跟蹤。
  13. 跟蹤一段時間後,假設trace ID是2,執行以下命令停止跟蹤:
    exec sp_trace_setstatus 2,0
    重啟跟蹤,執行:
    exec sp_trace_setstatus 2,1
  14. 停止並關閉跟蹤,這樣才能訪問跟蹤檔案,執行:
    exec sp_trace_setstatus 2,0
    exec sp_trace_setstatus 2,2

執行Database Engine Tuning Advisor:

  1. 選擇Start | Programs | Microsoft SQL Server 2008 | Performance Tools | Database Engine Tuning Advisor。

  2. 在Workload區域,選擇trace檔案。在Database for workload analysis下拉框,選擇需要分析的第一個資料庫。

  3. 在Select databases and table to tune,選擇需要索引建議的資料庫。

  4. 如果跟蹤檔案很大,Database Engine Tuning Advisor會花費很長時間進行分析。在Tuning Options選項頁,可以選擇何時停止分析。

  5. 點選Start Analysis按鍵開始分析。

 

注意Database Engine Tuning Advisor只是個程式。可以考慮這些建議,但自己做決定。確保在典型時段進行跟蹤,否則這些建議可能會使事情更糟。例如,如果提供晚上抓取的跟蹤檔案,那時只處理少量事務,但生成大量報表,那麼這些建議就會去優化報表而不是事務。

昂貴查詢

如果使用SQL Server 2008或更高版本,可以使用活動監視器查詢最近執行的昂貴查詢。在SSMS中,右擊資料庫伺服器,選擇Activity Monitor。

可以通過使用DMV dm_exec_query_stats可以獲取更多的資訊。當查詢優化器為查詢建立執行計劃時,它會快取計劃進行重用。每次使用查詢計劃執行查詢時,效能統計會被保留。可以使用dm_exec_query_stats檢視這些統計。

SELECT
  est.text AS batchtext,
  SUBSTRING(est.text, (eqs.statement_start_offset/2)+1,  
    (CASE eqs.statement_end_offset WHEN -1 
    THEN DATALENGTH(est.text) 
    ELSE eqs.statement_end_offset END - 
    ((eqs.statement_start_offset/2) + 1))) AS querytext,
  eqs.creation_time, eqs.last_execution_time, eqs.execution_count, 
  eqs.total_worker_time, eqs.last_worker_time, 
  eqs.min_worker_time, eqs.max_worker_time, 
  eqs.total_physical_reads, eqs.last_physical_reads, 
  eqs.min_physical_reads, eqs.max_physical_reads, 
  eqs.total_elapsed_time, eqs.last_elapsed_time, 
  eqs.min_elapsed_time, eqs.max_elapsed_time, 
  eqs.total_logical_writes, eqs.last_logical_writes, 
  eqs.min_logical_writes, eqs.max_logical_writes,
  eqs.query_plan_hash 
FROM
  sys.dm_exec_query_stats AS eqs
  CROSS APPLY sys.dm_exec_sql_text(eqs.sql_handle) AS est
ORDER BY eqs.total_physical_reads DESC

DMV有一個限制:當執行它時,自從上次伺服器重啟後執行的查詢在快取不是都有查詢計劃。一些計劃因為使用次數少會過期。那些生成開銷很小,但執行開銷又不夠小的計劃,根本就不會儲存。如果計劃被重新編譯了,那統計資料是從上次重新編譯開始的。

另一個限制是查詢只適用於儲存過程。如果使用臨時查詢,引數是嵌入在查詢中的。這會導致查詢優化器為每一組引數生成一個計劃,除非查詢已經是引數化的。這會在查詢計劃重用部分進一步討論。

為了解決這個問題,dm_exec_query_stats返回query_plan_hash列,如果查詢的執行計劃是相同的,這列的值就是相同的。通過使用GROUP BY聚合這一列,可以得到使用相同邏輯的查詢的總效能資料。

這個查詢返回下資訊:

描述
batchtext Text of the entire batch or stored procedure containing the query.
querytext Text of the actual query.
creation_time Time that the execution plan was created.
last_execution_time Last time the plan was executed.
execution_count Number of times the plan was executed after it was created. This is not the number of times the query itself was executed; its plan may have been recompiled at some statge.
total_worker_time Total amount of CPU time in microseconds that was consumed by executions of this plan since it was created.
last_worker_time CPU time in microseconds that was consumed the last time the plan was executed.
min_worker_time Minimum CPU time in microseconds that this plan has ever consumed during a single execution.
max_worker_time Maximum CPU time in microseconds that this plan has ever consumed during a single execution.
total_physical_reads Total number of physical reads performed by executions of this plan since it was compiled.
last_physical_reads Number of physical reads performed the last time the plan was executed.
min_physical_reads Minimum number of physical reads that this plan has ever performed during a single execution.
max_physical_reads Maximum number of physical reads that this plan has ever performed during a single execution.
total_logical_writes Total number of logical writes performed by executions of this plan since it was compiled.
last_logical_writes Number of logical writes performed the last time the plan was executed.
min_logical_writes Minimum number of logical writes that this plan has ever performed during a single execution.
max_logical_writes Maximum number of logical writes that this plan has ever performed during a single execution.
total_elapsed_time Total elapsed time in microseconds for completed executions of this plan.
last_elapsed_time Elapsed time in microseconds for the most recently completed execution of this plan.
min_elapsed_time Minimum elapsed time in microseconds for any completed execution of this plan.
max_elapsed_time Maximum elapsed time in microseconds for any completed execution of this plan.

另一種方法是分析SQL Server Profiler生成的跟蹤檔案。

為了更好的分析,可以將跟蹤檔案儲存為表格:

  1. 開啟SQL Profiler。
  2. 開啟跟蹤檔案:File | Open | Trace File。
  3. 儲存跟蹤為表格:File | Save As | Trace Table。

還可以使用fn_trace_gettable:

SELECT * INTO newtracetable FROM ::fn_trace_gettable('c:\trace.trc', default)

找到最昂貴查詢或儲存過程的最容易的方法是使用GROUP BY,根據查詢或儲存過程聚合效能資料。但是,如果查看錶中的TextData列,就會發現所有的查詢或儲存過程呼叫都包括引數值。如果想要聚合它們,必須過濾掉這些引數。

如果呼叫的是儲存過程,刪除引數還不是很難,因為它們總是是儲存過程名後面。

如果呼叫的是臨時查詢,刪除引數就比較困難了,因為它們在每個查詢中的位置都是不一樣的。有一些工具可以是工作變得容易些:

一旦定位了最昂貴的查詢,就可以判斷新增索引是否可以加速執行:

  1. 在SMMS中開啟一個查詢視窗。
  2. 在“Query”選單,選擇“Include Actual Execution Plan”或者使用快捷鍵Ctrl+M。
  3. 複製昂貴查詢到查詢視窗並執行,開啟“Execution plan”選項頁。
  4. 如果查詢優化器發現缺失索引,就會顯示綠色的訊息。
  5. 查詢更多資訊,可以右鍵單擊執行計劃視窗,選擇“顯示查詢計劃XML”。在XML中,查詢MissingIndexes元素。

如果發現是缺失索引,參考修復瓶頸節的缺失索引子節。

如果發現昂貴查詢,參考修復瓶頸節的昂貴查詢子節。

未使用索引

索引的一個缺點是當資料更新時,它們也需要更新,這就導致了延遲。它們也會佔用磁碟空間。如果一個索引拖慢更新,並且幾乎不使用,最好刪除它。

使用DMV dm_db_index_usage_stats可以獲得每個索引的使用資訊:

SELECT d.name, t.name, i.name, ius.*
FROM sys.dm_db_index_usage_stats ius
JOIN sys.databases d ON d.database_id = ius.database_id
JOIN sys.tables t ON t.object_id = ius.object_id
JOIN sys.indexes i ON i.object_id = ius.object_id AND i.index_id = 
ius.index_id 
ORDER BY user_updates DESC

 

這個查詢會顯示每個上次伺服器啟動後有活動的索引的名稱、表和資料庫,和自從上次伺服器啟動後更新和讀的數量。

user_updates列,顯示由INSERT、UPDATE和DELETE操作引起的更新的數量。如果這個數字相對於讀的數量很高,考慮刪除這個索引:

DROP INDEX IX_TITLE ON dbo.Book

如果資料庫有很多查詢在同時執行,一些查詢會訪問相同的資源,例如一張表或索引。在一個查詢更新資源時,另一個查詢是不能讀的;否則會導致不一致的結果。

為了阻止查詢訪問資源,SQL Server會鎖資源。等待鎖釋放的查詢會有一些延遲。如果想要確定這些延遲是否過度,在資料庫伺服器上使用perfmon檢查以下計數器:

分類:SQL Server:Latches

Total Latch Wait Time (ms): Total wait time in milliseconds for latches in the last second.
Lock Timeouts/sec: Number of lock requests per second that timed out. This includes requests for NOWAIT locks.
Lock Wait Time (ms): Total wait time in milliseconds for locks in the last second.
Number of Deadlocks/sec: Number of lock requests per second that resulted in a deadlock.

如果Total Latch Wait Time (ms)的數值很高,表示SQL Server使用它自己的同步機制等待的時間很長。通常情況下,Lock Timeouts/sec應該為0,Lock Wait Time (ms)應該很低。如果不是,查詢等待鎖釋放的時間太長了。

最後,Number of Deadlocks/sec應該為0。否則,存在查詢互相等待對方釋放鎖,阻止它們訪問資源。SQL Server最後會檢測到這個情況,通過回滾其中一個查詢,但是這會浪費時間和已經完成的工作。

如果發現鎖的問題,參考修復瓶頸節的鎖子節,查找出哪些查詢導致過長的鎖等待時間。

執行計劃重用

在執行查詢時,SQL Server查詢優化器會編譯一個成本最低的查詢計劃。這會使用很多CPU週期,所以,SQL Server會在記憶體中快取查詢計劃。當接收查詢時,會試圖與快取的查詢計劃進行匹配。

效能計數器

分類:Processor(_Total)

% Processor Time: The percentage of elapsed time that the processor is busy.
型別:SQL Server: SQL Statistics

SQL Compilations/sec: Number of batch compiles and statement compiles per second. Expected to be very high initially after server startup.
SQL Re-Compilations/sec: Number of recompiles per second.

這些計數器在伺服器剛啟動時,會顯示很高的數值,因為每一個收到的查詢都需要編譯。執行計劃快取是在記憶體中的,所以每次重啟都需要重新編譯。在正常情況下,每秒編譯次數應該小於100,重新編譯次數應該接近0。

dm_exec_query_optimizer_info

另一種方法是檢視伺服器優化查詢花費的時間。因為查詢優化是CPU密集型操作,所以幾乎所有的CPU時間都花費在這上面了。

Dynamic Management View (DMV) sys.dm_exec_query_optimizer_info顯示上次伺服器重啟後查詢優化次數和平均時間(單位秒)。

SELECT 
  occurrence AS [Query optimizations since server restart],
  value AS [Avg time per optimization in seconds],
  occurrence * value AS [Time spend optimizing since server  
    restart in seconds]
FROM sys.dm_exec_query_optimizer_info
WHERE counter='elapsed time'

執行這個查詢,等待一段時間,再運行了一次。這樣就能得到這段時間優化查詢的時間。

sys.dm_exec_cached_plans

DMV sys.dm_exec_cached_plans提供計劃快取中所有執行計劃的資訊。可能與DMV sys.dm_exec_sql_text組合使用找出給定查詢的查詢計劃的重用頻率。如果一個查詢或儲存過程的執行計劃的重用比率很低,那麼從計劃快取中可以得到的好處也是很有限的。

SELECT ecp.objtype, ecp.usecounts, ecp.size_in_bytes, 
  REPLACE(REPLACE(est.text, char(13), ''), char(10), ' ') AS querytext
FROM sys.dm_exec_cached_plans ecp
cross apply sys.dm_exec_sql_text(ecp.plan_handle) est
WHERE cacheobjtype='Compiled Plan'

objtype列的值是Proc表示是儲存過程,Adhoc表示是臨時查詢,usecounts顯示計劃的使用次數。

碎片

資料庫中資料和索引在磁碟中是以8KB頁的大小組織的。頁是SQL Server與磁碟中交換資料的最小單位。

當插入或更新資料時,一個頁的空間可能不夠了,SQL Server建立一個新頁,將原來頁上的一半內容移動到新頁上。這使新頁和老頁上都留下了空閒空間。這樣,如果在老頁上不停地插入和更新資料,就不會持續得分割資料。

這樣,在很多次更新、插入和刪除資料後,就會有很多半滿的頁。這會佔用更多的磁碟空間,更重要的是會拖慢資料讀取。這些頁和物理順序與SQL Server需要讀取的邏輯順序也可能不一樣。結果,SQL Server需要等待磁碟頭到達下一頁,而不是順序讀取,這樣,就會有更多的延遲。

使用dm_db_index_physical_stats DMV查詢表和索引的碎片程度:

DECLARE @DatabaseName sysname
SET @DatabaseName = 'mydatabase' --use your own database name
SELECT o.name AS TableName, i.name AS IndexName, ips.index_type_desc,
  ips.avg_fragmentation_in_percent, ips.page_count, ips.fragment_count, 
  ips.avg_page_space_used_in_percent
FROM sys.dm_db_index_physical_stats(
  DB_ID(@DatabaseName),
  NULL, NULL, NULL, 'Sampled') ips
JOIN sys.objects o ON ips.object_id = o.object_id
JOIN sys.indexes i ON (ips.object_id = i.object_id) AND (ips.index_id = i.index_id)
WHERE (ips.page_count >= 7) AND (ips.avg_fragmentation_in_percent > 20)
ORDER BY o.name, i.name

這會統計所有使用超過7個頁並且這些頁的碎片超過20%的表和索引。

如果索引型別是CLUSTERED INDEX,實際上指的是表,因為表是聚集索引的一部分。索引型別HEAP指的是沒有聚集索引的表。

記憶體

在perfmon中使用以下資料器查詢是否記憶體不中拖慢資料庫伺服器:

分類:Memory

Pages/sec: When the server runs out of memory, it stores information temporarily on disk, and then later reads it back when needed, which is very expensive. This counter indicates how often this happens.

分類:SQL Server: Buffer Manager

Page Life Expectancy: Number of seconds a page will stay in the buffer pool without being used. The greater the life expenctancy, the greater the change that SQL Server will be able to get a page from memory instead of having to read it from disk.
Buffer cache hit ratio: Percentage of pages that were found in the buffer pool, without having to read from disk.

如果Pages/sec一直很高或者Page Life Expectancy一直很低,低於300,或Buffer cache hit ratio一直很低,低於90%,SQL Server可能沒有足夠的記憶體。這會導致過度的磁碟I/O,造成更大的CPU和磁碟壓力。

磁碟

如果在上一節發現記憶體問題,首先修復它,因為記憶體不足會導致更多的磁碟使用。否則,檢查以下計數器:

分類:PhysicalDisk and LogicalDisk

% Disk Time: Percentage of elapsed time that the selected disk was busy reading or writing.
Avg. Disk Queue Length: Average number of read and write requests queued during the sample interval.
Current Disk Queue Length: Current number of request queued.

如果Disk Time持續保持在85%以上,磁碟系統的壓力就比較大了。

Avg. Disk Queue Length和Current Disk Queue Length指磁碟控制器排隊的任務數和正在處理的任務數。正常的數字應該是2以下。如果使用了磁碟陣列,控制器被附加到多個磁碟,計數器值是磁碟數量的兩倍或更少。

CPU

如果發現記憶體或磁碟問題,先解決它們,因為它們會增加磁碟的壓力。CPU計數器:

分類:Processor

% Processor Time: Proportion of time that the processor is busy.

分類:System

Processor Queue Length: Number of threads waiting to be processed.

如果% Processor Time持續高於75%,或Processor Queue Length持續高於2,CPU可能壓力過大。

修復瓶頸

缺失索引

聚集索引

考察這張表:

CREATE TABLE [dbo].[Book](
  [BookId] [int] IDENTITY(1,1) NOT NULL,
  [Title] [nvarchar](50) NULL,
  [Author] [nvarchar](50) NULL,
  [Price] [decimal](4, 2) NULL)

 

因為這張表沒有聚集索引,所以稱為堆表。它的記錄是無序的。如果需要查詢標題包含某一關鍵字的所有書籍,必須讀取所有的記錄。這張表的結構非常簡單:

我們可以測試在這張表中定位一條記錄需要多少時間,然後與有索引的表進行對比。

告訴SQL Server顯示I/O和計算查詢的時間:

SET STATISTICS IO ON
SET STATISTICS TIME ON

清空記憶體快取:

CHECKPOINT
DBCC DROPCLEANBUFFERS

在有一百萬條記錄的表中執行查詢:

SELECT Title, Author, Price FROM dbo.Book WHERE BookId = 5000

測試機器上的結果是:9564, CPU time: 109 ms, elapsed time: 808 ms。

SQL Server使用8KB的頁儲存所有資料。結果顯示讀取了9564個頁,也就是整張表。

現在,加入聚集索引:

ALTER TABLE Book ADD CONSTRAINT [PK_Book] PRIMARY KEY CLUSTERED ([BookId] ASC)

這會在列BookId上建立一個索引,使得BookId上的WHERE和JOIN語句更快。索引會根據BookId排序表,並增加一個稱為B-樹的結構加速訪問:

現在,運行同樣的查詢,結果是:

reads: 2, CPU time: 0 ms, elapsed time: 32 ms。

非聚集索引

現在,我們使用Title替代BookId進行查詢:

SELECT Title, Author FROM dbo.Book WHERE Title = 'Don Quixote'

結果是:reads: 9146, CPU time: 156 ms, elapsed time: 1653 ms。

這和堆表的結果差不多。

解決方法是在Title列中建立索引。然而,因為聚集索引會使得表也按照索引欄位排序,所以只能有一個聚集索引。因為已經在BookId上建立了聚集索引,所以只能建立非聚集索引。

非聚集索引建立了表記錄的備份,這次是按照Title排序的。為了節省空間,SQL Server排除了聚集索引欄位以外的其它列。一張表上可以建立249個非聚集索引。

因為我們需要在查詢中訪問其它欄位,我們需要聚集索引可以連結到表記錄。方法是在非聚集索引記錄中加入BookId。因為BookId有聚集索引,一旦通過非聚集索引找到BookId,就可以使用聚集索引得到真正的表記錄。這個方法中的第二步驟稱作鍵查詢。

為什麼要通過聚集索引,而不在非聚集索引中使用表記錄的實體地址?因為當更新表記錄時,記錄可能會變大,SQL Server需要移動後面的記錄騰出空間。如果非聚集索引包括實體地址,每次移動記錄時,都需要更新地址。在更慢的更新和更慢的讀之間有一個平衡。如果沒有聚集索引或聚集索引沒有唯一約束,非聚集索引記錄就包括實體地址。

檢視非聚集索引的效果,首先建立它:

CREATE NONCLUSTERED INDEX [IX_Title] ON [dbo].[Book]([Title] ASC)

 

執行查詢:

SELECT Title, Author FROM dbo.Book WHERE Title = 'Don Quixote'

結果是: reads: 4, CPU time: 0 ms, elapsed time: 46 ms。

包含列

再一次檢查測試查詢,它只是返回Title和Author。Title已經在非聚集索引記錄中了。如果在索引中加入Author,就不需要等待SQL Server訪問表記錄了,跳過了鍵查詢步驟。

可以通過在非聚集索引中包括Author:

CREATE NONCLUSTERED INDEX [IX_Title] ON [dbo].[Book]([Title] ASC) INCLUDE(Author) WITH drop_existing

 

現在再執行查詢:

SELECT Title, Author FROM dbo.Book WHERE Title = 'Don Quixote'

結果:reads: 2, CPU time: 0 ms, elapsed time: 26 ms。

讀從4降到了2,使用時間從46ms降到了26ms,有50%的提升。從絕對值來看,提升不多,但如果查詢執行非常頻繁,這樣做還是很有意義的。但也不做過了頭,聚集索引記錄越大,8KB頁上儲存的記錄就越少,SQL Server就需要讀更多的頁。

選擇合適的列建立索引

因為建立和維護索引都需要開銷,所以必須選擇合適的列建立索引。

  • 在列上建立主鍵,預設會在這些列上建立聚集索引。
  • 在一列上建立索引,會影響使用這張表的所有查詢。所以不要只關注一個查詢。
  • 在資料庫上建立索引前,必須先做測試,確定這樣做真會改善效能。

何時使用索引

當選擇建立索引時,可以使用以下決策過程:

  • 找出最昂貴查詢。可以看到Database Engine Tuning Advisor生成的索引建議。
  • 在每個JOIN的至少一列上建立一個索引。
  • 考慮ORDER BY和GROUP BY子句中使用的列。
  • 考慮使用WHERE子句中的列,特別是如果WHERE選擇的記錄數較少。但是,需要注意:
    • 使用函式的WHERE子句不能使用索引,因為函式輸出不在索引中。例如:
      SELECT Title, Author FROM dbo.Book WHERE LEFT(Title, 3) = 'Don'
      在Title列中放置索引不會使這個查詢更快。
    • 如果在WHERE子句中使用查詢字串開關是萬用字元的LIKE語句,SQL Server不會使用索引:
      SELECT Title, Author FROM dbo.Book WHERE Title LIKE '%Quixote'
      但是如果查詢字串是以文字常量開頭的,能夠使用索引:
      SELECT Title, Author FROM dbo.Book WHERE Title LIKE 'Don%'
  • 考慮使用有唯一約束的列。這會有助於檢查新值是否是唯一的。
  • MIN和MAX函式可以從索引中獲益,因為值是排序的,就不需要查詢整張表確定最大或最小值。
  • 使用佔用很多空間的欄位建立索引要三思。如果是非聚集索引,列值會在索引中重複儲存。如果是聚集索引,列值會在所有非聚集索引中重複儲存。這會增加索引記錄的大小,這樣,在8KB頁上只能存放更少的索引記錄,這會使得SQL Server讀取更多的頁。

何時不使用索引

實際上建立過多的索引會降低效能,不在列上放置索引的主要原因:

  • 列經常更新。
  • 列低特殊性,也就是列上的值有很多重複。

列經常更新

當更新沒有索引的列時,如果沒有頁分割,SQL Server需要向磁碟寫入一個8KB的頁。

但是,如果列上有一個非聚集索引,或者包含上非聚集索引中,SQL Server也需要更新索引。所以至少需要寫入至少一個額外頁。它還需要更新索引使用的B-樹結構,潛在地需要更新更多的頁。

如果更新了聚集索引的列,使用了舊值的非聚集索引記錄也需要更新,因為非聚集索引中使用聚集索引鍵,導航到真正的資料庫記錄。第二,資料庫記錄也是根據聚集索引排序的,如果更新導致排序順序改變了,就需要更多的寫。最後,聚集索引需要B-樹。

低特殊性

即使列上有索引,查詢優化器也不是使用它。每一次SQL Server通過索引訪問一條記錄,必須使用索引結構。在非聚集索引中,可能還需要進行鍵查詢。例如,如果選擇所有價格為20元的書,恰好也有很多書是這個價格,有可能簡單地讀取所有書記錄反而更快一點。在這種情況下,20元價格就是低特殊性。

可以使用一個簡單的查詢計算一列中的值的平均選擇性。例如,計算Book表中的Price列的平均選擇性:

SELECT
  COUNT(DISTINCT Price) AS 'Unique prices',
  COUNT(*) AS 'Number of rows',
  CAST((100 * COUNT(DISTINCT Price) / CAST(COUNT(*) AS REAL)) 
    AS nvarchar(10)) + '%'  AS 'Selectivity'
FROM Book

 

如果每本書都有不同的價格,選擇性就是100%。如果選擇性低於85%,索引增加開銷會比節省的開銷更大。

有一些價格會比其它價格出現的次數更多。檢視每一個價格的選擇性,執行:

DECLARE @c real
SELECT @c = CAST(COUNT(*) AS real) FROM Book
SELECT 
  Price, 
  COUNT(BookId) AS 'Number of rows',
  CAST((1 - (100 * COUNT(BookId) / @c)) 
    AS nvarchar(20)) + '%'  AS 'Selectivity'
FROM Book
GROUP BY Price
ORDER BY COUNT(BookId)

查詢優化器不太可能使用選擇性低於85%的索引。

何時使用聚集索引

聚集索引和非聚集索引特性的比較:

特性 聚集索引與非聚集索引對比
更快。因為不需要鍵查詢。如果需要的列包含中非聚集索引中,沒有區別。
更新 更慢。不僅是表記錄,所有非聚集索引也需要更新。
插入/刪除 更快。對於非聚集索引,在表中插入新記錄意味著在非聚集索引中也要插入新記錄。對於聚集索引,表就是索引的一部分,所以不需要二次插入。刪除記錄也是一樣的。
另一方面,當記錄不是插入表的非常後部,插入可能導致頁分割,這樣頁的一半內容就需要轉移到另一個頁上。非聚集索引上的頁分割可能性更低,因為它們的記錄更小。
當記錄插入到表的後部,不需要進行頁分割。
列大小 需要保持短小和快速。因為每一個非聚集索引都包含聚集索引值,進行鍵查詢。使用int型別要比使用nvarchar(50)要好得多。

如果多個列需要索引,最好將聚集索引改在主鍵上:

  • 讀:主鍵會包含在很多JOIN子句中,使得讀效能很重要。
  • 更新:主鍵不應該或很少更新,否則就需要更新外來鍵。
  • 插入/刪除:大多數情況上,會將主鍵設為標識列,這樣,每條記錄就分配了一個唯一的,自增長的數字。這樣,如果在主鍵上建立聚集索引,新記錄始終加到表的結尾。當記錄加到有聚集索引的表結尾,並且當前頁沒有空間時,新記錄儲存到新頁上,當前頁的資料依然儲存在當前頁上。這樣,就避免了昂貴的頁分割。
  • 大小:大多數情況下,主鍵是int型別。它是短而快的。

實際上,如果在SSMS表設計器中設定一列為主鍵,SSMS預設設定這列為聚集索引,除非其它列已經設定了聚集索引。

維護索引

以下方法可以保持索引效率:

  • 索引碎片整理。不斷地更新導致索引和表碎片增多,降低了效能。測量碎片程式,參考查明瓶頸節的碎片子節。
  • 保持統計更新。SQL Server維護統計資料以決定針對一個查詢是否使用索引。這些統計資料一般情況是自動更新的,但這個功能可以關閉。如果關閉了,確保統計資料是最新的。
  • 刪除未使用的索引。索引加速讀訪問,但減慢了更新。辨別未使用的索引,參考查明瓶頸節的缺失索引和昂貴查詢。

昂貴查詢

快取聚集查詢

聚集語句,例如COUNT和AVG是很昂貴的,因為它們需要訪問很多記錄。如果一個網頁需要聚集資料,考慮在一個表中快取聚集結果,而不是每一次頁面請求都重新查詢一次。例如,以下程式碼在Aggreates表中儲存一個COUNT聚集資料:

DECLARE @n int
SELECT @n = COUNT(*) FROM dbo.Book
UPDATE Aggregates SET BookCount = @n

當底層資料改變時,可以使用觸發器或儲存過程更新聚集資料。也可以使用SQL Server作業重新計算聚集結果。建立作業,參考:How to: Create a Transact-SQL Job Step (SQL Server Management Studio)  http://msdn.microsoft.com/en-us/library/ms187910.aspx

保持記錄短小

減少表記錄佔用的空間可以加速訪問。記錄在磁碟上儲存在8KB的頁中。一個頁中儲存的記錄越多,SQL Server獲取給定結果集需要讀取的頁就越少。

保持記錄短小的方法:

  • 使用短資料型別。如果值能放在1個位元組的TinyInt中,不要使用四個位元組的Int。如果只是儲存ASCII字元,使用varchar(n),每個字元使用一個位元組,不要使用nvarchar(n),使用兩個位元組儲存一個字元。如果儲存固定長度的字串,使用char(n)或nchar(n),不要使用varchar(n)或nvarchar(n),節省兩個位元組的長度空間。
  • 考慮使用行外儲存大的,很少使用的列。大物件欄位,例如nvarchar(max),varchar(max),varbinary(max),和XML欄位如果小於8000位元組通常存在行中,如果大於8000位元組就會中行中儲存一個2位元組的指標,指向行外區域。在行外儲存意味著訪問這個欄位至少需要讀取兩次,而不是一次。對於小得多的記錄,如果這個列很少訪問,也可以將它存放到行外。強制表中的大物件始終儲存在行外:使用
    EXEC sp_tableoption 'mytable', 'large value types out of row', '1'
  • 考慮垂直分割槽。如果表中一些列訪問的比其它列頻繁地多,把很少使用的列存放在另一張表中。訪問頻繁訪問的列會更快,代價是當訪問不經常訪問的列時,需要使用JOIN。
  • 避免重複列。例如,不要這樣做:

    這個方案不僅建立了長記錄,也使得更新書名很困難,一個作者也不能有多於兩本的書。將書名儲存在一張單獨的book表中,其中包括AuthorId列人心如向書的作者。
  • 避免重複值。例如,不要這樣做:

    使用者名稱和國家重複了。除了長記錄,更新作者資訊需要更新多個記錄,並且增加了不一致的風險。在單獨的表中儲存使用作者和書籍,在書記錄中儲存作者的主鍵。

考慮反規範化

反規範化是上節兩個觀點的對立—避免重複列和重複值。

問題是這些建議提升了更新速度,一致性和記錄大小,但使得資料分散在不同的表中,這意味著更多的JOIN。

例如,假設有100個地址分散在50個地址中,城市儲存在一個單獨的表中。這使得地址記錄更短,並且更新城市名稱更容易,但這也意味著每一次獲取地址都需要JOIN。如果城市名稱不太可能改變,並且獲取城市時,都會獲取地址的其它部分。那麼,在地址記錄中包括城市名稱會更好。這個解決方案包含了重複內容(城市名稱),但是,會少一次JOIN。

小心觸發器

觸發器是非常方便的。但它們隱藏在開發者的視野外,所以開發者可能沒有意識到觸發器的額外開銷。

保持觸發器的短小。它們執行中觸發它們的事務中。所以當觸發器執行時,持有鎖的那個事務會一直持有鎖。注意,即使沒有使用BIGIN TRAN顯式建立事務,每一個INSERT,UPDATE,或DELETE在操作期間建立它們自己的事務。

在決定使用哪些索引時,不要忘記和儲存過程和函式一樣,檢視觸發器。

對小而且臨時的結果集使用表變數

考慮在儲存過程中使用表變數代替臨時表。例如,不要這樣寫:

CREATE TABLE #temp (Id INT, Name nvarchar(100))
INSERT INTO #temp
...

可以這樣寫:

DECLARE @temp TABLE(Id INT, Name nvarchar(100))
INSERT INTO @temp
...

相對於臨時表,表變數有這些好處:

  • SQL Server更可能把它們儲存在記憶體中,而不是tempdb中。這意味著更少的通訊量和對tempdb的鎖。
  • 沒有事務日誌開銷。
  • 更少地儲存過程重編譯。

然而,它們也有缺點:

  • 在表變數建立後,不能加索引或約束。如果需要加索引,必須作為DECLARE語句的一部分:
    DECLARE @temp TABLE(Id INT primary key, Name nvarchar(100))
  • 當超過100條記錄後,表變數的效率要比臨時表低,因為不會為表變數建立統計資訊。使得查詢優化器建立優化的執行計劃更困難。

使用全文搜尋代替LIKE

你可能使用LIKE在文字列中搜索子串:

SELECT Title, Author FROM dbo.Book WHERE Title LIKE '%Quixote'

但是,除非查詢字串以常量文字開關,SQL Server不能使用列上的索引,就需要做全表掃描。

考慮使用SQL Server全文搜尋。這會為文字列中的所有單詞建立一個索引,這樣搜尋就會更快。使用全文搜尋,參考:

使用基於集合的程式碼代替遊標

考慮使用基於集合的程式碼代替遊標,這樣效能提高1000倍也是很常見的。基於集合的程式碼使用的內部演算法相比遊標,被極大的優化了。

更多資訊,訪問:

最小化SQL伺服器到Web伺服器的流量

不要使用SELECT *。這會返回所有的行。只返回需要的列。

如果網站只需要長文字的一部分,只發送這部分。例如:

SELECT LEFT(longtext, 100) AS excerpt FROM Articles WHERE ...

物件命名

儲存過程名不要以sp_開頭。SQL Server假設以sp_開頭的是系統儲存過程,即使以應用資料庫開頭,也會在master資料庫中查詢這些儲存過程。

物件名應該以schema所有都開頭。這樣會節省SQL Server辨別物件的時間,提高執行計劃重新性。例如:

SELECT Title, Author FROM dbo.Book

而不要使用

SELECT Title, Author FROM Book

使用SET NOCOUNT ON

在儲存過程和觸發器的開發加入命令SET NOCOUNT ON。會這禁止SQL Server在每個SQL語句後傳送影響的行數。

對超過1M的值使用FILESTREAM

在FILESTRAM列中儲存超過1M的BLOB型別的值。這會直接使用NTFS檔案系統儲存物件,而不使用資料庫資料檔案。實現方法:

WHERE子句中的列避免使用函式

WHERE子句中的列使用函式會使得SQL Server不使用這個列上的索引。

例如,下面的查詢:

SELECT Title, Author FROM dbo.Book WHERE LEFT(Title, 1)='D'

SQL Server不知道LEFT函式返回的值,所以只能掃描整張表,對Title列的每一個值執行LEFT函式。

但是,它知道如何處理LIKE。重寫查詢:

SELECT Title, Author FROM dbo.Book WHERE Title LIKE 'D%'

SQL Server現在可以使用Title上的索引,因為LIKE字串以文字常量開頭。

使用UNION ALL替代UNION

UNION子句合併兩個SELECT語句的結果集,從最終結果集中去除重複資料。這個操作很昂貴,它使用一張工作表,執行DISTINCT選擇實現這個功能。

如果不介意重複,或者知道不會有重複,使用UNION ALL。

如果優化器檢查到不會有重複,它會選擇UNION ALL,即使使用了UNION。例如,下面的語句永遠不會有重複的記錄,優化器會使用UNION替代UNION ALL:

SELECT BookId, Title, Author FROM dbo.Book WHERE Author LIKE 'J%'
UNION
SELECT BookId, Title, Author FROM dbo.Book WHERE Author LIKE 'M%'

使用EXISTS替代COUNT查詢重複記錄

如果需要檢查結果集中是否有記錄,不要使用COUNT:

DECLARE @n int
SELECT @n = COUNT(*) FROM dbo.Book
IF @n > 0
  print 'Records found'

這會讀整張表獲取記錄數量。使用EXISTS:

IF EXISTS(SELECT * FROM dbo.Book)
  print 'Records found'

這樣,SQL Server找到一條記錄後,就會停止讀取。

組合SELECT和UPDATE

有時候,需要SELECT和UPDATE同一條記錄。例如,需要在訪問記錄時,更新“LastAccessed”列。可以使用SELECT和UPDATE:

UPDATE dbo.Book
SET LastAccess = GETDATE()
WHERE BookId=@BookId
SELECT Title, Author
FROM dbo.Book
WHERE BookId=@BookId

但是,也可以組合SELECT到UPDATE中:

DECLARE @title nvarchar(50)
DECLARE @author nvarchar(50)
UPDATE dbo.Book
SET LastAccess = GETDATE(),
  @title = Title,
  @author = Author
WHERE BookId=@BookId
SELECT @title, @author

這可以節省一些時間,並且減少記錄持有的鎖的時間。

收集鎖詳細資訊

可以通過跟蹤SQL Server Profiler的“Blocked process report”事件查詢哪些查詢包含在嚴重的鎖延遲中。

這個事件的觸發發件是查詢的鎖等待時間超過“鎖程序閾值”。使用以下查詢設定這個閾值:

EXEC sp_configure 'show advanced options', 1
RECONFIGURE
EXEC sp_configure 'blocked process threshold', 30
RECONFIGURE

然後,在Profiler中開啟跟蹤:

  1. 開啟SQL Profiler。
  2. 在SQL Profiler,點選File | New Trace。
  3. 點選Events Selection選項卡。
  4. 選擇Show all events checkbox檢視所有事件。選擇Show all columns檢視所有資料列。
  5. 在主視窗中,展開Errors and Warnings,並選擇Blocked process report事件,確保TextData列中的複選框被選中。
  6. 如果需要跟蹤死鎖,展開Locks,選擇Deadlock graph事件。如果要得到死鎖的額外資訊,讓SQL Server將每個死鎖的資訊寫入到它的錯誤日誌中,執行:
    DBCC TRACEON(1222,-1)
  7. 反選其它選擇事件。
  8. 點選Run啟動跟蹤。
  9. 儲存模板,這樣下次就不需要重新建立。點選File | Save As | Trace Template。
  10. 一旦捕捉到資料後,點選File | Save儲存跟蹤資料到跟蹤檔案中,用於今後的分析。可以點選File | Open載入一個跟蹤檔案。

當在Profiler中點選一個Block process report時,可以在下面的視窗中看到事件的資訊,包括加鎖的查詢和被阻塞的查詢。使用同樣的方式可以得到死鎖圖的詳細資訊。

檢查SQL Server死鎖事件的錯誤日誌:

  1. 在SSMS中展開資料庫伺服器,展開Management並展開SQL Server Logs。雙擊一條日誌。
  2. 在日誌文件檢視器中,點選視窗頂部的Search,查詢“deadlock-list”。在死鎖列表事件後,可以找到死鎖包含的查詢語句的更多資訊。

減少鎖延遲

最有效的減少鎖延遲的方法是減少持有鎖的時間:

  • 優化查詢。查詢時間超短,持有鎖的時間超短。
  • 儲存過程優於臨時查詢。減少編譯執行計劃的時間和在網路上傳輸單個查詢的時間。
  • 如果必須使用遊標,頻繁提交更新。遊標處理要比集合處理慢得多。
  • 在持有鎖的時候不要處理長操作,例如傳送郵件。在開啟事務時,不要等待使用者輸入。使用樂觀鎖:

第二個減少鎖等待時間的方法是減少鎖住的資源:

  • 不要在頻繁更新的列上放置聚集索引。這會要求聚集索引和非聚集索引上都要鎖,因為它們的行上包含需要更新的值。
  • 考慮在非聚集索引上包括列。這會防止查詢讀表記錄,所以它不會阻塞其它查詢更新同一條記錄上不相關的列。
  • 考慮使用行版本控制。SQL Server的這個特性防止讀資料錶行的查詢阻塞更新相同行的查詢,或相反。更新相同行的查詢仍然相互阻塞。

    行版本控制在更新前將行儲存在臨時區域(tempdb資料庫),所以讀操作可以在進行更新的同時訪問臨時儲存的版本。這會產生額外的開銷用來維護行版本,在使用進行測試。並且,如果設定了事務的隔離級別,行版本控制只能工作在讀提交隔離模式下,這個模式是預設的模式。

    實現行版本控制,設定READ_COMMITTED_SNAPSHOT選項。當進行設定時,只能有一個連線開啟。可以通過將資料庫切換到單使用者模式。

    ALTER DATABASE mydatabase SET SINGLE_USER WITH ROLLBACK  
      IMMEDIATE;
    ALTER DATABASE mydatabase SET READ_COMMITTED_SNAPSHOT ON;
    ALTER DATABASE mydatabase SET MULTI_USER;

    檢查資料庫是否開啟了行版本控制,執行:

    select is_read_committed_snapshot_on
    from sys.databases 
    where name='mydatabase'

    最後,可以設定鎖超時時間。例如,中止等待時間超過5秒的語句:

    SET LOCK_TIMEOUT 5000

    使用1無限制等待。使用0不等待。

減少死鎖

死鎖是兩個事務都在等待對方釋放一個鎖。事務1有一個資源A的鎖,試圖獲得資源B的鎖,同時,事務2有一個資源B的鎖,試圖獲得資源A的鎖。現在,兩個事務都不能繼續。

一個減少死鎖的方法是減少鎖延遲,上節已經論述了。這會減少死鎖可能發生的時間窗。

第二個方法是始終以相同的順序鎖資源。如果事務2與事務1以相同的順序鎖資源(先A後B),那麼,事務2就不會在等待資源A前鎖住資源B,也就不會阻塞事務1了。

最後,小心使用HOLDLOCK或Repeatable Read或Serializable Read隔離級別。例如,以下程式碼:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN 
  SELECT Title FROM dbo.Book
  UPDATE dbo.Book SET Author='Charles Dickens' 
  WHERE Title='Oliver Twist'
COMMIT

假如有兩個事務同時執行這段程式碼。當它們執行SELECT時,都獲得了Book表中的行的選擇鎖。因為Repeatable Read隔離級別,它們都會持有鎖。現在,兩者都試圖請求Book表中的一行的更新鎖,以執行UPDATE。每一個事務現在都被另一個事務持有的選擇鎖阻塞了。

在SELECT語句中使用UPDLOCK防止這種情況。這會使得SELECT獲得更新鎖,這樣,只有一個事務可以執行SELECT。獲得鎖的事務可以執行UPDATE,然後釋放鎖,另一個事務也可以執行了。程式碼如下:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN 
  SELECT Title FROM dbo.Book WITH(UPDLOCK)
  UPDATE dbo.Book SET Author='Charles Dickens'  
  WHERE Title='Oliver Twist'
COMMIT

執行計劃重用

臨時查詢

考慮這個臨時查詢:

SELECT b.Title, a.AuthorName 
FROM dbo.Book b JOIN dbo.Author a ON b.LeadAuthorId=a.Authorid 
WHERE BookId=5

當SQL Server第一次接收到這個查詢時,它會編譯一個執行計劃,在計劃快取中儲存計劃,然後執行計劃。

如果SQL Server再一次接收到查詢,重用它執行計劃的條件是執行計劃還在計劃快取中,並且:

  • 查詢中的物件引用至少使用schema名稱限定。使用dbo.Book,不要使用Book。加上資料庫會更好。
  • 查詢文字精確匹配。匹配時是區分大小寫的,任何空白字元都會影響精確匹配。

作為第二個規則的結果,如果使用了相同的查詢,但不同的BookId,也不能匹配:

SELECT b.Title, a.AuthorName 
FROM dbo.Book b JOIN dbo.Author a ON b.LeadAuthorId=a.Authorid 
WHERE BookId=9 -- Doesn't match query above, uses 9 instead of 5

簡單引數化

為了使得臨時查詢重用快取的執行計劃容易一些,SQL Server支援簡單引數化。它會自動辨別查詢中的變數。因為這個功能很難做對,但是很容易做錯,SQL Server只會對單張表的非常簡單的查詢使用,例如

SELECT Title, Author FROM dbo.Book WHERE BookId=5

它可以使用以下查詢生成的執行計劃:

SELECT Title, Author FROM dbo.Book WHERE BookId=9

sp_executesql

為了不讓SQL Server去猜測查詢的哪一部分可以轉換為引數,可以使用系統儲存過程sp_executesql告訴SQL Server。呼叫sp_executesql的方式:

sp_executesql @query, @parameter_definitions, @parameter1, @parameter2, ...

例如:

EXEC sp_executesql 
  N'SELECT b.Title, a.AuthorName
  FROM dbo.Book b JOIN dbo.Author a ON b.LeadAuthorId=a.Authorid
  WHERE BookId=@BookId',
  N'@BookId int',
  @BookId=5

注意sp_executesql接收的前兩個引數是nvarchar值,所以需要使用以N為字首的字串。

儲存過程

除了向資料庫傳送單個的查詢,還可以將它們打包在一個儲存過程中,永久地儲存在資料庫中。這有以下好處:

  • 和sp_executesql一樣,儲存過程也允許顯式地定義引數,使得SQL Server更容易地重用執行計劃。
  • 儲存過程可以包含一系列查詢和T-SQL控制語句,例如IF…THEN。使用者只需要傳送儲存過程名和引數到伺服器,不需要傳送單獨的查詢語句,節省了網路開銷。
  • 儲存過程對網站程式碼隔離了資料庫細節。當表定義變化了,只需要更新一個或多個儲存過程,不需要修改網站。
  • 只允許通過儲存過程訪問資料庫,可以實現更好的安全性。這樣,可以允許使用者只訪問他們需要的資訊,而不能做計劃之外的操作。

建立儲存過程的程式碼:

CREATE PROCEDURE GetBook
  @BookId int
AS
BEGIN
  SET NOCOUNT ON;
  SELECT Title, Author FROM dbo.Book WHERE BookId=@BookId
END
GO

在開頭加入SET NOCOUNT ON,可以通過阻止SQL Server傳送儲存過程影響的行數的訊息,提高效能。

在查詢視窗執行儲存過程:

EXEC dbo.GetBook @BookId=5

或者,更簡單的方式

EXEC dbo.GetBook 5

在C#程式碼中使用儲存過程也很簡單:

string connectionString = "...";
using (SqlConnection connection = new SqlConnection(connectionString))
{
    string sql = "dbo.GetBook";
    using (SqlCommand cmd = new SqlCommand(sql, connection))
    {
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.Add(new SqlParameter("@BookId", bookId));
        connection.Open();
        // Execute database command ...
    }
}

阻止重用

有時,我們不想重用一個執行計劃。當編譯儲存過程的執行計劃時,計劃是基於那時使用的引數的。當計劃使用不同的引數重用時,使用第一組引數生成的計劃,在使用第二組引數時進行了重用。但是,我們並不希望這樣。

例如,考慮以下查詢:

SELECT SupplierName FROM dbo.Supplier WHERE City=@City

假設Supplier表在City列上有一個索引。假設Supplier中的一半記錄的City欄位值是“New York”。對於“New York”進行優化的執行計劃會使用全表掃描。但是,如果“San Diego”只有幾條記錄,對於“San Diego”的優化查詢計劃應該使用索引。對於一個引數好的計劃可能對於另一個計劃就是一個壞的計劃。如果使用次優查詢計劃的代價要比重新編譯查詢的代價高,最好是告訴SQL Server為每個查詢生成一個新計劃。

當建立儲存過程時,可以使用WITH RECOMPILE選項告訴SQL Server不要快取執行計劃。

CREATE PROCEDURE dbo.GetSupplierByCity
  @City nvarchar(100)
  WITH RECOMPILE
AS
BEGIN
...
END

也可以對於特定的執行過程生成一個新的計劃:

EXEC dbo.GetSupplierByCity 'New York' WITH RECOMPILE

最後,可以設定儲存過程在下次呼叫時重新編譯:

EXEC sp_recompile 'dbo.GetSupplierByCity'

設定使用某張表的儲存過程在下次呼叫時都重新編譯:

EXEC sp_recompile 'dbo.Book'

碎片

SQL Server提供兩種方法對錶和索引進行碎片整理:重建(rebuild)和重組(reorganize)。

索引重建

重建索引是對索引或表進行碎片整理最有效的方法。

ALTER INDEX myindex ON mytable REBUILD

這會使用更新頁方式物理地重建索引,最大限度地減少碎片。

如果重建的是聚集索引,實際上重建的是資料表,因為表是聚集索引的一部分。

重新一張表的所有索引:

ALTER INDEX ALL ON mytable REBUILD

索引重建有一個缺點是會阻塞所有訪問表和它的索引的查詢。它也可能阻塞所有正在訪問的查詢。可以使用ONLINE選擇減少這種情況:

ALTER INDEX myindex ON mytable REBUILD WITH (ONLINE=ON)

 

但是,這會導致重新時間更長。

另一個問題是重建是一個原子操作。如果有它完成前停止,所有已經完成的碎片整理工作都會丟失。

索引重組

與索引重建不同,索引重組不會阻塞表和它的索引,並且當它中途停止後,已完成的工作也不會丟失。但是,這是以降低效果為代價的。

重組索引,使用命令:

ALTER INDEX myindex ON mytable REORGANIZE

使用LOB_COMPACTION選項壓縮大物件(Large Object, LOB)資料,例如image、text、ntext、varchar(max)、nvarchar(max)、varbinary(max)和xml:

ALTER INDEX myindex ON mytable REORGANIZE WITH (LOB_COMPACTION=ON)

在一個繁忙的系統中,索引重組要比索引重建好更好。它不是原子性的,所以如果操作失敗了,不會導致所有的工作丟失。當它執行時,它只需要少量的持續較短的時間的鎖,而不是鎖住整張表和它的索引。如果發現一個頁正在使用,它只是跳過這個頁,並不再重試。

索引重組的缺點是它的效果更差,因為它會跳過頁,而且它不會建立新頁以達到更好地物理組織表或索引的目的。

堆表碎片整理

堆表是沒有聚集索引的表,因為沒有聚集索引,所以不能使用ALTER INDEX REBUILD或ALTER INDEX REORGANIZE進行碎片整理。

堆表碎片不是個大問題,因為表中的記錄根本就是無序的。當插入記錄時,SQL Server檢查表中是否還有空間,如果有,在那兒插入記錄。如果總是插入記錄,而更新或刪除記錄,所有記錄都會寫在表的結尾。如果更新或刪除記錄,堆表中就依然可能有間隙。

因為堆表碎片整理通常不是個問題,所以這裡不討論。但是,也有一些方法:

  • 建立一個聚集索引,然後刪除它。
  • 將堆表中的記錄插入到一個新表中。
  • 匯出資料,truncate表,再導回資料到那張表中。

記憶體

緩解記憶體壓力最常用的方法:

  • 增加實體記憶體。
  • 增加分配給SQL Server的記憶體。檢視當前分配的記憶體,執行:
    EXEC sp_configure 'show advanced option', '1'
    RECONFIGURE
    EXEC sp_configure 'max server memory (MB)'

    如果伺服器上的實體記憶體更多,增加分配。例如,增加到3000MB,執行:

    EXEC sp_configure 'show advanced option', '1'
    RECONFIGURE
    EXEC sp_configure 'max server memory (MB)', 3000
    RECONFIGURE WITH OVERRIDE

    不要分配所有的實體記憶體。留幾百MB給作業系統和其它軟體。

  • 減少從磁碟讀取的資料。從磁碟讀取的每一頁都需要儲存在記憶體中,並在記憶體中處理。全表掃描、聚集查詢和表連線都會讀取大量的資料。參考查明瓶頸的索引缺失和昂貴查詢小節,減少從磁碟讀取的資料。

  • 儘量重用執行計劃,減少計劃快取需要的記憶體。參考查明瓶頸的執行計劃重用小節。

磁碟

一些減少磁碟系統壓力的常用方法:

  • 優化查詢處理。
  • 將日誌檔案移動到一個專用的物理磁碟上。
  • 減少NTFS檔案系統的碎片。
  • 移動tempdb資料庫到專用磁碟上。
  • 將資料分散到兩個或多個磁碟上,分散負載。
  • 移動負載大的資料庫物件到另一個磁碟上。
  • 使用正確的RAID配置

優化查詢處理

確保正確的索引和優化最昂貴的查詢。

將日誌檔案移動到一個專用的物理磁碟上

移動磁碟的讀/寫頭是相同較慢的過程。日誌檔案是順序寫的,它本身需要很少的磁碟頭移動。但是如果日誌檔案和資料檔案在同一磁碟上,是沒有用的,因為磁碟頭必須在日誌文字和資料檔案間移動。

如果將日誌檔案放在它自己的磁碟上,那個磁碟上磁碟頭移動會很小,結果是更快的訪問日誌檔案。修改操作,例如更新、插入和刪除等修改操作會更快。

移動一個已存在的資料庫的日誌檔案到另一個磁碟,首先分離資料庫,移動日誌檔案到專用磁碟。然後重新附加資料庫,指定日誌檔案的新位置。

減少NTFS檔案系統的碎片

如果NTFS資料庫檔案有碎片了,磁碟頭在讀檔案時,必須不停地移動磁碟頭。為了減少碎片,為資料庫和日誌檔案設定一個比較大的初始檔案大小和較大的增長大小。設定足夠大,保證檔案不會增長到那麼大,會更好。這樣做的目的就是避免檔案增長和收縮。

如果需要增長和收縮資料庫或日誌檔案,考慮使用64-KB的NTFS簇大小,以匹配SQL Server讀的模式。

移動tempdb資料庫到專用磁碟上

tempdb用來排序、子查詢、臨時表、聚集、遊標等。它可能非常繁忙。這使得將它移動到它專屬的磁碟或不是很忙的磁碟會比較好。檢查伺服器上的tempdb和其它資料庫的資料庫和日誌檔案的活動資訊,使用 dm_io_virtual_file_stats DMV:

SELECT d.name, mf.physical_name, mf.type_desc, vfs.*
FROM sys.dm_io_virtual_file_stats(NULL,NULL) vfs
JOIN sys.databases d ON vfs.database_id = d.database_id 
JOIN sys.master_files mf ON mf.database_id=vfs.database_id AND 
mf.file_id=vfs.file_id

移動tempdb資料和日誌檔案到G:盤,設定它們大小為10MB和1MB,執行並重啟伺服器:

ALTER DATABASE tempdb MODIFY FILE (NAME = tempdev, FILENAME = 'G:\
tempdb.mdf', SIZE = 10MB) 
GO
ALTER DATABASE tempdb MODIFY FILE (NAME = templog, FILENAME = 'G:\
templog.ldf', SIZE = 1MB) 
GO

為了減少碎片,防止tempdb資料和日誌增長和收縮,可以給它們可能需要的最大空間。

將資料分散到兩個或多個磁碟上

增加檔案到資料庫的PRIMARY檔案組。SQL Server會將資料分散到已存在的和新檔案。將新檔案放到新磁碟或負載不是很大的磁碟。如果可以,設定初始大小足夠大,這會減少碎片。

例如,為資料uneUp資料在G:盤上增加一個初始大小為20GB的檔案,執行:

ALTER DATABASE TuneUp 
ADD FILE (NAME = TuneUp_2, FILENAME = N'G:\TuneUp_2.ndf', SIZE = 20GB)

注意副檔名.ndf,這是推薦的第二檔案的副檔名。

移動負載大的資料庫物件到另一個磁碟上

你可以移動負載大的資料物件,例如索引,到一個新磁碟,或不太繁忙的磁碟上。使用查明瓶頸的索引缺失和昂貴查詢小節介紹的dm_db_index_usage_stats DMV可以檢視每個索引上執行的讀和寫的數量。

如果伺服器有多個磁碟,在查明瓶頸的磁碟小節有度量磁碟使用情況的方法。使用這個資訊決定物件移動到哪個磁碟。

將索引移動到另一個磁碟,首先建立一個新的使用者自定義檔案組。例如,以下語句建立檔案組FG2:

ALTER DATABASE TuneUp ADD FILEGROUP FG2

然後,在檔案組中加入檔案:

ALTER DATABASE TuneUp 
ADD FILE (NAME = TuneUp_Fg2, FILENAME = N'G:\TuneUp_Fg2.ndf', SIZE = 200MB)
TO FILEGROUP FG2

最後,移動物件到檔案組中,例如,將表Book的Title列上的非聚集索引IX_Title移動到檔案組FG2中:

CREATE NONCLUSTERED INDEX [IX_Title] ON [dbo].[Book]([Title] ASC) WITH DROP_EXISTING ON FG2

可以分配多個物件給一個檔案組。可以在一個檔案組中加入多個檔案,這將允許將一個非常繁忙的表或索引分散到多個磁碟。

將表和它們的非聚集索引到不同的磁碟,這樣一個任務可以讀索引,另一個任務可以在表中進行鍵查詢。

使用正確的RAID配置

為了提高效能和容錯性,很多資料庫伺服器使用RAID子系統替代單獨的驅動器。RAID子系統有不同的配置。為資料檔案、日誌檔案和tempdb檔案選擇正確的配置可能極大地影響效能。

最常用的RAID配置是:

RAID配置 描述
RAID 0 Each fle is spread ("striped") over each disk in the array. When reading or writing a fle, all disks are accessed in parallel, leading to high transfer rates.
RAID 5 Each file is striped over all disks. Parity information for each disk is stored on the other disks, providing fault tolerance. File writes are slow—a single fle write requires 1 data read + 1 parity read + 1 data write + 1 parity write = 4 accesses.
RAID 10 Each fle is striped over half the disks. Those disks are mirrored by the other half, providing excellent fault tolerance. A fle write requires 1 data write to a main disk + 1 data write to a mirror disk.
RAID 1 This is RAID 10 but with just 2 disks, a main disk and a mirror disk. That gives you fault tolerance but no striping.

下表是RAID與單個磁碟效能比較,N表示磁碟陣列中的磁碟個數:

  讀速度 寫速度 容錯
單個磁碟 1 1 no
RAID 0 N N no
RAID 5 N N/4 yes
RAID 10 N N/2 yes
RAID 1 2 1 yes

下表是對tempdb、資料和日誌檔案合理的RAID配置:

檔案 效能相關屬性 建議的RAID配置
tempdb Requires good read and write performance for random access. Relatively small. Losing temporary data may be acceptable. RAID 0, RAID 1, RAID 10
log Requires very good write performance, and fault tolerance. Uses sequential access, so striping is no beneft. RAID 1, RAID 10

data (writes make up less than 10 percent of accesses)

Requires fault tolerance. Random access means striping is benefcial. Large data volume. RAID 5, RAID 10

data (writes make up over 10 percent of accesses)

Same as above, plus good write performance. RAID 10

有電池後備電源快取的RAID控制器能很大地提高寫效能,因為這允許SQL Server將寫請求交付給快取,需要等待物理磁碟訪問完成。控制器在後臺執行快取的寫請求。

CPU

解決處理器瓶頸的一般方法包括:

  • 優化CPU密集查詢。在查明瓶頸的索引缺失和昂貴查詢小節中可以找到最昂貴查詢的方法。DMV可以列出每個查詢的CPU使用率。
  • 建立查詢計劃是高CPU密集的。提高執行計劃重用。
  • 安裝更多更快的處理器、L2/L3快取,或更有效的驅動器。

轉自:https://www.cnblogs.com/ntwo/archive/2010/12/15/1907040.html