当前位置: 首页 > 科技观察

加速SQL查询和避免锁定数据库的数据库代码的技巧

时间:2023-03-15 14:33:25 科技观察

由于数据库世界仍然相对不成熟,每个平台上的SQL开发人员都在苦苦挣扎,一遍又一遍地犯同样的错误。当然,数据库供应商正在取得一些进展,并继续努力解决更大的问题。无论SQL开发人员是在SQLServer、Oracle、DB2、Sybase、MySQL还是任何其他关系数据库平台上编写代码,并发性、资源管理、空间管理和速度仍然困扰着他们。部分问题在于没有灵丹妙药;对于几乎所有***做法,我都可以说出至少一个例外。我们说,调优数据库既是一门科学,也是一门艺术,并且有充分的理由,很少有全面适用的硬性规定。您在一个系统上解决的问题在另一个系统上不是问题,反之亦然。当谈到调优查询时,没有正确答案,但这并不意味着您应该放弃它。您可以遵循以下一些原则,您有望收到很好的结果。不使用UPDATE而不是CASE的问题很常见,但很难发现,并且经常被许多开发人员忽视,因为如此自然地使用UPDATE似乎合乎逻辑。以这个场景为例:你向一个临时表中插入数据,并且需要它在另一个值存在时显示某个值。也许您从Customer表中获取记录并希望将订单超过100,000美元的客户标记为“首选”。因此,您将数据插入表中,运行UPDATE语句,并将订单价值超过100,000美元的任何客户的CustomerRank列设置为“Preferred”。问题是,UPDATE语句被记录下来,这意味着每次写入表都会完成两次。解决方法:在SQL查询中使用内联CASE语句,检查每行的订单金额条件,并在写入表之前设置“Preferred”标志,处理性能得到显着提升。不盲目复用代码的问题也很普遍,很容易复制别人写的代码,因为你知道它会得到你需要的数据。问题是,它经常会获取太多你不需要的数据,而开发人员很少对其进行精简,所以你最终会得到大量数据。这通常表现为WHERE子句中的额外外部连接或额外条件。如果根据您的确切要求精简重用代码,则可以大大提高性能。需要多少列,提取多少列这个问题和第二个问题类似,但是它是列特有的。使用SELECT*对所有查询进行编码而不是一一列出列很容易。问题还在于它获取了太多不需要的数据,这是我见过无数次的错误。开发人员对具有120列和数百万行的表执行SELECT*查询,但***仅使用了其中的三或五列。因此,您处理的数据比您实际需要的多得多,查询返回结果真是奇迹。您不仅处理了太多不需要的数据,而且还在抢夺其他进程的资源。不要查询两次(double-dip)这是我看到很多人犯的另一个错误:写入存储过程,从具有数亿行的表中获取数据。一位开发人员想要提取有关居住在加利福尼亚且年收入超过40,000美元的客户的信息。因此,他查询居住在加利福尼亚州的客户并将结果放入一个临时表中。然后查询年收入大于40,000美元的客户,并将这些结果放入另一个临时表中。***他加入这两个表得到最终的结果。你在开玩笑吧?这应该通过一个查询来完成,而不是为一个非常大的表查询两次。别傻了:尝试只查询一次大表,您会发现存储过程执行得更快。一个稍微不同的场景是当一个过程中的几个步骤需要大表的一个子集时,导致每次都要查询大表。为避免此问题,只需查询此子集,将其保存在别处,然后将后续步骤指向此较小的数据集。知道何时使用临时表解决起来有点棘手,但它是有回报的。临时表可以在很多情况下使用,比如防止大表被查询两次。也可以使用临时表,大大降低连接大表所需的处理能力。如果一定要join一个表到一个大表,而大表是有条件的,只需要将大表中需要的部分数据提取到临时表中,然后与临时表join即可,可以提高查询性能。如果存储过程中有多个查询需要对同一个表执行类似的连接,它也会有很大帮助。预暂存数据是我最喜欢的主题之一,因为它是一种经常被忽视的旧方法。如果您有一个报表或存储过程(或它们的集合)要对大型表执行类似的连接操作,那么通过提前连接表并将它们持久存储在表中来预暂存数据可能会很好对你有帮助。现在可以针对这个预安排的表运行报告,避免大型连接。您不会总是能够使用此方法,但是当您这样做时,您会发现它绝对是节省服务器资源的好方法。注意:许多开发人员通过关注查询本身来避免此连接问题,从连接创建只读视图,这样他们就不必一遍又一遍地键入连接条件。但这种方法的问题是您仍然必须为每个需要它的报告运行查询。如果pre-stage数据,只需要运行一次连接(例如,报告前10分钟),其他人可以避免大连接。你不知道我有多喜欢这个技巧,在大多数环境中,一些公用表一直都在连接,所以没有理由不能先预留它们。批量删除和更新这是另一个经常被忽视的技巧,如果操作不当,从大表中删除或更新大量数据可能是一场噩梦。问题是,这两个语句作为单个事务运行。如果你需要敲定它们,或者系统在执行它们时遇到问题,系统不得不回滚整个事务,这需要很长时间。这些操作还会在持续时间内阻塞其他事务,这实际上给系统带来了瓶颈。解决方法是小批量删除或更新。这从几个方面解决了这个问题:无论事务因什么原因终止,它只有少量行要回滚,所以数据库恢复联机的速度要快得多。当小批量事务提交到磁盘时,其他事务可以进来做一些工作,从而大大提高并发性。还有,很多开发者一直固执地认为这些删除和更新操作必须在同一天完成。情况并非总是如此,尤其是在您提交申请时。如果你需要延长操作,你可以这样做,小批量有助于实现这一点;如果您需要更长的时间来执行这些密集型操作,请不要降低系统速度。使用临时表提高游标性能尽可能避免使用游标。游标不仅有速度问题,速度问题本身就是很多操作的大问题,还会导致你的操作长时间阻塞其他操作,大大降低系统的并发性。但是游标的使用总是不可避免的,当无法避免游标时,可以通过对临时表进行游标操作来代替游标带来的性能问题。例如,考虑一个查找表并根据某些比较更新一些列的游标。您可以将该数据放入临时表中,并与临时表而不是实时表进行比较。然后,您可以对锁定时间非常短的小得多的活动表运行单个UPDATE语句。进行此类数据修改可以大大提高并发性。***我会说你根本不需要使用游标,总会有一个基于集合的解决方法。使用表值函数这是我一直以来最喜欢的技巧之一,因为这是只有专家才知道的秘密。当在查询的SELECT列表中使用标量函数时,会为结果集中的每一行调用该函数,这会显着降低大型查询的性能。但是,可以将标量函数转换为表值函数,然后在查询中使用CROSSAPPLY,可以大大提高性能。这个绝妙的技巧可以显着提高性能。不要在同一批次中对许多表执行大型操作这似乎很明显,但事实并非如此。我将使用另一个实例,因为它更具说明性。我有一个严重阻塞的系统,许多操作停滞不前。发现每天运行几次的删除例程在显式事务中删除了14个表的数据。在一个事务中处理所有14个表意味着锁定每个表,直到所有删除完成。解决方案是将每个表删除分解成一个单独的事务,使每个删除事务只锁定一个表。这会释放其他表,解除阻塞,并允许其他操作继续进行。您应该始终将此类大交易分解为单独的小交易以防止阻塞。不要使用触发器这个与前一个大致相同,但仍然值得一提。触发器的问题:无论你想让触发器做什么,它都会在与原始操作相同的事务中完成。如果您编写一个触发器,在更新Orders表中的一行时将数据插入另一个表,则两个表都将被锁定,直到触发器完成执行。如果需要在更新后向另一个表中插入数据,将更新和插入放入存储过程中,并在单独的事务中执行。如果需要回滚,不用锁住两个表也很容易回滚。一如既往,保持事务简短并且一次不要锁定多个资源。不要聚集在GUID这么多年之后,我不敢相信我们仍然在为这个问题而苦苦挣扎。但我仍然每年至少遇到两次GUID集群。一个GUID(GloballyUniqueIdentifier)是一个16字节的随机生成的数字。按此列对表中的数据进行排序比使用稳定增加的值(例如DATE或IDENTITY)更快地对表进行分段。几年前我做了一个基准测试,我将一堆数据插入到一个具有聚集GUID的表中,并将相同的数据插入到另一个具有IDENTITY列的表中。GUID表碎片非常严重,仅15分钟后,性能下降了数千个百分点。5小时后,IDENTITY表的性能仅下降了几个百分点,这不仅适用于GUID,适用于任何易变列。如果您只需要检查数据是否存在,请不要计算行数。您经常需要检查数据是否存在于表中,并根据检查结果执行某些操作。我经常看到人们做SELECTCOUNT(*)FROMdbo.T1来检查数据是否存在:SET@CT=(SELECTCOUNT(*)FROMdbo.T1);If@CT>0BEGINEND这是完全没有必要的,如果你要检查数据是否存在,只需执行:IfEXISTS(SELECT1FROMdbo.T1)BEGINEND不要计算表中的所有内容,只需获取找到的***行。SQLServer足够聪明,可以正确使用EXISTS,第二段代码返回速度超快。表越大,这方面的差距越明显。在数据变得过大之前做正确的事情。调整数据库永远不会太早。实际上,我只是在我的一个生产数据库上针对一个包含2.7亿行的表运行了这个示例。第一个查询耗时15秒,包含456197次逻辑读取,第二个查询在不到1秒的时间内返回结果,仅包含5次逻辑读取。但是,如果你真的需要统计一个表的行数,而且这个表很大,另一种方法是从系统表中提取,SELECTrowsfromsysindexes会得到所有索引的行数。由于聚集索引代表数据本身,只需添加WHEREindid=1即可获取表行,然后仅包含表名即可。所以,最好的查询是:SELECTrowsfromsysindexeswhereobject_name(id)='T1'andindexid=1在我2.7亿行的表中,不到1秒就返回结果,只有6次逻辑读,现在性能不一样了。不要做反向搜索以简单查询SELECT*FROMCustomersWHERERegionID<>3为例。您不能对这个查询使用索引,因为它是一个反向搜索,需要对表进行逐行比较扫描。如果您需要执行这样的任务,您可能会发现,如果重写查询以使用索引,性能会好很多。这个查询很容易重写,像这样:SELECT*FROMCustomersWHERERegionID<3UNIONALLSELECT*FROMCustomersWHERERegionID这个查询将使用索引,所以如果你的数据集很大,它的性能会比表扫描版本好很多。当然,没有那么容易,也许性能更差,所以在使用之前先尝试一下。它绝对有效,尽管涉及的因素太多。***,我意识到这个查询违反了规则4:不要查询两次,但也说明没有硬性规定。虽然我们在这里查询了两次,但这样做是为了避免昂贵的表扫描。你不可能一直使用所有这些技术,但如果你牢记它们,总有一天你会用它们来解决一些大问题。最重要的是要记住,不要把我所说的当成教条。在您的环境中尝试一下,相同的解决方案并非在所有情况下都适用,但我一直在对性能不佳进行故障排除时使用它们,并且它奏效了。