金沙国际平台登录-金沙国际会员登录

热门关键词: 金沙国际平台登录,金沙国际会员登录

我将在这个系列的文章中介绍如何编写最佳的数

摘自:

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

 

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

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

  类似这种故事在世界各个角落每天都会上演,几乎每个开发人员在其开发生涯中都会遇到这种事情,我也曾多次遇到这种情况,因此我希望将我解决这种问题的经验和大家分享。

 

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

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

  范围

  类似这种故事在世界各个角落每天都会上演,几乎每个开发人员在其开发生涯中都会遇到这种事情,我也曾多次遇到这种情况,因此我希望将我解决这种问题的经验和大家分享。

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

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

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

  范围

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

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

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

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

  第一步:应用正确的索引

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

  我之所以先从索引谈起是因为采用正确的索引会使生产系统的性能得到质的提升,另一个原因是创建或修改索引是在数据库上进行的,不会涉及到修改程序,并可以立即见到成效。

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

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

  第一步:应用正确的索引

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

  我之所以先从索引谈起是因为采用正确的索引会使生产系统的性能得到质的提升,另一个原因是创建或修改索引是在数据库上进行的,不会涉及到修改程序,并可以立即见到成效。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

金沙国际会员登录 1

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

  图 1 索引树结构

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

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

 金沙国际会员登录 2

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

图 1 索引树结构

  确保每个表都有主键

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

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

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

  在下面这些列上创建非聚集索引:

  确保每个表都有主键

  1)搜索时经常使用到的;

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

  2)用于连接其它表的;

  在下面这些列上创建非聚集索引:

  3)用于外键字段的;

  1)搜索时经常使用到的;

  4)高选中性的;

  2)用于连接其它表的;

  5)ORDER BY子句使用到的;

  3)用于外键字段的;

  6)XML类型。

  4)高选中性的;

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

  5)ORDER BY子句使用到的;

CREATEINDEX

  6)XML类型。

  NCLIX_OrderDetails_ProductID ON

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

  dbo.OrderDetails(ProductID)

CREATEINDEX

  也可以使用SQL Server管理工作台在表上创建索引,如图2所示。

  NCLIX_OrderDetails_ProductID ON

金沙国际会员登录 3

  dbo.OrderDetails(ProductID)

  图 2 使用SQL Server管理工作台创建索引  

  也可以使用SQL Server管理工作台在表上创建索引,如图2所示。

 

金沙国际会员登录 4

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

 

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

图 2 使用SQL Server管理工作台创建索引

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

 

SELECT SalesDate, SalesPersonID FROM Sales WHERE ProductID =112

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

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

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

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

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

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

SELECT SalesDate, SalesPersonID FROM Sales WHERE ProductID =112

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

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

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

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

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

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

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

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

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

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

CREATEINDEX NCLIX_Sales_ProductID--Index name

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

  ON dbo.Sales(ProductID)--Column on which index is to be created

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

  INCLUDE(SalesDate, SalesPersonID)--Additional column values to include

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

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

CREATEINDEX NCLIX_Sales_ProductID--Index name

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

  ON dbo.Sales(ProductID)--Column on which index is to be created

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

  INCLUDE(SalesDate, SalesPersonID)--Additional column values to include

  1)数据量

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

  2)统计数据

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

  3)索引变化

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

  4)TSQL中的参数值

  1)数据量

  5)服务器负载

  2)统计数据

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

  3)索引变化

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

  4)TSQL中的参数值

  虽然要模拟一个真实的负载比较困难,但目前已经有很多工具可以帮助我们。

  5)服务器负载

  使用SQL profiler跟踪生产服务器,尽管不建议在生产环境中使用SQL profiler,但有时没有办法,要诊断性能问题关键所在,必须得用,在 profiler的使用方法。

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

  使用SQL profiler创建的跟踪文件,在测试服务器上利用数据库调整顾问创建一个类似的负载,大多数时候,调整顾问会给出一些可以立即使用的索引建议,在

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

 

  虽然要模拟一个真实的负载比较困难,但目前已经有很多工具可以帮助我们。

  第三步:整理索引碎片

  使用SQL profiler跟踪生产服务器,尽管不建议在生产环境中使用SQL profiler,但有时没有办法,要诊断性能问题关键所在,必须得用,在 profiler的使用方法。

  你可能已经创建好了索引,并且所有索引都在工作,但性能却仍然不好,那很可能是产生了索引碎片,你需要进行索引碎片整理。

  使用SQL profiler创建的跟踪文件,在测试服务器上利用数据库调整顾问创建一个类似的负载,大多数时候,调整顾问会给出一些可以立即使用的索引建议,在

  什么是索引碎片?

 

  由于表上有过度地插入、修改和删除操作,索引页被分成多块就形成了索引碎片,如果索引碎片严重,那扫描索引的时间就会变长,甚至导致索引不可用,因此数据检索操作就慢下来了。

  第三步:整理索引碎片

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

  你可能已经创建好了索引,并且所有索引都在工作,但性能却仍然不好,那很可能是产生了索引碎片,你需要进行索引碎片整理。

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

  什么是索引碎片?

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

  由于表上有过度地插入、修改和删除操作,索引页被分成多块就形成了索引碎片,如果索引碎片严重,那扫描索引的时间就会变长,甚至导致索引不可用,因此数据检索操作就慢下来了。

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

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

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

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

 SELECTobject_name(dt.object_id) Tablename,si.name

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

  IndexName,dt.avg_fragmentation_in_percent AS

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

  ExternalFragmentation,dt.avg_page_space_used_in_percent AS

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

  InternalFragmentation

金沙国际会员登录 5金沙国际会员登录 6

  FROM

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

  (

View Code

  SELECTobject_id,index_id,avg_fragmentation_in_percent,avg_page_space_used_in_percent

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

  FROM sys.dm_db_index_physical_stats (db_id('AdventureWorks'),null,null,null,'DETAILED'

 

  )

金沙国际会员登录 7

  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

图 3 索引碎片信息

  AND dt.avg_page_space_used_in_percent<75ORDERBY avg_fragmentation_in_percent DESC

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

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

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

金沙国际会员登录 8

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

  图 3 索引碎片信息

  如何整理索引碎片?

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

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

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

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

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

  ALTER INDEX ALL ON TableName REORGANIZE

  如何整理索引碎片?

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

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

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

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

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

  ALTER INDEX ALL ON TableName REORGANIZE

金沙国际会员登录 9

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

 

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

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

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

  什么时候用重组,什么时候用重建呢?

金沙国际会员登录 10

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

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

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

  什么时候用重组,什么时候用重建呢?

  虽然索引可以提高查询速度,但如果你的数据库是一个事务型数据库,大多数时候都是更新操作,更新数据也就意味着要更新索引,这个时候就要兼顾查询和更新操作了,因为在OLTP数据库表上创建过多的索引会降低整体数据库性能。

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

  我给大家一个建议:如果你的数据库是事务型的,平均每个表上不能超过5个索引,如果你的数据库是数据仓库型,平均每个表可以创建10个索引都没问题。

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

 

  虽然索引可以提高查询速度,但如果你的数据库是一个事务型数据库,大多数时候都是更新操作,更新数据也就意味着要更新索引,这个时候就要兼顾查询和更新操作了,因为在OLTP数据库表上创建过多的索引会降低整体数据库性能。

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

  我给大家一个建议:如果你的数据库是事务型的,平均每个表上不能超过5个索引,如果你的数据库是数据仓库型,平均每个表可以创建10个索引都没问题。

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

 

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

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

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

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

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

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

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

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

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

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

  我向你保证,如果你花1-2人月来完成迁移,那以后肯定不止节约1-2人年的的成本。

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

  OK!假设你已经照我的做的了,完全将TSQL迁移到数据库上去了,下面就进入正题吧!

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

 

  我向你保证,如果你花1-2人月来完成迁移,那以后肯定不止节约1-2人年的的成本。

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

  OK!假设你已经照我的做的了,完全将TSQL迁移到数据库上去了,下面就进入正题吧!

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

 

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

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

  我觉得还是有必要介绍一下具有都有哪些最佳实践。

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

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

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

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

  我觉得还是有必要介绍一下具有都有哪些最佳实践。

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

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

  2、在select清单中避免不必要的列,在连接条件中避免不必要的表

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

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

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

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

  2、在select清单中避免不必要的列,在连接条件中避免不必要的表

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

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

  (1)不要使用

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

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

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

  使用

  (1)不要使用

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

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

  代替;

  使用

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

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

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

  代替;

  4、避免使用两个不同类型的列进行表的连接

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

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

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

  (2)如果你使用两个不同类型的列来连接表,其中一个列原本可以使用索引,但经过转换后,优化器就不会使用它的索引了。例如: 

  4、避免使用两个不同类型的列进行表的连接

 

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

金沙国际会员登录 11金沙国际会员登录 12

  (2)如果你使用两个不同类型的列来连接表,其中一个列原本可以使用索引,但经过转换后,优化器就不会使用它的索引了。例如: 

SELECT column_list FROM small_table, large_table WHERE

  smalltable.float_column = large_table.int_column

SELECT column_list FROM small_table, large_table WHERE

View Code

  smalltable.float_column = large_table.int_column

 

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

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

  5、避免死锁

  5、避免死锁

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 

 

  7、避免使用count(*)获得表的记录数

  7、避免使用count(*)获得表的记录数

  (1)为了获得表中的记录数,我们通常使用下面的SQL语句:

  (1)为了获得表中的记录数,我们通常使用下面的SQL语句:

 SELECTCOUNT(*) FROM dbo.orders

 SELECTCOUNT(*) FROM dbo.orders

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

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

  (2)但下面的SQL语句不会执行全表扫描一样可以获得行数:

  (2)但下面的SQL语句不会执行全表扫描一样可以获得行数:

SELECT rows FROM sysindexes

 

  WHERE id =OBJECT_ID('dbo.Orders') AND indid <2

金沙国际会员登录 13金沙国际会员登录 14

  8、避免使用动态SQL

SELECT rows FROM sysindexes

  WHERE id =OBJECT_ID('dbo.Orders') AND indid <2

  除非迫不得已,应尽量避免使用动态SQL,因为:

View Code

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

 

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

 8、避免使用动态SQL

  9、避免使用临时表

  除非迫不得已,应尽量避免使用动态SQL,因为:

  (1)除非却有需要,否则应尽量避免使用临时表,相反,可以使用表变量代替;

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

  (2)大多数时候(99%),表变量驻扎在内存中,因此速度比临时表更快,临时表驻扎在TempDb数据库中,因此临时表上的操作需要跨数据库通信,速度自然慢。

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

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

  9、避免使用临时表

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

  (1)除非却有需要,否则应尽量避免使用临时表,相反,可以使用表变量代替;

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

  (2)大多数时候(99%),表变量驻扎在内存中,因此速度比临时表更快,临时表驻扎在TempDb数据库中,因此临时表上的操作需要跨数据库通信,速度自然慢。

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

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

  11、使用union实现or操作

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

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

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

  (2)如果不是必须要不同的结果集,使用union all效果会更好,因为它不会对结果集排序。

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

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

  11、使用union实现or操作

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

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

  (2)在查询中检索所有主表数据,如果需要载入大对象,按需从大对象表中检索大对象。

  (2)如果不是必须要不同的结果集,使用union all效果会更好,因为它不会对结果集排序。

  13、使用VARCHAR(MAX),VARBINARY(MAX) 和 NVARCHAR(MAX)

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

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

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

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

  (2)在查询中检索所有主表数据,如果需要载入大对象,按需从大对象表中检索大对象。

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

  13、使用VARCHAR(MAX),VARBINARY(MAX) 和 NVARCHAR(MAX)

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

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

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

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

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

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

 

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

  15、在存储过程中使用下列最佳实践

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

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

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

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

 

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

  15、在存储过程中使用下列最佳实践

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

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

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

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

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

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

  (2)如果能够使用约束实现的,尽量不要使用触发器;

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

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

金沙国际会员登录,  16、在触发器中使用下列最佳实践

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

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

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

  (2)如果能够使用约束实现的,尽量不要使用触发器;

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

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

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

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

  (3)如果只从单个表中检索数据,就不需要使用视图了,如果在这种情况下使用视图反倒会增加系统开销,一般视图会涉及多个表时才有用。

  金沙国际平台登录,17、在视图中使用下列最佳实践

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

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

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

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

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

  (3)如果只从单个表中检索数据,就不需要使用视图了,如果在这种情况下使用视图反倒会增加系统开销,一般视图会涉及多个表时才有用。

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

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

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

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

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

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

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

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

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

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

 

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

  理解查询执行计划

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

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

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

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

 

金沙国际会员登录 15

  理解查询执行计划

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

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

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

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

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

 

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

 

金沙国际会员登录 16

 

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

金沙国际会员登录 17

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

 

  如果想知道执行计划中每个操作详细情况,将鼠标指针移到对应的图标上即可,你会看到类似于下面的这样一个窗口。

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

金沙国际会员登录 18

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

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

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

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

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

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

 

  当你的查询很慢时,你就应该看看预估的执行计划(当然也可以查看真实的执行计划),找出耗时最多的操作,注意观察以下成本通常较高的操作:

金沙国际会员登录 19

  1、表扫描(Table Scan)

 

  当表没有聚集索引时就会发生,这时只要创建聚集索引或重整索引一般都可以解决问题。

 

  2、聚集索引扫描(Clustered Index Scan)

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

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

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

  3、哈希连接(Hash Join)

  如果想知道执行计划中每个操作详细情况,将鼠标指针移到对应的图标上即可,你会看到类似于下面的这样一个窗口。

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

 

  4、嵌套循环(Nested Loops)

金沙国际会员登录 20

  当非聚集索引不包括select查询清单的列时会发生,只需要创建覆盖索引问题即可解决。

 

  5、RID查找(RID Lookup)

 

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

 

  TSQL重构真实的故事

 

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

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

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

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

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

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

 ALTERPROCEDURE uspGetSalesInfoForDateRange

  当你的查询很慢时,你就应该看看预估的执行计划(当然也可以查看真实的执行计划),找出耗时最多的操作,注意观察以下成本通常较高的操作:

  @startYearDateTime,

  1、表扫描(Table Scan)

  @endYearDateTime,

  当表没有聚集索引时就会发生,这时只要创建聚集索引或重整索引一般都可以解决问题。

  @keywordnvarchar(50)

  2、聚集索引扫描(Clustered Index Scan)

  AS

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

  BEGIN

  3、哈希连接(Hash Join)

  SET NOCOUNT ON;

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

  SELECT

  4、嵌套循环(Nested Loops)

  Name,

  当非聚集索引不包括select查询清单的列时会发生,只需要创建覆盖索引问题即可解决。

  ProductNumber,

  5、RID查找(RID Lookup)

  ProductRates.CurrentProductRate Rate,

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

  ProductRates.CurrentDiscount Discount,

  TSQL重构真实的故事

  OrderQty Qty,

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

  dbo.ufnGetLineTotal(SalesOrderDetailID) Total,

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

  OrderDate,

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

  DetailedDescription

 

  FROM

金沙国际会员登录 21金沙国际会员登录 22

  Products INNERJOIN OrderDetails

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

  ON Products.ProductID = OrderDetails.ProductID

View Code

  INNERJOIN Orders

 

  ON Orders.SalesOrderID = OrderDetails.SalesOrderID

 

  INNERJOIN ProductRates

摘自:

  ON

收货颇丰,非常感谢 瓶子0101

  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中的执行计划,通过分析,他找到了某些重要的线索:

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

  2、发生了一个嵌套循环连接。

  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引擎将会应用这个公式计算出这一列的值,在执行查询时,数据库引擎将会计算订单总价,并为计算列返回结果。

金沙国际会员登录 23

  图 1 计算列

  使用计算列你可以将计算工作全部交给后端执行,但如果表的行数太多可能计算性能也不高,如果计算列出现在Select查询的where子句中情况会更糟,在这种情况下,为了匹配where子句指定的值,数据库引擎不得不计算表中所有行中计算列的值,这是一个低效的过程,因为它总是需要全表扫描或全聚集索引扫描。

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

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

  创建索引视图

  你是否知道可以在视图上创建索引?OK,不知道没关系,看了我的介绍你就明白了。

本文由金沙国际平台登录发布于金沙国际平台登录,转载请注明出处:我将在这个系列的文章中介绍如何编写最佳的数

您可能还会对下面的文章感兴趣: