sql语句性能优化

 

故事开篇:你和你的团队通过不懈努力,终于使网站成功上线,刚开始时,注册用户较少,网站性能表现不错,但随着注册用户的增多,访问速度开始变慢,一些用户开始发来邮件表示抗议,事情变得愈来愈糟,为了留住用户,你开始着手调查访问变慢的缘由。html

  通过紧张的调查,你发现问题出在数据库上,当应用程序尝试访问/更新数据时,数据库执行得至关慢,再次深刻调查数据库后,你发现数据库表增加得很大,有些表甚至有上千万行数据,测试团队开始在生产数据库上测试,发现订单提交过程须要花5分钟时间,但在网站上线前的测试中,提交一次订单只须要2/3秒。程序员

  相似这种故事在世界各个角落天天都会上演,几乎每一个开发人员在其开发生涯中都会遇到这种事情,我也曾屡次遇到这种状况,所以我但愿将我解决这种问题的经验和你们分享。数据库

  若是你正身处这种项目,逃避不是办法,只有勇敢地去面对现实。首先,我认为你的应用程序中必定没有写数据访问程序,我将在这个系列的文章中介绍如何编写最佳的数据访问程序,以及如何优化现有的数据访问程序。后端

  范围浏览器

  在正式开始以前,有必要澄清一下本系列文章的写做边界,我想谈的是“事务性(OLTP)SQL Server数据库中的数据访问性能优化”,但文中介绍的这些技巧也能够用于其它数据库平台。缓存

  同时,我介绍的这些技巧主要是面向程序开发人员的,虽然DBA也是优化数据库的一支主要力量,但DBA使用的优化方法不在个人讨论范围以内。性能优化

  当一个基于数据库的应用程序运行起来很慢时,90%的可能都是因为数据访问程序的问题,要么是没有优化,要么是没有按最佳方法编写代码,所以你须要审查和优化你的数据访问/处理程序。服务器

  我将会谈到10个步骤来优化数据访问程序,先从最基本的索引提及吧!网络

  第一步:应用正确的索引并发

  我之因此先从索引谈起是由于采用正确的索引会使生产系统的性能获得质的提高,另外一个缘由是建立或修改索引是在数据库上进行的,不会涉及到修改程序,并能够当即见到成效。

  咱们仍是温习一下索引的基础知识吧,我相信你已经知道什么是索引了,但我见到不少人都还不是很明白,我先给你们将一个故事吧。

  好久之前,在一个古城的的大图书馆中珍藏有成千上万本书籍,但书架上的书没有按任何顺序摆放,所以每当有人询问某本书时,图书管理员只有挨个寻找,每一次都要花费大量的时间。

  [这就比如数据表没有主键同样,搜索表中的数据时,数据库引擎必须进行全表扫描,效率极其低下。]

  更糟的是图书馆的图书愈来愈多,图书管理员的工做变得异常痛苦,有一天来了一个聪明的小伙子,他看到图书管理员的痛苦工做后,想出了一个办法,他建议将每本书都编上号,而后按编号放到书架上,若是有人指定了图书编号,那么图书管理员很快就能够找到它的位置了。

  [给图书编号就象给表建立主键同样,建立主键时,会建立汇集索引树,表中的全部行会在文件系统上根据主键值进行物理排序,当查询表中任一行时,数据库首先使用汇集索引树找到对应的数据页(就象首先找到书架同样),而后在数据页中根据主键键值找到目标行(就象找到书架上的书同样)。]

  因而图书管理员开始给图书编号,而后根据编号将书放到书架上,为此他花了整整一天时间,但最后通过测试,他发现找书的效率大大提升了。

  [在一个表上只能建立一个汇集索引,就象书只能按一种规则摆放同样。]

  但问题并未彻底解决,由于不少人记不住书的编号,只记得书的名字,图书管理员无赖又只有扫描全部的图书编号挨个寻找,但此次他只花了20分钟,之前未给图书编号时要花2-3小时,但与根据图书编号查找图书相比,时间仍是太长了,所以他向那个聪明的小伙子求助。

  [这就好像你给Product表增长了主键ProductID,但除此以外没有创建其它索引,当使用Product Name进行检索时,数据库引擎又只要进行全表扫描,逐个寻找了。]

  聪明的小伙告诉图书管理员,以前已经建立好了图书编号,如今只须要再建立一个索引或目录,将图书名称和对应的编号一块儿存储起来,但这一次是按图书名称进行排序,若是有人想找“Database Management System”一书,你只须要跳到“D”开头的目录,而后按照编号就能够找到图书了。

  因而图书管理员兴奋地花了几个小时建立了一个“图书名称”目录,通过测试,如今找一本书的时间缩短到1分钟了(其中30秒用于从“图书名称”目录中查找编号,另外根据编号查找图书用了30秒)。

  图书管理员开始了新的思考,读者可能还会根据图书的其它属性来找书,如做者,因而他用一样的办法为做者也建立了目录,如今能够根据图书编号,书名和做者在1分钟内查找任何图书了,图书管理员的工做变得轻松了,故事也到此结束。

  到此,我相信你已经彻底理解了索引的真正含义。假设咱们有一个Products表,建立了一个汇集索引(根据表的主键自动建立的),咱们还须要在ProductName列上建立一个非汇集索引,建立非汇集索引时,数据库引擎会为非汇集索引自动建立一个索引树(就象故事中的“图书名称”目录同样),产品名称会存储在索引页中,每一个索引页包括必定范围的产品名称和它们对应的主键键值,当使用产品名称进行检索时,数据库引擎首先会根据产品名称查找非汇集索引树查出主键键值,而后使用主键键值查找汇集索引树找到最终的产品。

  下图显示了一个索引树的结构

  图 1 索引树结构

  它叫作B+树(或平衡树),中间节点包含值的范围,指引SQL引擎应该在哪里去查找特定的索引值,叶子节点包含真正的索引值,若是这是一个汇集索引树,叶子节点就是物理数据页,若是这是一个非汇集索引树,叶子节点包含索引值和汇集索引键(数据库引擎使用它在汇集索引树中查找对应的行)。

  一般,在索引树中查找目标值,而后跳到真实的行,这个过程是花不了什么时间的,所以索引通常会提升数据检索速度。下面的步骤将有助于你正确应用索引。

  确保每一个表都有主键

  这样能够确保每一个表都有汇集索引(表在磁盘上的物理存储是按照主键顺序排列的),使用主键检索表中的数据,或在主键字段上进行排序,或在where子句中指定任意范围的主键键值时,其速度都是很是快的。

  在下面这些列上建立非汇集索引:

  1)搜索时常用到的;

  2)用于链接其它表的;

  3)用于外键字段的;

  4)高选中性的;

  5)ORDER BY子句使用到的;

  6)XML类型。

  下面是一个建立索引的例子: 

CREATEINDEX
  NCLIX_OrderDetails_ProductID ON
  dbo.OrderDetails(ProductID)

  也能够使用SQL Server管理工做台在表上建立索引,如图2所示。

  图 2 使用SQL Server管理工做台建立索引

  第二步:建立适当的覆盖索引

  假设你在Sales表(SelesID,SalesDate,SalesPersonID,ProductID,Qty)的外键列(ProductID)上建立了一个索引,假设ProductID列是一个高选中性列,那么任何在where子句中使用索引列(ProductID)的select查询都会更快,若是在外键上没有建立索引,将会发生所有扫描,但还有办法能够进一步提高查询性能。

  假设Sales表有10,000行记录,下面的SQL语句选中400行(总行数的4%): 

SELECT SalesDate, SalesPersonID FROM Sales WHERE ProductID =112

  咱们来看看这条SQL语句在SQL执行引擎中是如何执行的:

  1)Sales表在ProductID列上有一个非汇集索引,所以它查找非汇集索引树找出ProductID=112的记录;

  2)包含ProductID = 112记录的索引页也包括全部的汇集索引键(全部的主键键值,即SalesID);

  3)针对每个主键(这里是400),SQL Server引擎查找汇集索引树找出真实的行在对应页面中的位置;

  SQL Server引擎从对应的行查找SalesDate和SalesPersonID列的值。

  在上面的步骤中,对ProductID = 112的每一个主键记录(这里是400),SQL Server引擎要搜索400次汇集索引树以检索查询中指定的其它列(SalesDate,SalesPersonID)。

  若是非汇集索引页中包括了汇集索引键和其它两列(SalesDate,,SalesPersonID)的值,SQL Server引擎可能不会执行上面的第3和4步,直接从非汇集索引树查找ProductID列速度还会快一些,直接从索引页读取这三列的数值。

  幸运的是,有一种方法实现了这个功能,它被称为“覆盖索引”,在表列上建立覆盖索引时,须要指定哪些额外的列值须要和汇集索引键值(主键)一块儿存储在索引页中。下面是在Sales 表ProductID列上建立覆盖索引的例子: 

CREATEINDEX NCLIX_Sales_ProductID--Index name   ON dbo.Sales(ProductID)--Column on which index is to be created   INCLUDE(SalesDate, SalesPersonID)--Additional column values to include

  应该在那些select查询中常使用到的列上建立覆盖索引,但覆盖索引中包括过多的列也不行,由于覆盖索引列的值是存储在内存中的,这样会消耗过多内存,引起性能降低。

  建立覆盖索引时使用数据库调整顾问

  咱们知道,当SQL出问题时,SQL Server引擎中的优化器根据下列因素自动生成不一样的查询计划:

  1)数据量

  2)统计数据

  3)索引变化

  4)TSQL中的参数值

  5)服务器负载

  这就意味着,对于特定的SQL,即便表和索引结构是同样的,但在生产服务器和在测试服务器上产生的执行计划可能会不同,这也意味着在测试服务器上建立的索引能够提升应用程序的性能,但在生产服务器上建立一样的索引却未必会提升应用程序的性能。由于测试环境中的执行计划利用了新建立的索引,但在生产环境中执行计划可能不会利用新建立的索引(例如,一个非汇集索引列在生产环境中不是一个高选中性列,但在测试环境中可能就不同)。

  所以咱们在建立索引时,要知道执行计划是否会真正利用它,但咱们怎么才能知道呢?答案就是在测试服务器上模拟生产环境负载,而后建立合适的索引并进行测试,若是这样测试发现索引能够提升性能,那么它在生产环境也就更可能提升应用程序的性能了。

  虽然要模拟一个真实的负载比较困难,但目前已经有不少工具能够帮助咱们。

  使用SQL profiler跟踪生产服务器,尽管不建议在生产环境中使用SQL profiler,但有时没有办法,要诊断性能问题关键所在,必须得用,在http://msdn.microsoft.com/en-us/library/ms181091.aspx有SQL profiler的使用方法。

  使用SQL profiler建立的跟踪文件,在测试服务器上利用数据库调整顾问建立一个相似的负载,大多数时候,调整顾问会给出一些能够当即使用的索引建议,在http://msdn.microsoft.com/en-us/library/ms166575.aspx有调整顾问的详细介绍。

  第三步:整理索引碎片

  你可能已经建立好了索引,而且全部索引都在工做,但性能却仍然很差,那极可能是产生了索引碎片,你须要进行索引碎片整理。

  什么是索引碎片?

  因为表上有过分地插入、修改和删除操做,索引页被分红多块就造成了索引碎片,若是索引碎片严重,那扫描索引的时间就会变长,甚至致使索引不可用,所以数据检索操做就慢下来了。

  有两种类型的索引碎片:内部碎片和外部碎片。

  内部碎片:为了有效的利用内存,使内存产生更少的碎片,要对内存分页,内存以页为单位来使用,最后一页每每装不满,因而造成了内部碎片。

  外部碎片:为了共享要分段,在段的换入换出时造成外部碎片,好比5K的段换出后,有一个4k的段进来放到原来5k的地方,因而造成1k的外部碎片。

碎片产生的缘由:

碎片是因为表中的数据修改产生的。当插入、更新表中的数据时,表对应的聚簇索引被修改,若是对索引的修改不能容纳在同一页面中,可能致使索引叶子页面被分割。从而添加一个新的页面用以包含原来页面的一部分,而且维持索引键中行的逻辑顺序。

虽然新的页面维护了与原页面的中行的逻辑顺序,可是两个页面通常状况下在硬盘上是不相邻的

  如何知道是否发生了索引碎片?

  执行下面的SQL语句就知道了(下面的语句能够在SQL Server 2005及后续版本中运行,用你的数据库名替换掉这里的AdventureWorks):

 SELECTobject_name(dt.object_id) Tablename,si.name
  IndexName,dt.avg_fragmentation_in_percent AS
  ExternalFragmentation,dt.avg_page_space_used_in_percent AS
  InternalFragmentation
  FROM
  (
  SELECTobject_id,index_id,avg_fragmentation_in_percent,avg_page_space_used_in_percent
  FROM sys.dm_db_index_physical_stats (db_id('AdventureWorks'),null,null,null,'DETAILED'
  )
  WHERE index_id <>0) AS dt INNERJOIN sys.indexes si ON si.object_id=dt.object_id
  AND si.index_id=dt.index_id AND dt.avg_fragmentation_in_percent>10
  AND dt.avg_page_space_used_in_percent<75ORDERBY avg_fragmentation_in_percent DESC

  执行后显示AdventureWorks数据库的索引碎片信息。

  图 3 索引碎片信息

  使用下面的规则分析结果,你就能够找出哪里发生了索引碎片:

  1)ExternalFragmentation的值>10表示对应的索引起生了外部碎片;

  2)InternalFragmentation的值<75表示对应的索引起生了内部碎片。

  如何整理索引碎片?

  有两种整理索引碎片的方法:

  1)重组有碎片的索引:执行下面的命令

  ALTER INDEX ALL ON TableName REORGANIZE

  2)重建索引:执行下面的命令

  ALTER INDEX ALL ON TableName REBUILD WITH (FILLFACTOR=90,ONLINE=ON)

  也能够使用索引名代替这里的“ALL”关键字重组或重建单个索引,也能够使用SQL Server管理工做台进行索引碎片的整理。

  图 4 使用SQL Server管理工做台整理索引碎片

  何时用重组,何时用重建呢?

  当对应索引的外部碎片值介于10-15之间,内部碎片值介于60-75之间时使用重组,其它状况就应该使用重建。

  值得注意的是重建索引时,索引对应的表会被锁定,但重组不会锁表,所以在生产系统中,对大表重建索引要慎重,由于在大表上建立索引可能会花几个小时,幸运的是,从SQL Server 2005开始,微软提出了一个解决办法,在重建索引时,将ONLINE选项设置为ON,这样能够保证重建索引时表仍然能够正常使用。

  虽然索引能够提升查询速度,但若是你的数据库是一个事务型数据库,大多数时候都是更新操做,更新数据也就意味着要更新索引,这个时候就要兼顾查询和更新操做了,由于在OLTP数据库表上建立过多的索引会下降总体数据库性能。

  我给你们一个建议:若是你的数据库是事务型的,平均每一个表上不能超过5个索引,若是你的数据库是数据仓库型,平均每一个表能够建立10个索引都没问题。

  在前面咱们介绍了如何正确使用索引,调整索引是见效最快的性能调优方法,但通常而言,调整索引只会提升查询性能。除此以外,咱们还能够调整数据访问代码和TSQL,本文就介绍如何以最优的方法重构数据访问代码和TSQL。

  第四步:将TSQL代码从应用程序迁移到数据库中

  也许你不喜欢个人这个建议,你或你的团队可能已经有一个默认的潜规则,那就是使用ORM(Object Relational Mapping,即对象关系映射)生成全部SQL,并将SQL放在应用程序中,但若是你要优化数据访问性能,或须要调试应用程序性能问题,我建议你将SQL代码移植到数据库上(使用存储过程,视图,函数和触发器),缘由以下:

  一、使用存储过程,视图,函数和触发器实现应用程序中SQL代码的功能有助于减小应用程序中SQL复制的弊端,由于如今只在一个地方集中处理SQL,为之后的代码复用打下了良好的基础。

  二、使用数据库对象实现全部的TSQL有助于分析TSQL的性能问题,同时有助于你集中管理TSQL代码。

  三、将TS QL移植到数据库上去后,能够更好地重构TSQL代码,以利用数据库的高级索引特性。此外,应用程序中没了SQL代码也将更加简洁。

  虽然这一步可能不会象前三步那样立竿见影,但作这一步的主要目的是为后面的优化步骤打下基础。若是在你的应用程序中使用ORM(如NHibernate)实现了数据访问例行程序,在测试或开发环境中你可能发现它们工做得很好,但在生产数据库上却可能遇到问题,这时你可能须要反思基于ORM的数据访问逻辑,利用TSQL对象实现数据访问例行程序是一种好办法,这样作有更多的机会从数据库角度来优化性能。

  我向你保证,若是你花1-2人月来完成迁移,那之后确定不止节约1-2人年的的成本。

  OK!假设你已经照个人作的了,彻底将TSQL迁移到数据库上去了,下面就进入正题吧!

  第五步:识别低效TSQL,采用最佳实践重构和应用TSQL

  因为每一个程序员的能力和习惯都不同,他们编写的TSQL可能风格各异,部分代码可能不是最佳实现,对于水平通常的程序员可能首先想到的是编写TSQL实现需求,至于性能问题往后再说,所以在开发和测试时可能发现不了问题。

  也有一些人知道最佳实践,但在编写代码时因为种种缘由没有采用最佳实践,等到用户发飙的那天才乖乖地从新埋头思考最佳实践。

  我以为仍是有必要介绍一下具备都有哪些最佳实践。

  一、在查询中不要使用“select *”

  (1)检索没必要要的列会带来额外的系统开销,有句话叫作“该省的则省”;

  (2)数据库不能利用“覆盖索引”的优势,所以查询缓慢。

  二、在select清单中避免没必要要的列,在链接条件中避免没必要要的表

  (1)在select查询中若有没必要要的列,会带来额外的系统开销,特别是LOB类型的列;

  (2)在链接条件中包含没必要要的表会强制数据库引擎检索和匹配不须要的数据,增长了查询执行时间。

  三、不要在子查询中使用count()求和执行存在性检查

  (1)不要使用

SELECT column_list FROMtableWHERE0< (SELECTcount(*) FROM table2 WHERE ..)

  使用

SELECT column_list FROMtableWHEREEXISTS (SELECT*FROM table2 WHERE ...)

  代替;

  (2)当你使用count()时,SQL Server不知道你要作的是存在性检查,它会计算全部匹配的值,要么会执行全表扫描,要么会扫描最小的非汇集索引;

  (3)当你使用EXISTS时,SQL Server知道你要执行存在性检查,当它发现第一个匹配的值时,就会返回TRUE,并中止查询。相似的应用还有使用IN或ANY代替count()。

  四、避免使用两个不一样类型的列进行表的链接

  (1)当链接两个不一样类型的列时,其中一个列必须转换成另外一个列的类型,级别低的会被转换成高级别的类型,转换操做会消耗必定的系统资源;

  (2)若是你使用两个不一样类型的列来链接表,其中一个列本来能够使用索引,但通过转换后,优化器就不会使用它的索引了。例如: 

SELECT column_list FROM small_table, large_table WHERE
  smalltable.float_column = large_table.int_column

  在这个例子中,SQL Server会将int列转换为float类型,由于int比float类型的级别低,large_table.int_column上的索引就不会被使用,但smalltable.float_column上的索引能够正常使用。

  五、避免死锁

  (1)在你的存储过程和触发器中访问同一个表时老是以相同的顺序;

  (2)事务应经可能地缩短,在一个事务中应尽量减小涉及到的数据量;

  (3)永远不要在事务中等待用户输入。

  六、使用“基于规则的方法”而不是使用“程序化方法”编写TSQL

  (1)数据库引擎专门为基于规则的SQL进行了优化,所以处理大型结果集时应尽可能避免使用程序化的方法(使用游标或UDF[User Defined Functions]处理返回的结果集) ;

  (2)如何摆脱程序化的SQL呢?有如下方法:

  - 使用内联子查询替换用户定义函数;

  - 使用相关联的子查询替换基于游标的代码;

  - 若是确实须要程序化代码,至少应该使用表变量代替游标导航和处理结果集。

  七、避免使用count(*)得到表的记录数

  (1)为了得到表中的记录数,咱们一般使用下面的SQL语句:

 SELECTCOUNT(*) FROM dbo.orders

  这条语句会执行全表扫描才能得到行数。

  (2)但下面的SQL语句不会执行全表扫描同样能够得到行数:

SELECT rows FROM sysindexes
  WHERE id =OBJECT_ID('dbo.Orders') AND indid <2

  八、避免使用动态SQL

  除非无可奈何,应尽可能避免使用动态SQL,由于:

  (1)动态SQL难以调试和故障诊断;

  (2)若是用户向动态SQL提供了输入,那么可能存在SQL注入风险。

  九、避免使用临时表

  (1)除非却有须要,不然应尽可能避免使用临时表,相反,能够使用表变量代替;

  (2)大多数时候(99%),表变量驻扎在内存中,所以速度比临时表更快,临时表驻扎在TempDb数据库中,所以临时表上的操做须要跨数据库通讯,速度天然慢。

  十、使用全文搜索搜索文本数据,取代like搜索

  全文搜索始终优于like搜索:

  (1)全文搜索让你能够实现like不能完成的复杂搜索,如搜索一个单词或一个短语,搜索一个与另外一个单词或短语相近的单词或短语,或者是搜索同义词;

  (2)实现全文搜索比实现like搜索更容易(特别是复杂的搜索);

  十一、使用union实现or操做

  (1)在查询中尽可能不要使用or,使用union合并两个不一样的查询结果集,这样查询性能会更好;

  (2)若是不是必需要不一样的结果集,使用union all效果会更好,由于它不会对结果集排序。

UNION和UNION ALL的做用和语法

UNION 用于合并两个或多个 SELECT 语句的结果集,并消去表中任何重复行。
UNION 内部的 SELECT 语句必须拥有相同数量的列,列也必须拥有类似的数据类型。
同时,每条 SELECT 语句中的列的顺序必须相同.
SQL UNION 语法:

复制代码代码以下:
SELECT column_name FROM table1
UNION
SELECT column_name FROM table2


注释:默认地,UNION 操做符选取不一样的值。若是容许重复的值,请使用 UNION ALL。
当 ALL 随 UNION 一块儿使用时(即 UNION ALL),不消除重复行
SQL UNION ALL 语法

复制代码代码以下:
SELECT column_name FROM table1
UNION ALL
SELECT column_name FROM table2

  十二、为大对象使用延迟加载策略

  (1)在不一样的表中存储大对象(如VARCHAR(MAX),Image,Text等),而后在主表中存储这些大对象的引用;

  (2)在查询中检索全部主表数据,若是须要载入大对象,按需从大对象表中检索大对象。

  1三、使用VARCHAR(MAX),VARBINARY(MAX) 和 NVARCHAR(MAX)

  (1)在SQL Server 2000中,一行的大小不能超过800字节,这是受SQL Server内部页面大小8KB的限制形成的,为了在单列中存储更多的数据,你须要使用TEXT,NTEXT或IMAGE数据类型(BLOB);

  (2)这些和存储在相同表中的其它数据不同,这些页面以B-Tree结构排列,这些数据不能做为存储过程或函数中的变量,也不能用于字符串函数,如REPLACE,CHARINDEX或SUBSTRING,大多数时候你必须使用READTEXT,WRITETEXT和UPDATETEXT;

  (3)为了解决这个问题,在SQL Server 2005中增长了VARCHAR(MAX),VARBINARY(MAX) 和 NVARCHAR(MAX),这些数据类型能够容纳和BLOB相同数量的数据(2GB),和其它数据类型使用相同的数据页;

  (4)当MAX数据类型中的数据超过8KB时,使用溢出页(在ROW_OVERFLOW分配单元中)指向源数据页,源数据页仍然在IN_ROW分配单元中。

  1四、在用户定义函数中使用下列最佳实践

  不要在你的存储过程,触发器,函数和批处理中重复调用函数,例如,在许多时候,你须要得到字符串变量的长度,不管如何都不要重复调用LEN函数,只调用一次便可,将结果存储在一个变量中,之后就能够直接使用了。

  1五、在存储过程当中使用下列最佳实践

  (1)不要使用SP_xxx做为命名约定,它会致使额外的搜索,增长I/O(由于系统存储过程的名字就是以SP_开头的),同时这么作还会增长与系统存储过程名称冲突的概率;

  (2)将Nocount设置为On避免额外的网络开销;

  (3)当索引结构发生变化时,在EXECUTE语句中(第一次)使用WITH RECOMPILE子句,以便存储过程能够利用最新建立的索引;

  (4)使用默认的参数值更易于调试。

  1六、在触发器中使用下列最佳实践

  (1)最好不要使用触发器,触发一个触发器,执行一个触发器事件自己就是一个耗费资源的过程;

  (2)若是可以使用约束实现的,尽可能不要使用触发器;

  (3)不要为不一样的触发事件(Insert,Update和Delete)使用相同的触发器;

  (4)不要在触发器中使用事务型代码。

  1七、在视图中使用下列最佳实践

  (1)为从新使用复杂的TSQL块使用视图,并开启索引视图;

  (2)若是你不想让用户意外修改表结构,使用视图时加上SCHEMABINDING选项;

  (3)若是只从单个表中检索数据,就不须要使用视图了,若是在这种状况下使用视图反倒会增长系统开销,通常视图会涉及多个表时才有用。

  1八、在事务中使用下列最佳实践

  (1)SQL Server 2005以前,在BEGIN TRANSACTION以后,每一个子查询修改语句时,必须检查@@ERROR的值,若是值不等于0,那么最后的语句可能会致使一个错误,若是发生任何错误,事务必须回滚。从SQL Server 2005开始,Try..Catch..代码块能够处理TSQL中的事务,所以在事务型代码中最好加上Try…Catch…;

  (2)避免使用嵌套事务,使用@@TRANCOUNT变量检查事务是否须要启动(为了不嵌套事务);

  (3)尽量晚启动事务,提交和回滚事务要尽量快,以减小资源锁定时间。

  要彻底列举最佳实践不是本文的初衷,当你了解了这些技巧后就应该拿来使用,不然了解了也没有价值。此外,你还须要评审和监视数据访问代码是否遵循下列标准和最佳实践。

  如何分析和识别你的TSQL中改进的范围?

  理想状况下,你们都想预防疾病,而不是等病发了去治疗。但实际上这个愿望根本没法实现,即便你的团队成员全都是专家级人物,我也知道你有进行评审,但代码仍然一团糟,所以须要知道如何治疗疾病同样重要。

  首先须要知道如何诊断性能问题,诊断就得分析TSQL,找出瓶颈,而后重构,要找出瓶颈就得先学会分析执行计划。

  理解查询执行计划

  当你将SQL语句发给SQL Server引擎后,SQL Server首先要肯定最合理的执行方法,查询优化器会使用不少信息,如数据分布统计,索引结构,元数据和其它信息,分析多种可能的执行计划,最后选择一个最佳的执行计划。

  能够使用SQL Server Management Studio预览和分析执行计划,写好SQL语句后,点击SQL Server Management Studio上的评估执行计划按钮查看执行计划,如图1所示。

  图 1 在Management Studio中评估执行计划

  在执行计划图中的每一个图标表明计划中的一个行为(操做),应从右到左阅读执行计划,每一个行为都一个相对于整体执行成本(100%)的成本百分比。

  在上面的执行计划图中,右边的那个图标表示在HumanResources表上的一个“汇集索引扫描”操做(阅读表中全部主键索引值),须要100%的整体查询执行成本,图中左边那个图标表示一个select操做,它只须要0%的整体查询执行成本。

  下面是一些比较重要的图标及其对应的操做:

  图 2 常见的重要图标及对应的操做

  注意执行计划中的查询成本,若是说成本等于100%,那极可能在批处理中就只有这个查询,若是在一个查询窗口中有多个查询同时执行,那它们确定有各自的成本百分比(小于100%)。

  若是想知道执行计划中每一个操做详细状况,将鼠标指针移到对应的图标上便可,你会看到相似于下面的这样一个窗口。

  图 3 查看执行计划中行为(操做)的详细信息

  这个窗口提供了详细的评估信息,上图显示了汇集索引扫描的详细信息,它要查找AdventureWorks数据库HumanResources方案下Employee表中 Gender = ‘M’的行,它也显示了评估的I/O,CPU成本。

  查看执行计划时,咱们应该得到什么信息

  当你的查询很慢时,你就应该看看预估的执行计划(固然也能够查看真实的执行计划),找出耗时最多的操做,注意观察如下成本一般较高的操做:

  一、表扫描(Table Scan)

  当表没有汇集索引时就会发生,这时只要建立汇集索引或重整索引通常均可以解决问题。

  二、汇集索引扫描(Clustered Index Scan)

  有时能够认为等同于表扫描,当某列上的非汇集索引无效时会发生,这时只要建立一个非汇集索引就ok了。

  三、哈希链接(Hash Join)

  当链接两个表的列没有被索引时会发生,只需在这些列上建立索引便可。

  四、嵌套循环(Nested Loops)

  当非汇集索引不包括select查询清单的列时会发生,只须要建立覆盖索引问题便可解决。

  五、RID查找(RID Lookup)

  当你有一个非汇集索引,但相同的表上却没有汇集索引时会发生,此时数据库引擎会使用行ID查找真实的行,这时一个代价高的操做,这时只要在该表上建立汇集索引便可。

  TSQL重构真实的故事

  只有解决了实际的问题后,知识才转变为价值。当咱们检查应用程序性能时,发现一个存储过程比咱们预期的执行得慢得多,在生产数据库中检索一个月的销售数据竟然要50秒,下面就是这个存储过程的执行语句:

  exec uspGetSalesInfoForDateRange ‘1/1/2009’, 31/12/2009,’Cap’

  Tom受命来优化这个存储过程,下面是这个存储过程的代码:

 ALTERPROCEDURE uspGetSalesInfoForDateRange
  @startYearDateTime,
  @endYearDateTime,
  @keywordnvarchar(50)
  AS
  BEGIN
  SET NOCOUNT ON;
  SELECT
  Name,
  ProductNumber,
  ProductRates.CurrentProductRate Rate,
  ProductRates.CurrentDiscount Discount,
  OrderQty Qty,
  dbo.ufnGetLineTotal(SalesOrderDetailID) Total,
  OrderDate,
  DetailedDescription
  FROM
  Products INNERJOIN OrderDetails
  ON Products.ProductID = OrderDetails.ProductID
  INNERJOIN Orders
  ON Orders.SalesOrderID = OrderDetails.SalesOrderID
  INNERJOIN ProductRates
  ON
  Products.ProductID = ProductRates.ProductID
  WHERE
  OrderDate between@startYearand@endYear
  AND
  (
  ProductName LIKE''+@keyword+' %'OR
  ProductName LIKE'% '+@keyword+''+'%'OR
  ProductName LIKE'% '+@keyword+'%'OR
  Keyword LIKE''+@keyword+' %'OR
  Keyword LIKE'% '+@keyword+''+'%'OR
  Keyword LIKE'% '+@keyword+'%'
  )
  ORDERBY
  ProductName
  END
  GO

  分析索引

  首先,Tom想到了审查这个存储过程使用到的表的索引,很快他发现下面两列的索引无端丢失了:

  OrderDetails.ProductID

  OrderDetails.SalesOrderID

  他在这两个列上建立了非汇集索引,而后再执行存储过程:

  exec uspGetSalesInfoForDateRange ‘1/1/2009’, 31/12/2009 with recompile

  性能有所改变,但仍然低于预期(此次花了35秒),注意这里的with recompile子句告诉SQL Server引擎从新编译存储过程,从新生成执行计划,以利用新建立的索引。

  分析查询执行计划

  Tom接下来查看了SQL Server Management Studio中的执行计划,经过分析,他找到了某些重要的线索:

  一、发生了一次表扫描,即便该表已经正确设置了索引,而表扫描占据了整体查询执行时间的30%;

  二、发生了一个嵌套循环链接。

  Tom想知道是否有索引碎片,由于全部索引配置都是正确的,经过TSQL他知道了有两个索引都产生了碎片,很快他重组了这两个索引,因而表扫描消失了,如今执行存储过程的时间减小到25秒了。

  为了消除嵌套循环链接,他又在表上建立了覆盖索引,时间进一步减小到23秒。

  实施最佳实践

  Tom发现有个UDF有问题,代码以下: 

ALTERFUNCTION[dbo].[ufnGetLineTotal]
  (
  @SalesOrderDetailIDint
  )
  RETURNSmoney
  AS
  BEGIN
  DECLARE@CurrentProductRatemoney
  DECLARE@CurrentDiscountmoney
  DECLARE@Qtyint
  SELECT
  @CurrentProductRate= ProductRates.CurrentProductRate,
  @CurrentDiscount= ProductRates.CurrentDiscount,
  @Qty= OrderQty
  FROM
  ProductRates INNERJOIN OrderDetails ON
  OrderDetails.ProductID = ProductRates.ProductID
  WHERE
  OrderDetails.SalesOrderDetailID =@SalesOrderDetailID
  RETURN (@CurrentProductRate-@CurrentDiscount)*@Qty
  END

  在计算订单总金额时看起来代码很程序化,Tom决定在UDF的SQL中使用内联SQL。

  dbo.ufnGetLineTotal(SalesOrderDetailID) Total -- 旧代码

  (CurrentProductRate-CurrentDiscount)*OrderQty Total -- 新代码

  执行时间一会儿减小到14秒了。

  在select查询清单中放弃没必要要的Text列

  为了进一步提高性能,Tom决定检查一下select查询清单中使用的列,很快他发现有一个Products.DetailedDescription列是Text类型,经过对应用程序代码的走查,Tom发现其实这一列的数据并不会当即用到,因而他将这一列从select查询清单中取消掉,时间一会儿从14秒减小到6秒,因而Tom决定使用一个存储过程应用延迟加载策略加载这个Text列。

  最后Tom仍是不死心,认为6秒也没法接受,因而他再次仔细检查了SQL代码,他发现了一个like子句,通过反复研究他认为这个like搜索彻底能够用全文搜索替换,最后他用全文搜索替换了like搜索,时间一会儿下降到1秒,至此Tom认为调优应该暂时结束了。

  小结

  看起来咱们介绍了好多种优化数据访问的技巧,但你们要知道优化数据访问是一个无止境的过程,一样你们要相信一个信念,不管你的系统多么庞大,多么复杂,只要灵活运用咱们所介绍的这些技巧,你同样能够驯服它们。下一篇将介绍高级索引和反范式化。

  通过索引优化,重构TSQL后你的数据库还存在性能问题吗?彻底有可能,这时必须得找另外的方法才行。SQL Server在索引方面还提供了某些高级特性,可能你还从未使用过,利用高级索引会显著地改善系统性能,本文将从高级索引技术谈起,另外还将介绍反范式化技术。

  第六步:应用高级索引

  实施计算列并在这些列上建立索引

  你可能曾经写过从数据库查询一个结果集的应用程序代码,对结果集中每一行进行计算生成最终显示输出的信息。例如,你可能有一个查询从数据库检索订单信息,在应用程序代码中你可能已经经过对产品和销售量执行算术操做计算出了总的订单价格,但为何你不在数据库中执行这些操做呢?

  请看下面这张图,你能够经过指定一个公式将一个数据库表列做为计算列,你的TSQL在查询清单中包括这个计算列,SQL引擎将会应用这个公式计算出这一列的值,在执行查询时,数据库引擎将会计算订单总价,并为计算列返回结果。

  图 1 计算列

  使用计算列你能够将计算工做所有交给后端执行,但若是表的行数太多可能计算性能也不高,若是计算列出如今Select查询的where子句中状况会更糟,在这种状况下,为了匹配where子句指定的值,数据库引擎不得不计算表中全部行中计算列的值,这是一个低效的过程,由于它老是须要全表扫描或全汇集索引扫描。

  所以问题就来了,如何提升计算列的性能呢?解决办法是在计算列上建立索引,当计算列上有索引后,SQL Server会提早计算结果,而后在结果之上构建索引。此外,当对应列(计算列依赖的列)的值更新时,计算列上的索引值也会更新。所以,在执行查询时,数据库引擎不会为结果集中的每一行都执行一次计算公式,相反,经过索引可直接得到计算列预先计算出的值,所以在计算列上建立一个索引将会加快查询速度。

  提示:若是你想在计算列上建立索引,必须确保计算列上的公式不能包括任何“非肯定的”函数,例如getdate()就是一个非肯定的函数,由于每次调用它,它返回的值都是不同的。

  建立索引视图

  你是否知道能够在视图上建立索引?OK,不知道不要紧,看了个人介绍你就明白了。

  为何要使用视图?

  你们都知道,视图自己不存储任何数据,只是一条编译的select语句。数据库会为视图生成一个执行计划,视图是能够重复使用的,由于执行计划也能够重复使用。

  视图自己不会带来性能的提高,我曾经觉得它会“记住”查询结果,但后来我才知道它除了是一个编译了的查询外,其它什么都不是,视图根本记不住查询结果,我敢打赌好多刚接触SQL的人都会有这个错误的想法。

  可是如今我要告诉你一个方法让视图记住查询结果,其实很是简单,就是在视图上建立索引就能够了。

  若是你在视图上应用了索引,视图就成为索引视图,对于一个索引视图,数据库引擎处理SQL,并在数据文件中存储结果,和汇集表相似,当基础表中的数据发生变化时,SQL Server会自动维护索引,所以当你在索引视图上查询时,数据库引擎简单地从索引中查找值,速度固然就很快了,所以在视图上建立索引能够明显加快查询速度。

  但请注意,天下没有免费的午饭,建立索引视图能够提高性能,当基础表中的数据发生变化时,数据库引擎也会更新索引,所以,当视图要处理不少行,且要求和,当数据和基础表不常常发生变化时,就应该考虑建立索引视图。

  如何建立索引视图?

  1)建立/修改视图时指定SCHEMABINDING选项:

REATE VIEW dbo.vOrderDetails
  WITH SCHEMABINDING
  AS
  SELECT…

  2)在视图上建立一个惟一的汇集索引;

  3)视须要在视图上建立一个非汇集索引。

  不是全部视图上均可以建立索引,在视图上建立索引存在如下限制:

  1)建立视图时使用了SCHEMABINDING选项,这种状况下,数据库引擎不容许你改变表的基础结构;

  2)视图不能包含任何非肯定性函数,DISTINCT子句和子查询;

  3)视图中的底层表必须由汇集索引(主键)。

  若是你发现你的应用程序中使用的TSQL是用视图实现的,但存在性能问题,那此时给视图加上索引可能会带来性能的提高。

  为用户定义函数(UDF)建立索引

  在用户定义函数上也能够建立索引,但不能直接在它上面建立索引,须要建立一个辅助的计算列,公式就使用用户定义函数,而后在这个计算列字段上建立索引。具体步骤以下:

  1)首先建立一个肯定性的函数(若是不存在的话),在函数定义中添加SCHEMABINDING选项,如:

CREATEFUNCTION[dbo.ufnGetLineTotal]
  (
  -- Add the parameters for the function here   @UnitPrice[money],
  @UnitPriceDiscount[money],
  @OrderQty[smallint]
  )
  RETURNSmoney
  WITH SCHEMABINDING
  AS
  BEGIN
  return (((@UnitPrice*((1.0)-@UnitPriceDiscount))*@OrderQty))
  END

  2)在目标表上增长一个计算列,使用前面定义的函数做为该列的计算公式,如图2所示。

CREATEFUNCTION[dbo.ufnGetLineTotal]
  (
  -- Add the parameters for the function here   @UnitPrice[money],
  @UnitPriceDiscount[money],
  @OrderQty[smallint]
  )
  RETURNSmoney
  WITH SCHEMABINDING
  AS
  BEGIN
  return (((@UnitPrice*((1.0)-@UnitPriceDiscount))*@OrderQty))
  END
 图 2 指定UDF为计算列的结算公式

  3)在计算列上建立索引

  当你的查询中包括UDF时,若是在该UDF上建立了以计算列为基础的索引,特别是两个表或视图的链接条件中使用了UDF,性能都会有明显的改善。

  在XML列上建立索引

  在SQL Server(2005和后续版本)中,XML列是以二进制大对象(BLOB)形式存储的,能够使用XQuery进行查询,但若是没有索引,每次查询XML数据类型时都很是耗时,特别是大型XML实例,由于SQL Server在运行时须要分隔二进制大对象评估查询。为了提高XML数据类型上的查询性能,XML列能够索引,XML索引分为两类。

  主XML索引

  建立XML列上的主索引时,SQL Server会切碎XML内容,建立多个数据行,包括元素,属性名,路径,节点类型和值等,建立主索引让SQL Server更轻松地支持XQuery请求。下面是建立一个主XML索引的示例语法。 

CREATEPRIMARY XML INDEX index_name ON<object> ( xml_column )

  次要XML索引

  虽然XML数据已经被切条,但SQL Server仍然要扫描全部切条的数据才能找到想要的结果,为了进一步提高性能,还须要在主XML索引之上建立次要XML索引。有三种次要XML索引。

  1)“路径”(Path)次要XML索引:使用.exist()方法肯定一个特定的路径是否存在时它颇有用;

  2)“值”(Value)次要XML索引:用于执行基于值的查询,但不知道完整的路径或路径包括通配符时;

  3)“属性”(Secondary)次要XML索引:知道路径时检索属性的值。

  下面是一个建立次要XML索引的示例:

CREATE XML INDEX index_name ON<object> ( xml_column ) USING XML INDEX primary_xml_index_name FOR { VALUE | PATH | PROPERTY }

  请注意,上面讲的原则是基础,若是盲目地在表上建立索引,不必定会提高性能,由于有时在某些表的某些列上建立索引时,可能会导致插入和更新操做变慢,当这个表上有一个低选中性列时更是如此,一样,当表中的记录不多(如<500)时,若是在这样的表上建立索引反倒会使数据检索性能下降,由于对于小表而言,全表扫描反而会更快,所以在建立索引时应放聪明一点。

  第七步:应用反范式化,使用历史表和预计算列

  反范式化

  若是你正在为一个OLTA(在线事务分析)系统设计数据库,主要指为只读查询优化过的数据仓库,你能够(和应该)在你的数据库中应用反范式化和索引,也就是说,某些数据能够跨多个表存储,但报告和数据分析查询在这种数据库上可能会更快。

  但若是你正在为一个OLTP(联机事务处理)系统设计数据库,这样的数据库主要执行数据更新操做(包括插入/更新/删除),我建议你至少实施第1、2、三范式,这样数据冗余能够降到最低,数据存储也能够达到最小化,可管理性也会好一点。

  不管咱们在OLTP系统上是否应用范式,在数据库上总有大量的读操做(即select查询),当应用了全部优化技术后,若是发现数据检索操做仍然效率低下,此时,你可能须要考虑应用反范式设计了,但问题是如何应用反范式化,以及为何应用反范式化会提高性能?让咱们来看一个简单的例子,答案就在例子中。

  假设咱们有两个表OrderDetails(ID,ProductID,OrderQty) 和 Products(ID,ProductName)分别存储订单详细信息和产品信息,如今要查询某个客户订购的产品名称和它们的数量,查询SQL语句以下:

SELECT Products.ProductName,OrderQty
  FROM OrderDetails INNERJOIN Products
  ON OrderDetails.ProductID = Products.ProductID
  WHERE SalesOrderID =47057

  若是这两个都是大表,当你应用了全部优化技巧后,查询速度仍然很慢,这时能够考虑如下反范式化设计:

  1)在OrderDetails表上添加一列ProductName,并填充好数据;

  2)重写上面的SQL语句

 SELECT ProductName,OrderQty
  FROM OrderDetails
  WHERE SalesOrderID =47057

  注意在OrderDetails表上应用了反范式化后,再也不须要链接Products表,所以在执行SQL时,SQL引擎不会执行两个表的链接操做,查询速度固然会快一些。

  为了提升select操做性能,咱们不得不作出一些牺牲,须要在两个地方(OrderDetails 和 Products表)存储相同的数据(ProductName),当咱们插入或更新Products 表中的ProductName字段时,不得不一样步更新OrderDetails表中的ProductName字段,此外,应用这种反范式化设计时会增长存储资源消耗。

  所以在实施反范式化设计时,咱们必须在数据冗余和查询操做性能之间进行权衡,同时在应用反范式化后,咱们不得不重构某些插入和更新操做代码。有一个重要的原则须要遵照,那就是只有当你应用了全部其它优化技术都还不能将性能提高到理想状况时才使用反范式化。同时还需注意不能使用太多的反范式化设计,那样会使本来清晰的表结构设计变得越来模糊。

  历史表

  若是你的应用程序中有按期运行的数据检索操做(如报表),若是涉及到大表的检索,能够考虑按期将事务型规范化表中的数据复制到反范式化的单一的历史表中,如利用数据库的Job来完成这个任务,并对这个历史表创建合适的索引,那么周期性执行的数据检索操做能够迁移到这个历史表上,对单个历史表的查询性能确定比链接多个事务表的查询速度要快得多。

  例如,假设有一个连锁商店的月度报表须要3个小时才能执行完毕,你被派去优化这个报表,目的只有一个:最小化执行时间。那么你除了应用其它优化技巧外,还能够采起如下手段:

  1)使用反范式化结构建立一个历史表,并对销售数据创建合适的索引;

  2)在SQL Server上建立一个按期执行的操做,每隔24小时运行一次,在半夜往历史表中填充数据;

  3)修改报表代码,从历史表获取数据。

  建立按期执行的操做

  按照下面的步骤在SQL Server中建立一个按期执行的操做,按期从事务表中提取数据填充到历史表中。

  1)首先确保SQL Server代理服务处于运行状态;

  2)在SQL Server配置管理器中展开SQL Server代理节点,在“做业”节点上建立一个新做业,在“常规”标签页中,输入做业名称和描述文字;

  3)在“步骤”标签页中,点击“新建”按钮建立一个新的做业步骤,输入名字和TSQL代码,最后保存;

  4)切换到“调度”标签页,点击“新建”按钮建立一个新调度计划;

  5)最后保存调度计划。

  在数据插入和更新中提早执行耗时的计算,简化查询

  大多数状况下,你会看到你的应用程序是一个接一个地执行数据插入或更新操做,一次只涉及到一条记录,但数据检索操做可能同时涉及到多条记录。

  若是你的查询中包括一个复杂的计算操做,毫无疑问这将致使总体的查询性能降低,你能够考虑下面的解决办法:

  1)在表中建立额外的一列,包含计算的值;

  2)为插入和更新事件建立一个触发器,使用相同的计算逻辑计算值,计算完成后更新到新建的列;

  3)使用新建立的列替换查询中的计算逻辑。

  实施完上述步骤后,插入和更新操做可能会更慢一点,由于每次插入和更新时触发器都会执行一下,但数据检索操做会比以前快得多,由于执行查询时,数据库引擎不会执行计算操做了。

  小结

  至此,咱们已经应用了索引,重构TSQL,应用高级索引,反范式化,以及历史表加速数据检索速度,但性能优化是一个永无终点的过程,最下一篇文章中咱们将会介绍如何诊断数据库性能问题。

  诊断数据库性能问题就象医生诊断病人病情同样,既要结合本身积累的经验,又要依靠科学的诊断报告,才能准确地判断问题的根源在哪里。前面三篇文章咱们介绍了许多优化数据库性能的方法,当然掌握优化技巧很重要,但诊断数据库性能问题是优化的前提,本文就介绍一下如何诊断数据库性能问题。

  第八步:使用SQL事件探查器和性能监控工具备效地诊断性能问题

  在SQL Server应用领域SQL事件探查器多是最著名的性能故障排除工具,大多数状况下,当获得一个性能问题报告后,通常首先启动它进行诊断。

  你可能已经知道,SQL事件探查器是一个跟踪和监控SQL Server实例的图形化工具,主要用于分析和衡量在数据库服务器上执行的TSQL性能,你能够捕捉服务器实例上的每一个事件,将其保存到文件或表中供之后分析。例如,若是生产数据库速度很慢,你能够使用SQL事件探查器查看哪些存储过程执行时耗时过多。

  SQL事件探查器的基本用法

  你可能已经知道如何使用它,那么你能够跳过这一小节,但我仍是要重复一下,也许有许多新手阅读本文。

  1)启动SQL事件探查器,链接到目标数据库实例,建立一个新跟踪,指定一个跟踪模板(跟踪模板预置了一些事件和用于跟踪的列),如图1所示;

  图 1 选择跟踪模板

  2)做为可选的一步,你还能够选择特定事件和列

  图 2 选择跟踪过程要捕捉的事件

  3)另外你还能够点击“组织列”按钮,在弹出的窗口中指定列的显示顺序,点击“列过滤器”按钮,在弹出的窗口中设置过滤器,例如,经过设置数据库的名称(在like文本框中),只跟踪特定的数据库,若是不设置过滤器,SQL事件探查器会捕捉全部的事件,跟踪的信息会很是多,要找出有用的关键信息就如大海捞针。

  图 3 过滤器设置

  4)运行事件探查器,等待捕捉事件

  图 4 运行事件探查器

  5)跟踪了足够的信息后,停掉事件探查器,将跟踪信息保存到一个文件中,或者保存到一个数据表中,若是保存到表中,须要指定表名,SQL Server会自动建立表中的字段。

  图 5 将探查器跟踪数据保存到表中

  6)执行下面的SQL查询语句找出执行代价较高的TSQL

SELECT TextData,Duration,…, FROM Table_Name ORDERBY
  Duration DESC

  图 6 查找成本最高的TSQL/存储过程

  有效利用SQL事件探查器排除与性能相关的问题

  SQL事件探查器除了能够用于找出执行成本最高的那些TSQL或存储过程外,还能够利用它许多强大的功能诊断和解决其它不一样类型的问题。当你收到一个性能问题报告后,或者想提早诊断潜在的性能问题时均可以使用SQL事件探查器。下面是一些SQL事件探查器使用技巧,或许对你有帮助。

  1)使用现有的模板,但须要时应建立你本身的模板

  大多数时候现有的模板可以知足你的需求,但当诊断一个特殊类型的数据库性能问题时(如数据库发生死锁),你可能须要建立本身的模板,在这种状况下,你能够点击“文件”*“模板”*“新建模板”建立一个新模板,须要指定模板名、事件和列。固然也能够从现有的模板修改而来。

  图 7 建立一个新模板

  图 8 为新模板指定事件和列

  2)捕捉表扫描(TableScan)和死锁(DeadLock)事件

  没错,你能够使用SQL事件探查器监听这两个有趣的事件。

  先假设一种状况,假设你已经在你的测试库上建立了合适的索引,通过测试后,如今你已经将索引应用到生产服务器上了,但因为某些不明缘由,生产数据库的性能一直没达到预期的那样好,你推测执行查询时发生了表扫描,你但愿有一种方法可以检测出是否真的发生了表扫描。

  再假设另外一种状况,假设你已经设置好了将错误邮件发送到一个指定的邮件地址,这样开发团队能够第一时间得到通知,并有足够的信息进行问题诊断。某一天,你忽然收到一封邮件说数据库发生了死锁,并在邮件中包含了数据库级别的错误代码,你须要找出是哪一个TSQL创造了死锁。

  这时你能够打开SQL事件探查器,修改一个现有模板,使其能够捕捉表扫描和死锁事件,修改好后,启动事件探查器,运行你的应用程序,当再次发生表扫描和死锁事件时,事件探查器就能够捕捉到,利用跟踪信息就能够找出执行代价最高的TSQL。

  注意:从SQL Server日志文件中可能也能够找到死锁事件记录,在某些时候,你可能须要结合SQL Server日志和跟踪信息才能找出引发数据库死锁的数据库对象和TSQL。

  图 9 检测表扫描

  图 10 检测死锁

  3)建立重放跟踪

  某些时候,为了解决生产数据库的性能问题,你须要在测试服务器上模拟一个生产环境,这样能够重演性能问题。使用SQL事件探查器的TSQL_Replay模板捕捉生产库上的事件,并将跟踪信息保存为一个.trace文件,而后在测试服务器上播放跟踪文件就能够重现性能问题是如何出现的了。

  图 11 建立重放跟踪

  4)建立优化跟踪

  数据库调优顾问是一个伟大的工具,它能够给你提供很好的调优建议,但要真正从它那得到有用的建议,你须要模拟出与生产库同样的负载,也就是说,你须要在测试服务器上执行相同的TSQL,打开相同数量的并发链接,而后运行调优顾问。SQL事件探查器的Tuning模板能够捕捉到这类事件和列,使用Tuning模板运行事件探查器,捕捉跟踪信息并保存,经过调优顾问使用跟踪文件在测试服务器上建立相同的负载。

  图 12 建立Tuning事件探查器跟踪

  5)捕捉ShowPlan在事件探查器中包括SQL执行计划

  有时相同的查询在测试服务器和生产服务器上的性能彻底不同,假设你遇到这种问题,你应该仔细查看一下生产数据库上TSQL的执行计划。但问题是如今不能在生产库上执行这个TSQL,由于它已经有严重的性能问题。这时SQL事件探查器能够派上用场,在跟踪属性中选中ShowPlan或ShowPlan XML,这样能够捕捉到SQL执行计划和TSQL文本,而后在测试服务器上执行相同的TSQL,并比较二者的执行计划。

  图 13 指定捕捉执行计划

  图 14 在事件探查器跟踪中的执行计划

  使用性能监视工具(PerfMon)诊断性能问题

  当你的数据库遇到性能问题时,大多数时候使用SQL事件探查器就可以诊断和找出引发性能问题的背后缘由了,但有时SQL事件探查器并非万能的。

  例如,在生产库上使用SQL事件探查器分析查询执行时间时,对应的TSQL执行很慢(假设须要10秒),但一样的TSQL在测试服务器上执行时间却只要200毫秒,经过分析执行计划和数据列,发现它们都没有太大的差别,所以在生产库上确定有其它问题,那该如何揪出这些问题呢?

  此时性能监视工具(著名的PerfMon)能够帮你一把,它能够按期收集硬件和软件相关的统计数据,还有它是内置于Windows操做系统的一个免费的工具。

  当你向SQL Server数据库发送一条TSQL语句,会产生许多相关的执行参与者,包括TSQL执行引擎,服务器缓存,SQL优化器,输出队列,CPU,磁盘I/O等,只要这些参与者任何一环执行节奏没有跟上,最终的查询执行时间就会变长,使用性能监视工具能够对这些参与者进行观察,以找出根本缘由。

  使用性能监视工具能够建立多个不一样的性能计数器,经过图形界面分析计数器日志,此外还能够将性能计数器日志和SQL事件探查器跟踪信息结合起来分析。

  性能监视器基本用法介绍

  Windows内置了许多性能监视计数器,安装SQL Server时会添加一个SQL Server性能计数器,下面是建立一个性能计数器日志的过程。

  1)在SQL事件探查器中启动性能监视工具(“工具”*“性能监视器”);

  图 15 启动性能监视工具

  2)点击“计数器日志”*“新建日志设置”建立一个新的性能计数器日志

  图 16 建立一个性能计数器日志

  指定日志文件名,点击“肯定”。

  图 17 为性能计数器日志指定名字

  3)点击“添加计数器”按钮,选择一个须要的计数器

  图 18 为性能计数器日志指定计数器

  4)从列表中选择要监视的对象和对应的计数器,点击“关闭”

  图 19 指定对象和对应的计数器

  5)选择的计数器应显示在窗体中

  图 20 指定计数器

  6)点击“日志文件”标签,再点击“配置”按钮,指定日志文件保存位置,若是须要如今还能够修改日志文件名

  图 21 指定性能计数器日志文件保存位置

  7)点击“调度”标签,指定一个时间读取计数器性能,写入日志文件,也能够选择“手动”启动和中止计数器日志。

  图 22 指定性能计数器日志运行时间

  8)点击“常规”标签,指定收集计数器数据的间隔时间

  图 23 设置计数器间隔采样时间

  9)点击“肯定”,选择刚刚建立的计数器日志,点击右键启动它。

  图 24 启动性能计数器日志

  10)为了查看日志数据,再次打开性能监视工具,点击查看日志图标(红色),在“源”标签上选中“日志文件”单选按钮,点击“添加”按钮添加一个日志文件。

  图 25 查看性能计数器日志

  11)默认状况下,在日志输出中只有三个计数器被选中,点击“数据”标签能够追加其它计数器。

  图 26 查看日志数据时追加计数器

  12)点击“肯定”,返回图形化的性能计数器日志输出界面

  图 27 查看性能计数器日志

  关联性能计数器日志和SQL事件探查器跟踪信息进行深刻的分析

  经过SQL事件探查器能够找出哪些SQL执行时间过长,但它却不能给出致使执行时间过长的上下文信息,但性能监视工具能够提供独立组件的性能统计数据(即上下文信息),它们正好互补。

  若是相同的查询在生产库和测试库上的执行时间差异过大,那说明测试服务器的负载,环境和查询执行上下文都和生产服务器不同,所以须要一种方法来模拟生产服务器上的查询执行上下文,这时就须要结合SQL事件探查器的跟踪信息和性能监视工具的性能计数器日志。

  将两者结合起来分析能够更容易找出性能问题的根本缘由,例如,你可能发如今生产服务器上每次查询都须要10秒,CPU利用率达到了100%,这时就应该放下SQL调优,先调查一下为何CPU利用率会上升到100%。

  关联SQL事件探查器跟踪信息和性能计数器日志的步骤以下:

  1)建立性能计数器日志,包括下列常见的性能计数器,指定“手动”方式启动和中止计数器日志:

  --网络接口\输出队列长度

  --处理器\%处理器时间

  --SQL Server:缓冲管理器\缓冲区缓存命中率

  --SQL Server:缓冲管理器\页面生命周期

  --SQL Server:SQL统计\批量请求数/秒

  --SQL Server:SQL统计\SQL 编译

  --SQL Server:SQL统计\SQL 从新编译/秒

  建立好性能计数器日志,但不启动它。

  2)使用SQL事件探查器TSQL Duration模板建立一个跟踪,添加“开始时间”和“结束时间”列跟踪,同时启动事件探查器跟踪和前一步建立的性能计数器日志;

  3)跟踪到足够信息后,同时停掉SQL事件探查器跟踪和性能计数器日志,将SQL事件探查器跟踪信息保存为一个.trc文件;

  4)关闭SQL事件探查器跟踪窗口,再使用事件探查器打开.trc文件,点击“文件”*“导入性能数据”关联性能计数器日志,此时会打开一个文件浏览器窗口,选择刚刚保存的性能计数器日志文件进行关联;

  5)在打开的窗口中选择全部计数器,点击“肯定”,你将会看到下图所示的界面,它同时显示SQL事件探查器的跟踪信息和性能计数器日志;

  图 28 关联SQL事件探查器和性能监视工具输出

  6)在事件探查器跟踪信息输出中选择一条TSQL,你将会看到一个红色竖条,这表明这条TSQL执行时相关计数器的统计数据位置,一样,点击性能计数器日志输出曲线中高于正常值的点,你会看到对应的TSQL在SQL事件探查器输出中也是突出显示的。

  我相信你学会如何关联这两个工具的输出数据后,必定会以为很是方便和有趣。

  小结

  诊断SQL Server性能问题的工具和技术有不少,例如查看SQL Server日志文件,利用调优顾问(DTA)得到调优建议,不管使用哪一种工具,你都须要深刻了解内部的细节缘由,只有找出最根本的缘由以后,解决性能问题才会驾轻就熟。

  本系列最后一篇将介绍如何优化数据文件和应用分区。

  优化技巧主要是面向DBA的,但我认为即便是开发人员也应该掌握这些技巧,由于不是每一个开发团队都配有专门的DBA的。

  第九步:合理组织数据库文件组和文件

  建立SQL Server数据库时,数据库服务器会自动在文件系统上建立一系列的文件,以后建立的每个数据库对象实际上都是存储在这些文件中的。SQL Server有下面三种文件:

  1).mdf文件

  这是最主要的数据文件,每一个数据库只能有一个主数据文件,全部系统对象都存储在主数据文件中,若是不建立次要数据文件,全部用户对象(用户建立的数据库对象)也都存储在主数据文件中。

  2).ndf文件

  这些都是次要数据文件,它们是可选的,它们存储的都是用户建立的对象。

  3).ldf文件

  这些是事务日志文件,数量从一到几个不等,它里面存储的是事务日志。

  默认状况下,建立SQL Server数据库时会自动建立主数据文件和事务日志文件,固然也能够修改这两个文件的属性,如保存路径。

  文件组

  为了便于管理和得到更好的性能,数据文件一般都进行了合理的分组,建立一个新的SQL Server数据库时,会自动建立主文件组,主数据文件就包含在主文件组中,主文件组也被设为默认组,所以全部新建立的用户对象都自动存储在主文件组中(具体说就是存储在主数据文件中)。

  若是你想将你的用户对象(表、视图、存储过程和函数等)存储在次要数据文件中,那须要:

  1)建立一个新的文件组,并将其设为默认文件组;

  2)建立一个新的数据文件(.ndf),将其归于第一步建立的新文件组中。

  之后建立的对象就会所有存储在次要文件组中了。

  注意:事务日志文件不属于任何文件组。

  文件/文件组组织最佳实践

  若是你的数据库不大,那么默认的文件/文件组应该就能知足你的须要,但若是你的数据库变得很大时(假设有1000MB),你能够(应该)对文件/文件组进行调整以得到更好的性能,调整文件/文件组的最佳实践内容以下:

  1)主文件组必须彻底独立,它里面应该只存储系统对象,全部的用户对象都不该该放在主文件组中。主文件组也不该该设为默认组,将系统对象和用户对象分开能够得到更好的性能;

  2)若是有多块硬盘,能够将每一个文件组中的每一个文件分配到每块硬盘上,这样能够实现分布式磁盘I/O,大大提升数据读写速度;

  3)将访问频繁的表及其索引放到一个单独的文件组中,这样读取表数据和索引都会更快;

  4)将访问频繁的包含Text和Image数据类型的列的表放到一个单独的文件组中,最好将其中的Text和Image列数据放在一个独立的硬盘中,这样检索该表的非Text和Image列时速度就不会受Text和Image列的影响;

  5)将事务日志文件放在一个独立的硬盘上,千万不要和数据文件共用一块硬盘,日志操做属于写密集型操做,所以保证日志写入具备良好的I/O性能很是重要;

  6)将“只读”表单独放到一个独立的文件组中,一样,将“只写”表单独放到一个文件组中,这样只读表的检索速度会更快,只写表的更新速度也会更快;

  7)不要过分使用SQL Server的“自动增加”特性,由于自动增加的成本实际上是很高的,设置“自动增加”值为一个合适的值,如一周,一样,也不要过分频繁地使用“自动收缩”特性,最好禁用掉自动收缩,改成手工收缩数据库大小,或使用调度操做,设置一个合理的时间间隔,如一个月。

  第十步:在大表上应用分区

  什么是表分区?

  表分区就是将大表拆分红多个小表,以避免检索数据时扫描的数据太多,这个思想参考了“分而治之”的理论。

  当你的数据库中有一个大表(假设有上百万行记录),若是其它优化技巧都用上了,但查询速度仍然很是慢时,你就应该考虑对这个表进行分区了。首先来看一下分区的类型:

  水平分区:假设有一个表包括千万行记录,为了便于理解,假设表有一个自动增加的主键字段(如id),咱们能够将表拆分红10个独立的分区表,每一个分区包含100万行记录,分区就要依据id字段的值实施,即第一个分区包含id值从1-1000000的记录,第二个分区包含1000001-2000000的记录,以此类推。这种以水平方向分割表的方式就叫作水平分区。

  垂直分区:假设有一个表的列数和行数都很是多,其中某些列被常常访问,其他的列不是常常访问。因为表很是大,全部检索操做都很慢,所以须要基于频繁访问的列进行分区,这样咱们能够将这个大表拆分红多个小表,每一个小表由大表的一部分列组成,这种垂直拆分表的方法就叫作垂直分区。

  另外一个垂直分区的原则是按有索引的列无索引列进行拆分,但这种分区法须要当心,由于若是任何查询都涉及到检索这两个分区,SQL引擎不得不链接这两个分区,那样的话性能反而会低。

  本文主要对水平分区作一介绍。

  分区最佳实践

  1)将大表分区后,将每一个分区放在一个独立的文件中,并将这个文件存放在独立的硬盘上,这样数据库引擎能够同时并行检索多块硬盘上的不一样数据文件,提升并发读写速度;

  2)对于历史数据,能够考虑基于历史数据的“年龄”进行分区,例如,假设表中存储的是订单数据,能够使用订单日期列做为分区的依据,如将每一年的订单数据作成一个分区。

  如何分区?

  假设Order表中包含了四年(1999-2002)的订单数据,有上百万的记录,那若是要对这个表进行分区,采起的步骤以下:

  1)添加文件组

  使用下面的命令建立一个文件组:

  ALTER DATABASE OrderDB ADD FILEGROUP [1999]

  ALTER DATABASE OrderDB ADD FILE (NAME = N'1999', FILENAME

  = N'C:\OrderDB\1999.ndf', SIZE = 5MB, MAXSIZE = 100MB, FILEGROWTH = 5MB) TO

  FILEGROUP [1999]

  经过上面的语句咱们添加了一个文件组1999,而后增长了一个次要数据文件“C:\OrderDB\1999.ndf”到这个文件组中。

  使用上面的命令再建立三个文件组2000,2001和2002,每一个文件组存储一年的销售数据。

  2)建立分区函数

  分区函数是定义分界点的一个对象,使用下面的命令建立分区函数:

  CREATE PARTITION FUNCTION FNOrderDateRange (DateTime) AS

  RANGE LEFT FOR VALUES ('19991231', '20001231', '20011231')

  上面的分区函数指定:

  DateTime<=1999/12/31的记录进入第一个分区;

  DateTime > 1999/12/31 且 <= 2000/12/31的记录进入第二个分区;

  DateTime > 2000/12/31 且 <= 2001/12/31的记录进入第三个分区;

  DateTime > 2001/12/31的记录进入第四个分区。

  RANGE LEFT指定应该进入左边分区的边界值,例如小于或等于1999/12/31的值都应该进入第一个分区,下一个值就应该进入第二个分区了。若是使用RANGE RIGHT,边界值以及大于边界值的值都应该进入右边的分区,所以在这个例子中,边界值2000/12/31就应该进入第二个分区,小于这个边界值的值就应该进入第一个分区。

  3)建立分区方案

  经过分区方案在表/索引的分区和存储它们的文件组之间创建映射关系。建立分区方案的命令以下:

  CREATE PARTITION SCHEME OrderDatePScheme AS PARTITION FNOrderDateRange

  TO ([1999], [2000], [2001], [2002])

  在上面的命令中,咱们指定了:

  第一个分区应该进入1999文件组;

  第二个分区就进入2000文件组;

  第三个分区进入2001文件组;

  第四个分区进入2002文件组。

  4)在表上应用分区

  至此,咱们定义了必要的分区原则,如今须要作的就是给表分区了。首先使用DROP INDEX命令删除表上现有的汇集索引,一般主键上有汇集索引,若是是删除主键上的索引,还能够经过DROP CONSTRAINT删除主键来间接删除主键上的索引,以下面的命令删除PK_Orders主键:

  ALTER TABLE Orders DROP CONSTRAINT PK_Orders;

  在分区方案上从新建立汇集索引,命令以下:

  CREATE UNIQUE CLUSTERED INDEX PK_Orders ON Orders(OrderDate) ON

  OrderDatePScheme (OrderDate)

  假设OrderDate列的数据在表中是惟一的,表将基于分区方案OrderDatePScheme被分区,最终被分红四个小的部分,存放在四个文件组中。若是你对如何分区还有不清楚的地方,建议你去看看微软的官方文章“SQL Server 2005中的分区表和索引”(地址:http://msdn.microsoft.com/en-us/library/ms345146%28SQL.90%29.aspx)。

  第十一步:使用TSQL模板更好地管理DBMS对象(额外的一步)

  为了更好地管理DBMS对象(存储过程,函数,视图,触发器等),须要遵循一致的结构,但因为某些缘由(主要是时间限制),咱们未能维护一个一致的结构,所以后来遇到性能问题或其它缘由须要从新调试这些代码时,那感受就像是作噩梦。

  为了帮助你们更好地管理DBMS对象,我建立了一些TSQL模板,利用这些模板你能够快速地开发出结构一致的DBMS对象。

  若是你的团队有人专门负责检查团队成员编写的TSQL代码,在这些模板中专门有一个“审查”段落用来描写审查意见。

  我提交几个常见的DBMS对象模板,它们是:

   Template_StoredProcedure.txt:存储过程模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_StoredProcedure.txt)

   Template_View.txt:视图模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_Trigger.txt)

   Template_Trigger.txt:触发器模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_ScalarFunction.txt)

   Template_ScalarFunction.txt:标量函数模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_TableValuedFunction.txt)

   emplate_TableValuedFunction.txt:表值函数模板(http://www.codeproject.com/KB/database/OrganizeFilesAndPartition/Template_View.txt)

  1)如何建立模板?

   首先下载前面给出的模板代码,然打开SQL Server管理控制台,点击“查看”*“模板浏览器”;

   点击“存储过程”节点,点击右键,在弹出的菜单中选择“新建”*“模板”,为模板取一个易懂的名字;

   在新建立的模板上点击右键,选择“编辑”,在弹出的窗口中输入身份验证信息,点击“链接”;

   链接成功后,在编辑器中打开下载的Template_StoredProcedure.txt,拷贝文件中的内容粘贴到新建的模板中,而后点击“保存”。

  上面是建立一个存储过程模板的过程,建立其它DBMS对象过程相似。

  2)如何使用模板?

  建立好模板后,下面就演示如何使用模板了。

   首先在模板浏览器中,双击刚刚建立的存储过程模板,弹出身份验证对话框,输入对应的身份信息,点击“链接”;

   链接成功后,模板将会在编辑器中打开,变量将会赋上适当的值;

   按Ctrl+Shift+M为模板指定值,以下图所示;

  图 1 为模板参数指定值

   点击“OK”,而后在SQL Server管理控制台中选择目标数据库,而后点击“执行”按钮;

  若是一切顺利,存储过程就建立成功了。你能够根据上面的步骤建立其它DBMS对象。

  小结

  优化讲究的是一种“心态”,在优化数据库性能时,首先要相信性能问题老是能够解决的,而后就是结合经验和最佳实践努力进行优化,最重要的是要尽可能预防性能问题的发生,在开发和部署期间,要利用一切可利用的技术和经验进行提早评估,千万不要等问题出现了才去想办法解决,在开发期间多花一个小时实施最佳实践,最后可能会给你节约上百小时的故障诊断和排除时间,要学会聪明地工做,而不是辛苦地工做!

(注:本文来源于摘抄,由于文章不错,又担忧有天连接出问题,因此就复制借鉴了,所以若有雷同,不属巧合!!!)

..
 
 
相关文章
相关标签/搜索