Sqlserver 性能優化

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

查明瓶頸

缺少索引和昂貴查詢

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

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

缺少索引

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

1 select d.name AS DatabaseName, mid.*
2 from sys.dm_db_missing_index_details mid
3 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. 查看系統中的跟蹤狀態,在查詢窗口執行命令:
    1 select * from ::fn_trace_getinfo(default)

    查找執行的trace ID行中property列爲5的行。如果這行的value列值是1,跟蹤正在運行。trace ID爲1的跟蹤的系統跟蹤。
  13. 跟蹤一段時間後,假設trace ID是2,運行以下命令停止跟蹤:
    1 exec sp_trace_setstatus 2,0<br>重啓跟蹤,運行:<br><pre class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: true; light: false; ruler: true; smart-tabs: true; tab-size: 4; toolbar: true;">exec sp_trace_setstatus 2,1</pre>
  14. 1 停止並關閉跟蹤,這樣才能訪問跟蹤文件,運行:<br><pre class="brush: csharp; auto-links: true; collapse: false; first-line: 1; gutter: true; html-script: true; light: false; ruler: true; smart-tabs: true; tab-size: 4; toolbar: true;">exec sp_trace_setstatus 2,0
    2 exec sp_trace_setstatus 2,2</pre>

運行Database Engine Tuning Advisor:

      1 <ol><li><p>選擇Start | Programs | Microsoft SQL Server 2008 | Performance Tools | Database Engine Tuning Advisor。</p></li><li><p>在Workload區域,選擇trace文件。在Database for workload analysis下拉框,選擇需要分析的第一個數據庫。</p></li><li><p>在Select databases and table to tune,選擇需要索引建議的數據庫。</p></li><li><p>如果跟蹤文件很大,Database Engine Tuning Advisor會花費很長時間進行分析。在Tuning Options選項頁,可以選擇何時停止分析。</p></li><li><p>點擊Start Analysis按鍵開始分析。</p></li></ol>

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

      昂貴查詢

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

      可以通過使用DMV dm_exec_query_stats可以獲取更多的信息。當查詢優化器爲查詢創建執行計劃時,它會緩存計劃進行重用。每次使用查詢計劃執行查詢時,性能統計會被保留。可以使用dm_exec_query_stats查看這些統計。

      01 SELECT
      02   est.text AS batchtext,
      03   SUBSTRING(est.text, (eqs.statement_start_offset/2)+1, 
      04     (CASE eqs.statement_end_offset WHEN -1
      05     THEN DATALENGTH(est.text)
      06     ELSE eqs.statement_end_offset END -
      07     ((eqs.statement_start_offset/2) + 1))) AS querytext,
      08   eqs.creation_time, eqs.last_execution_time, eqs.execution_count,
      09   eqs.total_worker_time, eqs.last_worker_time,
      10   eqs.min_worker_time, eqs.max_worker_time,
      11   eqs.total_physical_reads, eqs.last_physical_reads,
      12   eqs.min_physical_reads, eqs.max_physical_reads,
      13   eqs.total_elapsed_time, eqs.last_elapsed_time,
      14   eqs.min_elapsed_time, eqs.max_elapsed_time,
      15   eqs.total_logical_writes, eqs.last_logical_writes,
      16   eqs.min_logical_writes, eqs.max_logical_writes,
      17   eqs.query_plan_hash
      18 FROM
      19   sys.dm_exec_query_stats AS eqs
      20   CROSS APPLY sys.dm_exec_sql_text(eqs.sql_handle) AS est
      21 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:

      1 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可以獲得每個索引的使用信息:

      1 SELECT d.name, t.name, i.name, ius.*
      2 FROM sys.dm_db_index_usage_stats ius
      3 JOIN sys.databases d ON d.database_id = ius.database_id
      4 JOIN sys.tables t ON t.object_id = ius.object_id
      5 JOIN sys.indexes i ON i.object_id = ius.object_id AND i.index_id =
      6 ius.index_id
      7 ORDER BY user_updates DESC

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

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

      1 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顯示上次服務器重啓後查詢優化次數和平均時間(單位秒)。

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

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

      sys.dm_exec_cached_plans

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

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

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

      碎片

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

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

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

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

      01 DECLARE @DatabaseName sysname
      02 SET @DatabaseName = 'mydatabase' --use your own database name
      03 SELECT o.name AS TableName, i.name AS IndexName, ips.index_type_desc,
      04   ips.avg_fragmentation_in_percent, ips.page_count, ips.fragment_count,
      05   ips.avg_page_space_used_in_percent
      06 FROM sys.dm_db_index_physical_stats(
      07   DB_ID(@DatabaseName),
      08   NULL, NULL, NULL, 'Sampled') ips
      09 JOIN sys.objects o ON ips.object_id = o.object_id
      10 JOIN sys.indexes i ON (ips.object_id = i.object_id) AND (ips.index_id = i.index_id)
      11 WHERE (ips.page_count >= 7) AND (ips.avg_fragmentation_in_percent > 20)
      12 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可能壓力過大。

      修復瓶頸

      缺失索引

      聚集索引

      考察這張表:

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

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

      2010-12-08 15 42 41

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

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

      1 SET STATISTICS IO ON
      2 SET STATISTICS TIME ON

      清空內存緩存:

      1 CHECKPOINT
      2 DBCC DROPCLEANBUFFERS

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

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

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

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

      現在,加入聚集索引:

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

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

      2010-12-08 15 52 28

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

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

      非聚集索引

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

      1 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,就可以使用聚集索引得到真正的表記錄。這個方法中的第二步驟稱作鍵查找。

      2010-12-09 10 56 28

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

      查看非聚集索引的效果,首先創建它:

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

      運行查詢:

      1 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訪問表記錄了,跳過了鍵查找步驟。

      2010-12-09 13 34 43

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

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

      現在再運行查詢:

      1 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子句不能使用索引,因爲函數輸出不在索引中。例如:
          1 SELECT Title, Author FROM dbo.Book WHERE LEFT(Title, 3) = 'Don'
          在Title列中放置索引不會使這個查詢更快。
        • 如果在WHERE子句中使用查詢字符串開關是通配符的LIKE語句,SQL Server不會使用索引:
          1 SELECT Title, Author FROM dbo.Book WHERE Title LIKE '%Quixote'
          但是如果查詢字符串是以文本常量開頭的,能夠使用索引:
          1 SELECT Title, Author FROM dbo.Book WHERE Title LIKE 'Don%'
      • 1 考慮使用有唯一約束的列。這會有助於檢查新值是否是唯一的。
      • 1 MIN和MAX函數可以從索引中獲益,因爲值是排序的,就不需要查找整張表確定最大或最小值。
      • 1 使用佔用很多空間的字段創建索引要三思。如果是非聚集索引,列值會在索引中重複存儲。如果是聚集索引,列值會在所有非聚集索引中重複存儲。這會增加索引記錄的大小,這樣,在8KB頁上只能存放更少的索引記錄,這會使得SQL Server讀取更多的頁。

      何時不使用索引

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

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

      列經常更新

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

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

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

      低特殊性

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

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

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

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

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

      01 DECLARE @c real
      02 SELECT @c = CAST(COUNT(*) AS real) FROM Book
      03 SELECT
      04   Price,
      05   COUNT(BookId) AS 'Number of rows',
      06   CAST((1 - (100 * COUNT(BookId) / @c))
      07     AS nvarchar(20)) + '%'  AS 'Selectivity'
      08 FROM Book
      09 GROUP BY Price
      10 ORDER BY COUNT(BookId)

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

      何時使用聚集索引

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

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

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

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

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

      維護索引

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

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

      昂貴查詢

      緩存聚集查詢

      聚集語句,例如COUNT和AVG是很昂貴的,因爲它們需要訪問很多記錄。如果一個網頁需要聚集數據,考慮在一個表中緩存聚集結果,而不是每一次頁面請求都重新查詢一次。例如,以下代碼在Aggreates表中保存一個COUNT聚集數據:

      1 DECLARE @n int
      2 SELECT @n = COUNT(*) FROM dbo.Book
      3 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字節的指針,指向行外區域。在行外存儲意味着訪問這個字段至少需要讀取兩次,而不是一次。對於小得多的記錄,如果這個列很少訪問,也可以將它存放到行外。強制表中的大對象始終保存在行外:使用
        1 EXEC sp_tableoption 'mytable', 'large value types out of row', '1'
      • 1 考慮垂直分區。如果表中一些列訪問的比其它列頻繁地多,把很少使用的列存放在另一張表中。訪問頻繁訪問的列會更快,代價是當訪問不經常訪問的列時,需要使用JOIN。
      • 1 避免重複列。例如,不要這樣做:<br><a href="http://images.cnblogs.com/cnblogs_com/ntwo/201012/201012151716519364.png"><imgstyle="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="2010-12-10 13 05 31" border="0" alt="2010-12-10 13 05 31"src="http://images.cnblogs.com/cnblogs_com/ntwo/201012/201012151716525362.png" width="691" height="113"></a><br>這個方案不僅創建了長記錄,也使得更新書名很困難,一個作者也不能有多於兩本的書。將書名存儲在一張單獨的book表中,其中包括AuthorId列人心如向書的作者。
      • 1 避免重複值。例如,不要這樣做:<br><a href="http://images.cnblogs.com/cnblogs_com/ntwo/201012/20101215171652312.png"><imgstyle="background-image: none; border-bottom: 0px; border-left: 0px; padding-left: 0px; padding-right: 0px; display: inline; border-top: 0px; border-right: 0px; padding-top: 0px" title="2010-12-10 13 09 13" border="0" alt="2010-12-10 13 09 13"src="http://images.cnblogs.com/cnblogs_com/ntwo/201012/201012151716535578.png" width="577" height="171"></a><br>用戶名和國家重複了。除了長記錄,更新作者信息需要更新多個記錄,並且增加了不一致的風險。在單獨的表中存儲使用作者和書籍,在書記錄中保存作者的主鍵。

      考慮反規範化

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

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

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

      小心觸發器

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

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

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

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

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

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

      可以這樣寫:

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

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

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

      然而,它們也有缺點:

      • 在表變量創建後,不能加索引或約束。如果需要加索引,必須作爲DECLARE語句的一部分:
        1 DECLARE @temp TABLE(Id INT primary key, Name nvarchar(100))
      • 1 <font face="Verdana">當超過100條記錄後,表變量的效率要比臨時表低,因爲不會爲表變量創建統計信息。使得查詢優化器創建優化的執行計劃更困難。</font>

      使用全文搜索代替LIKE

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

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

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

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

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

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

      更多信息,訪問:

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

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

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

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

      對象命名

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

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

      1 SELECT Title, Author FROM dbo.Book

      而不要使用

      1 SELECT Title, Author FROM Book

      使用SET NOCOUNT ON

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

      對超過1M的值使用FILESTREAM

      在FILESTRAM列中存儲超過1M的BLOB類型的值。這會直接使用NTFS文件系統存儲對象,而不使用數據庫數據文件。實現方法:

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

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

      例如,下面的查詢:

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

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

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

      1 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:

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

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

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

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

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

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

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

      組合SELECT和UPDATE

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

      1 UPDATE dbo.Book
      2 SET LastAccess = GETDATE()
      3 WHERE BookId=@BookId
      4 SELECT Title, Author
      5 FROM dbo.Book
      6 WHERE BookId=@BookId

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

      1 DECLARE @title nvarchar(50)
      2 DECLARE @author nvarchar(50)
      3 UPDATE dbo.Book
      4 SET LastAccess = GETDATE(),
      5   @title = Title,
      6   @author = Author
      7 WHERE BookId=@BookId
      8 SELECT @title, @author

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

      收集鎖詳細信息

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

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

      1 EXEC sp_configure 'show advanced options', 1
      2 RECONFIGURE
      3 EXEC sp_configure 'blocked process threshold', 30
      4 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將每個死鎖的信息寫入到它的錯誤日誌中,執行:
        1 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選項。當進行設置時,只能有一個連接打開。可以通過將數據庫切換到單用戶模式。

        1 ALTER DATABASE mydatabase SET SINGLE_USER WITH ROLLBACK 
        2   IMMEDIATE;
        3 ALTER DATABASE mydatabase SET READ_COMMITTED_SNAPSHOT ON;
        4 ALTER DATABASE mydatabase SET MULTI_USER;

        檢查數據庫是否開啓了行版本控制,運行:

        1 select is_read_committed_snapshot_on
        2 from sys.databases
        3 where name='mydatabase'

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

        1 SET LOCK_TIMEOUT 5000

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

      減少死鎖

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

      2010-12-14 13 08 34

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

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

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

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

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

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

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

      執行計劃重用

      臨時查詢

      考慮這個臨時查詢:

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

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

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

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

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

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

      簡單參數化

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

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

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

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

      sp_executesql

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

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

      例如:

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

      注意sp_executesql接收的前兩個參數是nvarchar值,所以需要使用以N爲前綴的字符串。

      存儲過程

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

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

      創建存儲過程的代碼:

      1 CREATE PROCEDURE GetBook
      2   @BookId int
      3 AS
      4 BEGIN
      5   SET NOCOUNT ON;
      6   SELECT Title, Author FROM dbo.Book WHERE BookId=@BookId
      7 END
      8 GO

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

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

      1 EXEC dbo.GetBook @BookId=5

      或者,更簡單的方式

      1 EXEC dbo.GetBook 5

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

      01 string connectionString = "...";
      02 using (SqlConnection connection = new SqlConnection(connectionString))
      03 {
      04     string sql = "dbo.GetBook";
      05     using (SqlCommand cmd = new SqlCommand(sql, connection))
      06     {
      07         cmd.CommandType = CommandType.StoredProcedure;
      08         cmd.Parameters.Add(new SqlParameter("@BookId", bookId));
      09         connection.Open();
      10         // Execute database command ...
      11     }
      12 }

      阻止重用

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

      例如,考慮以下查詢:

      1 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不要緩存執行計劃。

      1 CREATE PROCEDURE dbo.GetSupplierByCity
      2   @City nvarchar(100)
      3   WITH RECOMPILE
      4 AS
      5 BEGIN
      6 ...
      7 END

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

      1 EXEC dbo.GetSupplierByCity 'New York' WITH RECOMPILE

      最後,可以設置存儲過程在下次調用時重新編譯:

      1 EXEC sp_recompile 'dbo.GetSupplierByCity'

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

      1 EXEC sp_recompile 'dbo.Book'

      碎片

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

      索引重建

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

      1 ALTER INDEX myindex ON mytable REBUILD

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

      如果重建的是聚集索引,實際上重建的是數據表,因爲表是聚集索引的一部分。

      重新一張表的所有索引:

      1 ALTER INDEX ALL ON mytable REBUILD

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

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

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

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

      索引重組

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

      重組索引,使用命令:

      1 ALTER INDEX myindex ON mytable REORGANIZE

      使用LOB_COMPACTION選項壓縮大對象(Large Object, LOB)數據,例如image、text、ntext、varchar(max)、nvarchar(max)、varbinary(max)和xml:

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

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

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

      堆表碎片整理

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

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

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

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

      內存

      緩解內存壓力最常用的方法:

      • 增加物理內存。
      • 增加分配給SQL Server的內存。查看當前分配的內存,運行:
        1 EXEC sp_configure 'show advanced option', '1'
        2 RECONFIGURE
        3 EXEC sp_configure 'max server memory (MB)'

        如果服務器上的物理內存更多,增加分配。例如,增加到3000MB,運行:

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

        不要分配所有的物理內存。留幾百MB給操作系統和其它軟件。

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

      • 儘量重用執行計劃,減少計劃緩存需要的內存。參考查明瓶頸的執行計劃重用小節。

      磁盤

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

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

      優化查詢處理

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

      將日誌文件移動到一個專用的物理磁盤上

      移動磁盤的讀/寫頭是相同較慢的過程。日誌文件是順序寫的,它本身需要很少的磁盤頭移動。但是如果日誌文件和數據文件在同一磁盤上,是沒有用的,因爲磁盤頭必須在日誌文本和數據文件間移動。

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

      移動一個已存在的數據庫的日誌文件到另一個磁盤,首先分離數據庫,移動日誌文件到專用磁盤。然後重新附加數據庫,指定日誌文件的新位置。

      減少NTFS文件系統的碎片

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

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

      移動tempdb數據庫到專用磁盤上

      tempdb用來排序、子查詢、臨時表、聚集、遊標等。它可能非常繁忙。這使得將它移動到它專屬的磁盤或不是很忙的磁盤會比較好。檢查服務器上的tempdb和其它數據庫的數據庫和日誌文件的活動信息,使用 dm_io_virtual_file_stats DMV:

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

      移動tempdb數據和日誌文件到G:盤,設置它們大小爲10MB和1MB,運行並重啓服務器:

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

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

      將數據分散到兩個或多個磁盤上

      增加文件到數據庫的PRIMARY文件組。SQL Server會將數據分散到已存在的和新文件。將新文件放到新磁盤或負載不是很大的磁盤。如果可以,設置初始大小足夠大,這會減少碎片。

      例如,爲數據uneUp數據在G:盤上增加一個初始大小爲20GB的文件,運行:

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

      注意文件擴展名.ndf,這是推薦的第二文件的擴展名。

      移動負載大的數據庫對象到另一個磁盤上

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

      如果服務器有多個磁盤,在查明瓶頸的磁盤小節有度量磁盤使用情況的方法。使用這個信息決定對象移動到哪個磁盤。

      將索引移動到另一個磁盤,首先創建一個新的用戶自定義文件組。例如,以下語句創建文件組FG2:

      1 ALTER DATABASE TuneUp ADD FILEGROUP FG2

      然後,在文件組中加入文件:

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

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

      1 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緩存,或更有效的驅動器。
      發表評論
      所有評論
      還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
      相關文章