对于性能分析,我建议首先查看的是 SQL 分析器。这可以捕获 EF 正在运行的确切 SQL 语句,并帮助识别可能的性能罪魁祸首。我介绍了其中的一些here。架构问题可能是最相关的起点。标题以 MVC 为目标,但大多数项目与 WPF 和任何应用程序有关。
我用于 SQL Server 的一个好的、简单的分析器是 ExpressProfiler。 (https://github.com/OleksiiKovalov/expressprofiler)
随着迁移到新服务器,它现在通过网络发送数据而不是从本地数据库中提取数据,您注意到的性能问题很可能属于“加载过多”的类别经常”。现在您不仅要等待数据库加载数据,还要等待它打包并通过网络发送。此外,新数据库是否代表相同的数据量并且只为单个客户端提供服务,还是现在为多个客户端提供服务?开发人员的其他问题是“在我的机器上工作”,其中本地测试数据库较小并且不处理来自服务器的并发查询。 (锁等会影响性能)
从这里开始,使用隔离的数据库服务器(没有其他客户端访问它以减少“噪音”)运行应用程序的副本,并在其上运行分析器。需要注意的事项:
延迟加载 - 在这种情况下,您有查询来加载数据,但随后会看到大量(数十到数百)个附加查询被剥离。您的代码可能会说“运行此查询并填充此数据”,您期望它应该是 1 个 SQL 查询,但是通过触摸延迟加载的属性,这可以衍生出许多其他查询。
延迟加载的解决方案:如果您需要额外的数据,请使用.Include() 急切加载。如果您只需要一些数据,请考虑使用.Select() 来选择您需要的数据的视图模型/DTO,而不是依赖完整的实体。这将消除延迟加载场景,但可能需要对代码进行一些重大更改才能使用视图模型/dto。像 Automapper 这样的工具在这里可以提供很大帮助。阅读 .ProjectTo() 以了解 Automapper 如何与 IQueryable 一起工作以消除延迟加载命中。
阅读过多 - 加载实体可能会很昂贵,尤其是在您不需要所有这些数据的情况下。性能的罪魁祸首包括过度使用.ToList(),这将实现需要数据子集的整个实体集,或者简单的存在检查或计数就足够了。例如,我见过这样的代码:
var data = context.MyObjects.SingleOrDefault(x => x.IsActive && x.Id = someId);
return (data != null);
这应该是:
var isData = context.MyObjects.Where(x => x.IsActive && x.Id = someId).Any();
return isData;
两者之间的区别在于,在第一个示例中,EF 将有效地执行 SELECT * 操作,因此在存在数据的情况下,它会将所有列返回到实体中,仅稍后检查实体是否展示。第二条语句将运行一个更快的查询来简单地返回一行是否存在。
var myDtos = context.MoyObjects.Where(x => x.IsActive && x.ParentId == parentId)
.ToList()
.Select( x => new ObjectDto
{
Id = x.Id,
Name = x.FirstName + " " + x.LastName,
Balance = calculateBalance(x.OrderItems.ToList()),
Children = x.Children.ToList()
.Select( c => new ChildDto
{
Id = c.Id,
Name = c.Name
}).ToList()
}).ToList();
这样的语句可以继续执行并且变得相当复杂,但真正的问题是 .Select() 之前的 .ToList()。由于开发人员试图做一些 EF 不理解的事情,例如调用方法,这些通常会悄悄出现。 (即calculateBalance()),它通过首先调用.ToList()“工作”。这里的问题是,此时您正在具体化整个实体并切换到 Linq2Object。这意味着对相关数据(例如 .Children)的任何“触摸”现在都将触发延迟加载,并且进一步的 .ToList() 调用可以使更多数据饱和到内存中,否则这些数据可能会在查询中减少。要注意的罪魁祸首是.ToList() 呼叫并尝试删除它们。在调用 .ToList() 之前选择更简单的值,然后将该数据输入到视图模型中,视图模型可以计算结果数据。
我见过的最糟糕的罪魁祸首是开发人员想在 Where 子句中使用函数:
var data = context.MyObjects.ToList().Where(x => calculateBalance(x) > 0).ToList();
第一个ToList() 语句将尝试使整个表饱和到内存中的实体。除了加载所有这些数据所需的时间/内存/带宽之外,一个巨大的性能影响仅仅是数据库必须进行的锁定数以可靠地读取/写入数据。您“触摸”的行越少,触摸它们的时间越短,您的查询就可以更好地处理来自多个客户端的并发操作。随着系统过渡到被更多用户使用,这些问题会大大放大。
如果您已经消除了额外的延迟加载和不必要的查询,接下来要看的是查询性能。对于看起来很慢的操作,将 SQL 语句从分析器中复制出来并在数据库中运行,同时查看执行计划。这可以提供有关可以添加以加快查询速度的索引的提示。同样,使用.Select() 可以通过更有效地使用索引并减少服务器需要拉回的数据量来大大提高查询性能。
对于文件存储:这些是作为列存储在相关表中还是在链接到相关记录的单独表中?我的意思是,如果您有发票记录,并且还有保存在数据库中的发票文件的副本,是不是:
发票
或
发票
发票文件
将大的、很少使用的数据保存在单独的表中而不是与常用数据组合是一种更好的结构。这使得加载实体的查询小而快,可以在需要时按需提取昂贵的数据。
如果您使用 GUID 作为键(而不是整数/长整数),您是否在利用 newsequentialid()? (假设 SQL Server)键设置为使用 newid() 或在代码中,Guid.New() 将导致索引碎片和性能不佳。如果您通过数据库默认值填充 ID,请将它们切换为使用 newsequentialid() 来帮助减少碎片。如果您通过代码填充 ID,请查看编写一个模仿 newsequentialid() (SQL Server) 或适合您的数据库的模式的 Guid 生成器。 SQL Server 与 Oracle 存储/索引 GUID 值不同,因此将 UUID 字节的“类静态”部分置于数据的高阶与低阶字节将有助于索引性能。还要考虑索引维护和其他数据库维护工作,以帮助保持数据库服务器高效运行。
谈到索引调优,数据库服务器报告是您的朋友。在您从代码中消除了大部分,或者至少是一些严重的性能违规者之后,接下来就是查看您的系统的实际使用情况。了解代码/索引调查目标的最佳方法是数据库服务器识别的最常用和问题查询。在这些是 EF 查询的情况下,您通常可以根据 EF 查询负责的命中表进行逆向工程。获取这些查询并通过执行计划提供它们,以查看是否有可能有帮助的索引。索引是开发人员要么忘记,要么过早关注的事情。太多的索引可能和太少一样糟糕。我发现最好在决定添加哪些索引之前监控实际使用情况。
这有望让您开始寻找要寻找的东西,并将该系统的速度提高一个档次。 :)