数据库设计与维护
很大程度上,这是决定数据驱动的应用程序或网站的吞吐量的最重要的决定因素,并且经常被程序员完全忽略。
如果您没有使用适当的规范化技术,那么您的网站必定会失败。如果您没有主键,几乎每个查询的速度都会很慢。如果您无缘无故地使用众所周知的反模式(例如,将表用于键-值对(AKA实体-属性-值)),则会激增物理读取和写入的次数。
如果您不利用数据库提供的功能,例如页面压缩,FILESTREAM
存储(用于二进制数据),SPARSE
列,hierarchyid
用于层次结构等(所有SQL Server示例),那么您将无法在数据库附近找到任何内容。您可能会看到的性能。
在设计了数据库并说服自己至少在目前为止它可能达到的最佳性能之后,您应该开始担心数据访问策略。
渴望与懒惰加载
大多数ORM使用一种称为延迟加载关系的技术,这意味着默认情况下,它将一次加载一个实体(表行),并且每次需要加载一个或多个相关(外部)时都要往返数据库。键)行。
这不是一件好事或坏事,而是取决于数据实际要执行的操作以及您预先知道多少。有时,延迟加载绝对是正确的选择。例如,NHibernate可以决定根本不查询任何内容,而仅生成特定ID 的代理。如果您只需要ID本身,为什么还要索取更多呢?另一方面,如果您尝试在3级层次结构中打印每个元素的树,则延迟加载将成为O(N²)操作,这对性能非常不利。
使用“纯SQL”(即原始ADO.NET查询/存储过程)的一个有趣的好处是,它基本上迫使您考虑显示任何给定屏幕或页面所需的数据是什么。ORM和延迟加载功能并不能阻止您执行此操作,但是它们确实为您提供了……好吧,懒惰,并意外地激增了您执行的查询数量。因此,您需要了解ORM的急切加载功能,并始终警惕要为任何给定页面请求发送到服务器的查询数量。
快取
所有主要的ORM都维护一个一级缓存,也就是“身份缓存”,这意味着如果您通过其ID两次请求同一实体,则不需要第二次往返,并且(如果您正确地设计了数据库, )使您能够使用开放式并发。
在L2S和EF中,L1缓存非常不透明,您必须信任它的正常运行。NHibernate对此更为明确(Get
/ Load
vs. Query
/ QueryOver
)。不过,只要您尝试通过ID进行查询,此处就可以了。许多人忘记了L1高速缓存,并通过其ID(即查找字段)以外的内容反复重复查找同一实体。如果需要执行此操作,则应保存ID或整个实体,以备将来查找。
还有一个2级缓存(“查询缓存”)。NHibernate具有此内置功能。Linq to SQL和Entity Framework具有已编译的查询,可以通过编译查询表达式本身来帮助大大降低应用服务器的负载,但它不缓存数据。微软似乎认为这是应用程序问题,而不是数据访问问题,这是L2S和EF的主要弱点。不用说,这也是“原始” SQL的弱点。为了使用NHibernate以外的任何ORM都能获得真正好的性能,您需要实现自己的缓存外观。
EF4还有一个L2缓存“扩展”,这是可以的,但实际上并不是批发替代应用程序级缓存。
查询数量
关系数据库基于数据集。它们确实很擅长在短时间内生成大量数据,但是在查询延迟方面却远远不及后者,因为每个命令都涉及一定的开销。设计良好的应用程序应发挥DBMS的优势,并尝试最小化查询数量并最大化每个查询中的数据量。
现在,我并不是说只需要一行时就查询整个数据库。我想说的是,如果你需要Customer
,Address
,Phone
,CreditCard
,和Order
排在同一时间,以满足单页,那么你应该问他们都在同一时间,不另行执行每个查询。有时情况更糟糕的是,您将看到代码Customer
连续查询5次相同的记录,首先要获取Id
,然后是Name
,然后是EmailAddress
,然后...这简直是效率低下。
即使您需要执行全部对完全不同的数据集运行的几个查询,将所有查询作为单个“脚本”发送到数据库并让其返回多个结果集通常更为有效。这是您所关心的开销,而不是数据总量。
这听起来像是常识,但通常常常很容易失去对应用程序各个部分中正在执行的所有查询的跟踪。您的成员资格提供者查询用户/角色表,标题操作查询购物车,菜单操作查询站点地图表,侧边栏操作查询特色产品列表,然后您的页面可以分为几个独立的自治区域,分别查询“订单历史记录”,“最近浏览过的”,“类别”和“库存”表,在不知不觉中,您将执行20个查询,甚至无法开始为该页面提供服务。它只会完全破坏性能。
一些框架-我在这里主要考虑的是NHibernate-对此非常聪明,可以让您使用称为Future的东西来对整个查询进行批处理,并尝试在可能的最后一刻立即执行所有查询。AFAIK,如果您想使用任何Microsoft技术来做到这一点,您就自己决定了;您必须将其构建到应用程序逻辑中。
索引,谓词和投影
我与之交谈的开发人员中至少有50%,甚至某些DBA似乎都难以理解覆盖索引的概念。他们认为,“好吧,该Customer.Name
列已建立索引,因此我对名称所做的每次查找都应该很快。” 除非Name
索引无法覆盖您要查找的特定列,否则它将无法正常工作。在SQL Server中,这是通过语句完成INCLUDE
的CREATE INDEX
。
如果您天真地SELECT *
在任何地方使用-除非您明确指定使用投影,否则几乎每个ORM都会这样做-DBMS可能会完全选择忽略索引,因为它们包含未覆盖的列。例如,投影意味着不这样做:
from c in db.Customers where c.Name == "John Doe" select c
您可以这样做:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
这将对于大多数现代的ORM,指示它只是去查询Id
和Name
这大概是由索引覆盖的列(而不是Email
,LastActivityDate
或任何其他你碰巧在那里坚持列)。
使用不适当的谓词也很容易完全吹走所有索引的好处。例如:
from c in db.Customers where c.Name.Contains("Doe")
...看起来几乎与我们之前的查询相同,但实际上将导致全表扫描或索引扫描,因为它转换为LIKE '%Doe%'
。同样,另一个看起来很简单的查询是:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
假设您在上有一个索引BirthDate
,则该谓词有很大的机会使其完全无用。我们这里的假设程序员显然尝试创建一种动态查询(“如果指定了该参数,则仅过滤出生日期”),但这不是正确的方法。改为这样写:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
...现在,数据库引擎知道如何对其进行参数化并执行索引查找。对查询表达式的一个微小的,看似微不足道的更改会极大地影响性能。
不幸的是,通常LINQ使得编写这样的错误查询变得非常容易,因为有时提供程序能够猜测您试图执行的操作并优化查询,而有时却不能。因此,最终您会得到令人沮丧的不一致结果,如果您只是编写普通的旧SQL,则结果将是显而易见的(无论如何,对于有经验的DBA)。
基本上,所有这些都归结为以下事实:您必须密切关注生成的SQL及其所导致的执行计划,并且,如果您没有获得预期的结果,请不要害怕绕过偶尔对ORM层进行手工编码,然后对SQL进行编码。这适用于任何 ORM,而不仅仅是EF。
交易和锁定
您是否需要显示最新的数据(毫秒)?也许-这取决于-但可能不是。可悲的是,实体框架没有给您nolock
,您只能READ UNCOMMITTED
在事务级别(而不是表级别)使用。实际上,没有一个ORM对此特别可靠。如果要进行脏读,则必须降到SQL级别并编写临时查询或存储过程。因此,归根结底,就是您在框架内进行此操作的难易程度。
实体框架在这方面已经走了很长的路要走-EF(在.NET 3.5中)的版本1真是太糟糕了,这使得突破“实体”抽象变得异常困难,但是现在您有了ExecuteStoreQuery和Translate,所以它真的还不错 与这些人交朋友,因为您会经常使用它们。
还有写锁定和死锁的问题,以及在数据库中保持锁定的时间尽可能短的一般做法。在这方面,大多数ORM(包括Entity Framework)实际上往往比原始SQL 更好,因为它们封装了工作单元模式,在EF中为SaveChanges。换句话说,只要您愿意,就可以“插入”,“更新”或“删除”实体到您的心脏内容中,以确保在您提交工作单元之前,实际上不会有任何更改被推送到数据库中。
请注意,UOW 与长时间运行的事务不相似。UOW仍然使用ORM的乐观并发功能,并跟踪内存中的所有更改。在最终提交之前,不会发出任何DML语句。这样可以使交易时间尽可能短。如果使用原始SQL构建应用程序,则很难实现这种延迟行为。
这对于EF特别意味着什么:使您的工作单元尽可能地粗糙,并且在绝对需要之前不要提交它们。这样做,您将得到比随机使用单个ADO.NET命令低得多的锁争用。
EF对于高流量/高性能应用程序来说完全合适,就像其他所有框架对于高流量/高性能应用程序一样。重要的是如何使用它。这是最流行的框架及其在性能方面提供的功能的快速比较(图例:N =不支持,P =部分,Y =是/受支持):
如您所见,EF4(当前版本)的表现还不错,但是如果您最关心的是性能,那可能并不是最好的选择。NHibernate在这一领域更加成熟,甚至Linq to SQL也提供了EF仍然没有的一些性能增强功能。对于非常特定的数据访问方案,原始ADO.NET通常会更快,但是,如果将所有部分放在一起,它实际上并没有提供从各种框架中获得的许多重要好处。