除非你遵循本文介绍的这些技巧,不然很容易编写出减慢查询速度或锁死数据库的数据库代码。前端
因为数据库领域仍相对不成熟,每一个平台上的 SQL 开发人员都在苦苦挣扎,一次又一次犯一样的错误。程序员
固然,数据库厂商在取得一些进展,并继续在竭力处理较重大的问题。数据库
不管 SQL 开发人员在 SQL Server、Oracle、DB二、Sybase、MySQL,仍是在其余任何关系数据库平台上编写代码,并发性、资源管理、空间管理和运行速度都仍困扰着他们。后端
问题的一方面是,不存在什么灵丹妙药;针对几乎每条最佳实践,我均可以举出至少一个例外。缓存
一般,开发人员找到本身青睐的方法,而懒得研究其余方法。这也许是缺少教育的表现,或者开发人员没有认识到本身什么时候作错了。也许针对一组本地测试数据,查询运行起来顺畅,可是换成生产级系统,表现就差强人意。服务器
我没有指望 SQL 开发人员成为管理员,但他们在编写代码时必须考虑到生产级环境的问题。若是他们在开发初期不这么作,数据库管理员后期会让他们返工,遭殃的就是用户。网络
咱们说调优数据库既是门艺术,又是门科学,这是有道理的,由于不多有全面适用的硬性规则。你在一个系统上解决的问题在另外一个系统上不是问题,反之亦然。并发
说到调优查询,没有正确的答案,但这并不意味着就此应该放弃。你能够遵循如下17条原则,有望收到很好的效果。函数
不要用 UPDATE 代替 CASE高并发
这个问题很常见,却很难发觉,许多开发人员经常忽视这个问题,缘由是使用 UPDATE 再天然不过,这彷佛合乎逻辑。
以这个场景为例:你把数据插入一个临时表中,若是另外一个值存在,须要它显示某个值。
也许你从 Customer 表中提取记录,想把订单金额超过 100000 美圆的客户标记为“Preferred”。
于是,你将数据插入到表中,运行 UPDATE 语句,针对订单金额超过 100000 美圆的任何客户,将 CustomerRank 这一列设为“Preferred”。
问题是,UPDATE 语句记入日志,这就意味着每次写入到表中,要写入两次。
解决办法:在 SQL 查询中使用内联 CASE 语句,这检验每一行的订单金额条件,并向表写入“Preferred”标记以前,设置该标记,这样处理性能提高幅度很惊人。
不要盲目地重用代码
这个问题也很常见,咱们很容易拷贝别人编写的代码,由于你知道它能获取所需的数据。
问题是,它经常获取过多你不须要的数据,而开发人员不多精简,所以到头来是一大堆数据。
这一般表现为 WHERE 子句中的一个额外外链接或额外条件。若是你根据本身的确切要求精简重用的代码,就能大幅提高性能。
须要几列,就提取几列
这个问题相似第 2 个问题,但这是列所特有的。很容易用 SELECT* 来编写全部查询代码,而不是把列逐个列出来。
问题一样是,它提取过多你不须要的数据,这个错误我见过无数次了。开发人员对一个有 120 列、数百万行的表执行 SELECT* 查询,但最后只用到其中的三五列。
所以,你处理的数据比实际须要的多得多,查询返回结果是个奇迹。你不只处理过多不须要的数据,还夺走了其余进程的资源。
不要查询两次(double-dip)
这是我看到好多人犯的另外一个错误:写入存储过程,从一个有数亿行的表中提取数据。
开发人员想提取住在加利福尼亚州,年收入高于 4 万美圆的客户信息。因而,他查询住在加利福尼亚州的客户,把查询结果放到一个临时表中。
而后再来查询年收入高于 4 万美圆的客户,把那些结果放到另外一个临时表中。最后他链接这两个表,得到最终结果。
你是在逗我吧?这应该用一次查询来完成,相反你对一个超大表查询两次。别犯傻了:大表尽可能只查询一次,你会发现存储过程执行起来快多了。
一种略有不一样的场景是,某个过程的几个步骤须要大表的一个子集时,这致使每次都要查询大表。
想避免这个问题,只需查询这个子集,并将它持久化存储到别处,而后将后面的步骤指向这个比较小的数据集。
知道什么时候使用临时表
这个问题解决起来要麻烦一点,但效果显著。在许多状况下可使用临时表,好比防止对大表查询两次。还可使用临时表,大幅减小链接大表所需的处理能力。
若是你必须将一个表链接到大表,该大表上又有条件,只需将大表中所需的那部分数据提取到临时表中,而后再与该临时表链接,就能够提高查询性能。
若是存储过程当中有几个查询须要对同一个表执行相似的链接,这一样大有帮助。
预暂存数据
这是我最爱聊的话题之一,由于这是一种常常被人忽视的老方法。
若是你有一个报表或存储过程(或一组)要对大表执行相似的链接操做,经过提早链接表,并将它们持久化存储到一个表中来预暂存数据,就能够对你大有帮助。
如今,报表能够针对该预暂存表来运行,避免大链接。你并不是老是可使用这个方法,但一旦用得上,你会发现这绝对是节省服务器资源的好方法。
请注意:许多开发人员避开这个链接问题的作法是,将注意力集中在查询自己上,根据链接建立只读视图,那样就没必要一次又一次键入链接条件。
但这种方法的问题是,仍要为须要它的每一个报表运行查询。若是预暂存数据,你只要运行一次链接(好比说报表前 10 分钟),别人就能够避免大链接了。
你不知道我有多喜欢这一招,在大多数环境下,有些经常使用表一直被链接起来,因此没理由不能先预暂存起来。
批量删除和更新
这是另外一个常常被忽视的技巧,若是你操做不当,删除或更新来自大表的大量数据多是一场噩梦。
问题是,这两种语句都做为单一事务来运行。若是你须要终结它们,或者它们在执行时系统遇到了问题,系统必须回滚(roll back)整个事务,这要花很长的时间。
这些操做在持续期间还会阻塞其余事务,实际上给系统带来了瓶颈,解决办法就是,小批量删除或更新。
这经过几个方法来解决问题:
不管事务因什么缘由而被终结,它只有少许的行须要回滚,那样数据库联机返回快得多。
小批量事务被提交到磁盘时,其余事务能够进来处理一些工做,于是大大提升了并发性。
一样,许多开发人员一直执拗地认为:这些删除和更新操做必须在同一天完成。事实并不是老是如此,若是你在归档更是如此。
若是你须要延长该操做,能够这么作,小批量有助于实现这点;若是你花更长的时间来执行这些密集型操做,切忌拖慢系统的运行速度。
使用临时表来提升游标性能
若是可能的话,最好避免游标。游标不只存在速度问题,而速度问题自己是许多操做的一大问题,还会致使你的操做长时间阻塞其余操做,这大大下降了系统的并发性。
然而没法老是避免使用游标,避免不了使用游标时,能够改而对临时表执行游标操做,以此摆脱游标引起的性能问题。
不妨以查阅一个表,基于一些比较结果来更新几个列的游标为例。你也许能够将该数据放入临时表中,而后针对临时表进行比较,而不是针对活动表进行比较。
而后你能够针对小得多,锁定时间很短的活动表运行单一的 UPDATE 语句。
进行这样的数据修改可大大提升并发性。最后我要说,你根本不须要使用游标,老是会有一种基于集合的解决方法。
不要嵌套视图
视图也许很方便,不过使用视图时要当心。
虽然视图有助于将庞大查询遮掩起来、无须用户操心,并实现数据访问标准化,但你很容易发现本身陷入这种困境:视图 A 调用视图 B,视图 B 调用视图 C,视图 C 又调用视图 D,这就是所谓的嵌套视图。
这会致使严重的性能问题,尤为是这两方面:
返回的数据颇有可能比你须要的多得多。
查询优化器将放弃并返回一个糟糕的查询方案。
我遇到过喜欢嵌套视图的客户,这个客户有一个视图用于几乎全部数据,由于它有两个重要的链接。
问题是,视图返回的一个列里面竟然有 2MB 大小的文档,有些文档甚至更大。
在运行的几乎每一次查询中,这个客户要在网络上为每一行至少多推送 2MB 的数据。天然,查询性能糟糕透顶。
没有一个查询实际使用该列!固然,该列被埋在七个视图的深处,要找出来都很难。我从视图中删除该文档列后,最大查询的时间从 2.5 小时缩短至 10 分钟。
我最后层层解开了嵌套视图(有几个没必要要的链接和列),并写了一个普通的查询,结果一样这个查询的时间缩短至不到 1 秒。
使用表值函数
这是一直以来我最爱用的技巧之一,由于它是只有专家才知道的那种秘诀。
在查询的 SELECT 列表中使用标量函数时,该函数因结果集中的每一行而被调用,这会大幅下降大型查询的性能。
然而能够将标量函数转换成表值函数,而后在查询中使用 CROSS APPLY,就能够大幅提高性能,这个奇妙的技巧能够显著提高性能。
使用分区避免移动大型数据
不是每一个人都能利用依赖 SQL Server Enterprise 中分区的这个技巧,可是对于能利用它的人来讲,这个技巧很棒。
大多数人没有意识到 SQL Server 中的全部表都是分区的。若是你喜欢,能够把一个表分红多个分区,但即便简单的表也从建立那一刻起就分区了。
然而,它们是做为单个分区建立的。若是你在运行 SQL Server Enterprise,已经能够随时享用分区表的优势了。
这意味着你可使用 SWITCH 之类的分区功能,归档来自仓库加载的大量数据。
举个实际例子,去年我碰到过这样一个客户:该客户须要将数据从当日的表复制到归档表中;那样万一加载失败,公司能够迅速用当日的表来恢复。
因为各类缘由,没法每次将表的名称改来改去,因此公司天天在加载前将数据插入到归档表中,而后从活动表删除当日的数据。
这个过程一开始很顺利,但一年后,复制每一个表要花 1 个半小时,天天要复制几个表,问题只会愈来愈糟。
解决办法是抛弃 INSERT 和 DELETE 进程,使用 SWITCH 命令。
SWITCH 命令让该公司得以免全部写入,由于它将页面分配给了归档表。
这只是更改了元数据,SWITCH 运行平均只要两三秒钟,若是当前加载失败,你能够经过 SWTICH 将数据切换回到原始表。
若是你非要用 ORM,请使用存储过程
ORM 是我常常炮轰的对象之一。简而言之,别使用 ORM(对象关系映射器)。
ORM 会生成世界上最糟糕的代码,我遇到的几乎每一个性能问题都是由它引发的。
相比知道本身在作什么的人,ORM 代码生成器不可能写出同样好的 SQL。可是若是你使用 ORM,那就编写本身的存储过程,让 ORM 调用存储过程,而不是写本身的查询。
我知道使用 ORM 的种种理由,也知道开发人员和经理都喜欢 ORM,由于它们有助于产品迅速投向市场。可是若是你看一下查询对数据库作了什么,就会发现代价过高了。
存储过程有许多优势,首先,你在网络上推送的数据少得多。若是有一个长查询,那么它可能在网络上要往返三四趟才能让整个查询到达数据库服务器。
这不包括服务器将查询从新组合起来并运行所花的时间;另外考虑这点:查询可能每秒运行几回或几百次。
使用存储过程可大大减小传输的流量,由于存储过程调用老是短得多。另外,存储过程在 Profiler 或其余任何工具中更容易追踪。
存储过程是数据库中的实际对象,这意味着相比临时查询(ad-hoc query),获取存储过程的性能统计数字要容易得多,于是发现性能问题、查明异常状况也要容易得多。
此外,存储过程参数化更一致,这意味着你更可能会重用执行方案,甚至处理缓存问题,要查明临时查询的缓存问题很难。
有了存储过程,处理边界状况(edge case),甚至增长审计或变动锁定行为变得容易多了。存储过程能够处理困扰临时查询的许多任务。
几年前,我妻子理清了 Entity Framework 的一个两页长的查询,该查询花了 25 分钟来运行。
她化繁为简,将这个大型查询改写为 SELECT COUNT(*) fromT1,这不是开玩笑。
那些只是要点,我知道,许多 .NET 程序员认为业务逻辑不适宜放在数据库中,这大错特错。
若是将业务逻辑放在应用程序的前端,仅仅为了比较就得将全部数据传送一遍,那样不会有好的性能。
我有个客户将全部逻辑保存在数据库的外面,在前端处理一切。该公司将成千上万行数据发送到前端,以便可以运用业务逻辑,并显示所需的数据。
这个过程花了 40 分钟,我把存储过程放在后端,让它从前端调用;页面在三秒钟内加载完毕。
固然,有时逻辑适宜放在前端上,有时适宜放在数据库中,可是 ORM 老是让我上火。
不要对同一批次的许多表执行大型操做
这个彷佛很明显,但实则否则。我会用另外一个鲜活的例子,由于它更能说明问题。
我有一个系统存在大量的阻塞,众多操做处于停滞状态。结果查明,天天运行几回的删除例程在删除显式事务中 14 个表的数据。处理一个事务中的全部 14 个表意味着,锁定每一个表,直到全部删除完成。
解决办法就是,将每一个表的删除分解成单独的事务,以便每一个删除事务只锁定一个表。
这解放了其余表,缓解了阻塞,让其余操做得以继续运行。你老是应该把这样的大事务分解成单独的小事务,以防阻塞。
不要使用触发器
这个与前一个大致同样,但仍是值得一提。触发器的问题:不管你但愿触发器执行什么,都会在与原始操做同一个的事务中执行。
若是你写一个触发器,以便更新 Orders 表中的行时将数据插入到另外一个表中,会同时锁定这两个表,直到触发器执行完毕。
若是你须要在更新后将数据插入到另外一个表中,要将更新和插入放入到存储过程当中,并在单独的事务中执行。
若是你须要回滚,就很容易回滚,没必要同时锁定这两个表。与往常同样,事务要尽可能短小,每次不要锁定多个资源。
不要在 GUID 上聚类
这么多年后,我难以相信咱们竟然还在为这个问题而苦恼。但我仍然每一年遇到至少两次聚类 GUID。
GUID(全局惟一标识符)是一个 16 字节的随机生成的数字。相比使用一个稳定增长的值(好比 DATE 或 IDENTITY),按此列对你表中的数据进行排序致使表碎片化快得多。
几年前我作过一项基准测试,我将一堆数据插入到一个带聚类 GUID 的表中,将一样的数据插入到另外一个带 IDENTITY 列的表中。
GUID 表碎片化极其严重,仅仅过了 15 分钟,性能就降低了几千个百分点。
5 小时后,IDENTITY 表的性能才降低了几个百分点,这不只仅适用于 GUID,它适用于任何易失性列。
若是只需查看数据是否存在,就不要计数行
这种状况很常见,你须要查看数据存在于表格中,根据这番检查的结果,你要执行某个操做。
我常常见到有人执行 SELECT COUNT(*)FROMdbo.T1来检查该数据是否存在:
SET @CT=(SELECT COUNT(*) FROM
dbo.T1);
If@CT>0
BEGIN
<Do something>
END
这彻底不必,若是你想检查数据是否存在,只要这么作:
If EXISTS (SELECT 1 FROM dbo.T1)
BEGIN
<Do something>
END
不要计数表中的一切,只要取回你找到的第一行。SQL Server 聪明得很,会正确使用 EXISTS,第二段代码返回结果超快。
表越大,这方面的差距越明显。在你的数据变得太大以前作正确的事情。调优数据库永不嫌早。
实际上,我只是在个人其中一个生产数据库上运行这个例子,针对一个有 2.7 亿行的表。
第一次查询用时 15 秒,包含 456197 个逻辑读取,第二次查询不到 1 秒就返回结果,只包含 5 个逻辑读取。
然而若是你确实须要计数表的行数,表又很大,另外一种方法就是从系统表中提取,SELECT rows fromsysindexes 将为你得到全部索引的行数。
又因为聚类索引表明数据自己,因此只要添加 WHERE indid = 1,就能得到表行,而后只需包含表名称便可。
因此,最后的查询是:
SELECT rows from sysindexes where object_name(id)='T1'and indexid =1
在我 2.7 亿行的表中,不到 1 秒就返回结果,只有 6 个逻辑读取,如今性能不同了。
不要进行逆向搜索
以简单的查询 SELECT * FROMCustomers WHERE RegionID <> 3 为例。
你不能将索引与该查询结合使用,由于它是逆向搜索,须要借助表扫描来逐行比较。
若是你须要执行这样的任务,可能发现若是重写查询以使用索引,性能会好得多。
该查询很容易重写,就像这样:
SELECT * FROM Customers WHERE RegionID<3 UNION ALL SELECT * FROM Customers WHERE RegionID
这个查询将使用索引,因此若是你的数据集很大,其性能会远赛过表扫描版本。
固然,没有什么是那么容易的,也许性能更糟,因此使用以前先试一下。它百分之百管用,虽然涉及太多的因素。
最后,我意识到这个查询违反了第 4 条规则:不要查询两次,但这也代表没有硬性规则。虽然咱们在这里查询两次,但这么作是为了不开销很大的表扫描。
你没法一直运用全部这些技巧,但若是牢记它们,有一天你会用它们来解决一些大问题。
要记住的最重要一点是,别将我说的话当成教条。在你的实际环境中试一下,一样的解决办法不是在每种状况下都管用,不过我排查糟糕的性能时一直使用这些方法,并且屡试不爽。