在C#.NET中优化LINQ查询的方法

介绍


本文中,我们讨论了LINQ查询的一些优化技术
这是与LINQ查询相关的更多代码优化方法

众所周知,LINQ(语言集成查询)是一种用于查询数据源的简单便捷的语言。

LINQ到SQL是一个数据库管理系统数据接入技术。这是用于处理数据的强大工具,其中查询是通过声明性语言构造的,然后平台会将其转换为SQL查询,并发送到已经执行的数据库服务器。在我们的例子中,DBMS是指MS SQL Server

然而,LINQ查询不会转换为最佳写入SQL查询是有经验的DBA可以用优化的所有细微差别写SQL查询

  1. 最佳连接(JOIN)和结果过滤(WHERE
  2. 使用化合物和组条件时有许多细微差别
  3. 在更换许多变化IN条件EXISTSNOT IN,<>与EXISTS
  4. 通过临时表,CTE,表变量对结果进行中间缓存
  5. 使用带有说明和表提示WITH(...)的子句(OPTION
  6. 使用索引视图作为摆脱样本中多余数据读取的手段之一

编译LINQ查询 ,所产生的SQL查询的主要性能瓶颈是:

  1. 将整个数据选择机制整合到一个请求中
  2. 复制相同的代码块,最终导致多次读取数据
  3. 的多组分条件组(逻辑“与”和“或”) - ANDOR,在困难的条件相结合,导致一个事实,即优化器,其具有合适的非聚簇索引,通过必要的字段,最终开始由上述簇索引扫描(INDEX SCAN)按条件组
  4. 子查询的深层嵌套使解析SQL语句和解析来自开发人员和DBA的查询计划变得非常困难

优化方法


现在我们直接传递给优化方法。

1)附加索引


最好考虑在主采样表上使用过滤器,因为整个查询通常围绕一个或两个主表(应用程序-人员-操作)构建,并具有一组标准条件(IsClosed,Cancel,Enabled,Status)。对于确定的样本,创建相应的索引很重要。

从这些字段中进行选择时,此解决方案很有意义,从而可以将返回的集显着限制为查询。

例如,我们有500,000个应用程序。但是,只有2,000个活动条目。然后,正确选择的索引将使我们免于在大型表上使用INDEX SCAN,并使我们能够通过非聚集索引快速选择数据。

也可以通过提示来分析查询计划或收集系统视图的统计信息来检测索引不足MS SQL Server

  1. sys.dm_db_missing_index_groups
  2. sys.dm_db_missing_index_group_stats
  3. sys.dm_db_missing_index_details

除空间索引外,所有视图数据均包含有关缺失索引的信息。

但是,索引和缓存通常是处理编写不佳的LINQ查询SQL查询的影响的方法

正如严酷的生活实践向企业展示的那样,在某个日期之前实现业务功能通常很重要。因此,经常将大量查询置于后台进行缓存。

这在一定程度上是合理的,因为用户并不总是需要最新数据,并且会发生可接受级别的用户界面响应。

这种方法使您能够解决业务需求,但最终会降低信息系统的效率,只是延迟解决问题。

还应该记住,在添加新索引所必需的搜索过程中,MS SQL优化建议可能不正确,包括以下情况:

  1. 如果已经存在具有相似字段集的索引
  2. 如果表中的字段由于索引限制而无法索引(有关更多信息,请参见此处)。

2)将属性合并为一个新属性


有时,可以通过引入一个新字段来替换同一表中发生一组条件的某些字段。

对于状态字段而言尤其如此,状态字段按类型通常是按位或整数。

示例:

IsClosed = 0 AND Canceled = 0 AND Enabled = 0替换为Status = 1

在这里,您可以输入整数属性Status,该属性是通过在表格中填写这些状态来提供的。下一步是索引这个新属性。

这是性能问题的根本解决方案,因为我们需要的数据没有不必要的计算。

3)提交实现


不幸的是,LINQ查询不能直接使用临时表,CTE和表变量。

但是,还有另一种针对这种情况进行优化的方法-这是索引视图。

一组条件(来自上面的示例)IsClosed = 0 AND Cancelled = 0 AND Enabled = 0(或一组其他类似条件)成为在索引视图中使用它们的好选择,从而从大型集中缓存一小部分数据。

但是,在实现视图时有很多限制:

  1. 使用子查询,应使用JOIN替换EXISTS子句
  2. 不能使用UNIONUNION ALLEXCEPTIONINTERSECT子句
  3. 您不能使用表提示和OPTION子句
  4. 无法循环工作
  5. 不可能在一个视图中显示来自不同表的数据

重要的是要记住,使用索引视图的真正好处实际上只能通过对其进行索引来获得。

但是,在调用视图时,可能不会使用这些索引,并且要显式使用它们,必须指定WITH(NOEXPAND)

由于不可能LINQ查询中定义表提示,因此我们必须进行另一种表示-以下形式的“包装”:

CREATE VIEW _ AS SELECT * FROM MAT_VIEW WITH (NOEXPAND);

4)使用表格功能


通常在LINQ查询中,大型子查询块或使用具有复杂结构的表示形式的块会形成具有非常复杂且不是最佳执行结构的最终查询。

LINQ查询中使用表函数的主要优点

  1. 与视图一样,具有使用并指定为对象的能力,但是您可以传递一组输入参数:最后,
    从FROM FUNCTION(@ param1,@ param2 ...)
    ,可以实现灵活的数据采样
  2. 使用表函数时,没有上述索引视图这样的强限制:

    1. :
      LINQ .
      .
      ,
    2. , , :

      • ( )
      • UNION EXISTS

  3. OPTION , OPTION(MAXDOP N), . :

    • OPTION (RECOMPILE)
    • , OPTION (FORCE ORDER)

    OPTION .
  4. :
    ( ), .
    , , WHERE (a, b, c).

    a = 0 and b = 0.

    , c .

    a = 0 and b = 0 , .

    在此表功能可能是一个更好的选择。

    此外,表函数更可预测,执行时间更恒定。

例子


让我们考虑一个使用Questions数据库示例的示例实现。

有一个SELECT查询,它结合了多个表并使用一个视图(OperativeQuestions),该视图通过电子邮件验证“活动查询”([OperativeQuestions])的从属关系(通过EXISTS):

要求1
(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM [dbo].[Questions] AS [Extent1]
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id],
[Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId],
[Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]) AND ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[OperativeQuestions] AS [Extent5]
WHERE (([Extent5].[Email] = @p__linq__0) OR (([Extent5].[Email] IS NULL) 
AND (@p__linq__0 IS NULL))) AND ([Extent5].[Id] = [Extent1].[Id])
));


该视图具有相当复杂的结构:它具有子查询联接和使用DISTINCT排序的功能,在一般情况下,这是一种相当耗费资源的操作。

从OperativeQuestions中选择了大约一万条记录。

该查询的主要问题是,对于来自外部查询的记录,在[OperativeQuestions]视图上执行内部子查询,这应将输出样本(通过EXISTS限制为[Email] = @ p__linq__0的数百条记录。

似乎子查询应该一次通过[Email] = @ p__linq__0计算记录,然后应该通过id c Questions连接这几百条记录,查询将很快。

实际上,所有表都是串联的:检查两个ID问题是否符合OperativeQuestions中的ID,并过滤电子邮件。

实际上,该请求适用于所有成千上万个OperativeQuestions记录,并且您仅需要在Email上关注的数据。

OperativeQuestions查看文本:

请求编号2
 
CREATE VIEW [dbo].[OperativeQuestions]
AS
SELECT DISTINCT Q.Id, USR.email AS Email
FROM            [dbo].Questions AS Q INNER JOIN
                         [dbo].ProcessUserAccesses AS BPU ON BPU.ProcessId = CQ.Process_Id 
OUTER APPLY
                     (SELECT   1 AS HasNoObjects
                      WHERE   NOT EXISTS
                                    (SELECT   1
                                     FROM     [dbo].ObjectUserAccesses AS BOU
                                     WHERE   BOU.ProcessUserAccessId = BPU.[Id] AND BOU.[To] IS NULL)
) AS BO INNER JOIN
                         [dbo].Users AS USR ON USR.Id = BPU.UserId
WHERE        CQ.[Exp] = 0 AND CQ.AnswerId IS NULL AND BPU.[To] IS NULL 
AND (BO.HasNoObjects = 1 OR
              EXISTS (SELECT   1
                           FROM   [dbo].ObjectUserAccesses AS BOU INNER JOIN
                                      [dbo].ObjectQuestions AS QBO 
                                                  ON QBO.[Object_Id] =BOU.ObjectId
                               WHERE  BOU.ProcessUserAccessId = BPU.Id 
                               AND BOU.[To] IS NULL AND QBO.Question_Id = CQ.Id));


DbContext中的原始映射表示(EF Core 2)
public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}


原始LINQ查询
var businessObjectsData = await context
    .OperativeQuestions
    .Where(x => x.Email == Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();


在这种特殊情况下,可以考虑在不进行基础结构更改的情况下解决此问题,而不引入带有现成结果的单独表(“活动查询”),为此需要一种机制来填充其数据并保持最新。

尽管这是一个很好的解决方案,但是还有另一种选项可以优化此任务。

主要目标是通过OperativeQuestions视图中的[Email] = @ p__linq__0来缓存条目。

我们在数据库中输入表函数[dbo]。[OperativeQuestionsUserMail]。

发送电子邮件作为输入参数,我们返回值表:

3号要求

CREATE FUNCTION [dbo].[OperativeQuestionsUserMail]
(
    @Email  nvarchar(4000)
)
RETURNS
@tbl TABLE
(
    [Id]           uniqueidentifier,
    [Email]      nvarchar(4000)
)
AS
BEGIN
        INSERT INTO @tbl ([Id], [Email])
        SELECT Id, @Email
        FROM [OperativeQuestions]  AS [x] WHERE [x].[Email] = @Email;
     
    RETURN;
END


这将返回具有预定义数据结构的值表。

为了使对OperativeQuestionsUserMail的查询达到最佳,具有最佳查询计划,需要严格的结构,而不是RETURNS TABLE AS RETURN ...

在这种情况下,所需的请求1转换为请求4:

请求编号4
(@p__linq__0 nvarchar(4000))SELECT
1 AS [C1],
[Extent1].[Id] AS [Id],
[Join2].[Object_Id] AS [Object_Id],
[Join2].[ObjectType_Id] AS [ObjectType_Id],
[Join2].[Name] AS [Name],
[Join2].[ExternalId] AS [ExternalId]
FROM (
    SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] (@p__linq__0)
) AS [Extent0]
INNER JOIN [dbo].[Questions] AS [Extent1] ON([Extent0].Id=[Extent1].Id)
INNER JOIN (SELECT [Extent2].[Object_Id] AS [Object_Id], [Extent2].[Question_Id] AS [Question_Id], [Extent3].[ExternalId] AS [ExternalId], [Extent3].[ObjectType_Id] AS [ObjectType_Id], [Extent4].[Name] AS [Name]
FROM [dbo].[ObjectQuestions] AS [Extent2]
INNER JOIN [dbo].[Objects] AS [Extent3] ON [Extent2].[Object_Id] = [Extent3].[Id]
LEFT OUTER JOIN [dbo].[ObjectTypes] AS [Extent4] 
ON [Extent3].[ObjectType_Id] = [Extent4].[Id] ) AS [Join2] 
ON [Extent1].[Id] = [Join2].[Question_Id]
WHERE ([Extent1].[AnswerId] IS NULL) AND (0 = [Extent1].[Exp]);


在DbContext(EF Core 2)中映射视图和函数
public class QuestionsDbContext : DbContext
{
    //...
    public DbQuery<OperativeQuestion> OperativeQuestions { get; set; }
    //...
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<OperativeQuestion>().ToView("OperativeQuestions");
    }
}
 
public static class FromSqlQueries
{
    public static IQueryable<OperativeQuestion> GetByUserEmail(this DbQuery<OperativeQuestion> source, string Email)
        => source.FromSql($"SELECT Id, Email FROM [dbo].[OperativeQuestionsUserMail] ({Email})");
}


最终LINQ查询
var businessObjectsData = await context
    .OperativeQuestions
    .GetByUserEmail(Email)
    .Include(x => x.Question)
    .Select(x => x.Question)
    .SelectMany(x => x.ObjectQuestions,
                (x, bo) => new
                {
                    Id = x.Id,
                    ObjectId = bo.Object.Id,
                    ObjectTypeId = bo.Object.ObjectType.Id,
                    ObjectTypeName = bo.Object.ObjectType.Name,
                    ObjectExternalId = bo.Object.ExternalId
                })
    .ToListAsync();


执行时间的顺序从200-800 ms减少到2-20 ms等,即快了十倍。

如果我们取更多的平均值,那么将得到8毫秒而不是350毫秒。

从明显的优点中,我们还得到:

  1. 总体上减少了阅读负荷,
  2. 大大降低了阻止概率
  3. 将平均阻塞时间减少到可接受的值

结论


通过LINQMS SQL数据库的调用的优化和微调是可以解决的问题。 在这项工作中,注意和保持一致性非常重要。 在过程开始时:





  1. 有必要检查查询所使用的数据(值,所选数据类型)
  2. 正确索引此数据
  3. 检查表之间连接条件的正确性

在下一次迭代中,优化显示:

  1. 请求的依据,并确定请求的主过滤器
  2. 重复类似的查询块和相交条件
  3. 在用于SQL Server的SSMS或其他GUI中SQL查询本身已进行了优化(分配了中间数据存储,使用该存储构建了结果查询(可能有多个))
  4. 在最后阶段,以生成的SQL查询为基础,重新构建LINQ查询结构

结果,生成的LINQ查询的结构应与第3段中确定的最佳SQL查询的结构相同

致谢


非常感谢同事 工作机会alex_ozr来自Fortis的文章帮助。

All Articles