C# LinqToSql查询返回时间太长性能太长

C# LinqToSql查询返回时间太长性能太长,c#,sql-server,linq,linq-to-sql,C#,Sql Server,Linq,Linq To Sql,我正在执行一个相当强大的LinqToSql语句,它返回一个新对象。由于SQL方法的数量(主要是求和和和转换),SQL的运行时间非常长,因此加载网页需要很长时间(10-15秒)。而我可以使用AJAX或类似的CSS加载器。首先,我想知道是否有一种简单的方法来实现我试图从SQL数据库中获取的内容 我正在努力: 返回给定字段不为空的所有用户 获取opportunities表中状态为“open”且外键匹配的所有当前项。(手动连接后) 在这些机会中,将几个字段的所有货币值的总和存储到我的类中 获取这些货币价

我正在执行一个相当强大的LinqToSql语句,它返回一个新对象。由于SQL方法的数量(主要是求和和和转换),SQL的运行时间非常长,因此加载网页需要很长时间(10-15秒)。而我可以使用AJAX或类似的CSS加载器。首先,我想知道是否有一种简单的方法来实现我试图从SQL数据库中获取的内容

我正在努力:

  • 返回给定字段不为空的所有用户
  • 获取opportunities表中状态为“open”且外键匹配的所有当前项。(手动连接后)
  • 在这些机会中,将几个字段的所有货币值的总和存储到我的类中
  • 获取这些货币价值的计数
  • Linq语句本身是一个相当长的编写过程,但是当转换成SQL时,它充满了COALESCE和其他强大的SQL方法

    以下是我的LINQ声明:

     decimal _default = (decimal)0.0000;
                var users = from bio in ctx.tbl_Bios.Where(bio => bio.SLXUID != null)
                          join opp in ctx.slx_Opportunities.Where(opp => opp.STATUS == "open") on bio.SLXUID equals opp.ACCOUNTMANAGERID  into opps
                          select new UserStats{
                              Name = bio.FirstName + " " + bio.SurName,
                              EnquiryMoney = opps.Where(opp => opp.SALESCYCLE == "Enquiry").Sum(opp => (opp.ACTUALAMOUNT.HasValue && opp.ACTUALAMOUNT.Value != _default ? opp.ACTUALAMOUNT : opp.SALESPOTENTIAL.HasValue ? (decimal)opp.SALESPOTENTIAL.Value : _default)).GetValueOrDefault(_default),
                              EnquiryNum = opps.Where(opp =>  opp.SALESCYCLE == "Enquiry").Count(),
                              GoingAheadMoney = opps.Where(opp => opp.SALESCYCLE == "Going Ahead").Sum(opp => (opp.ACTUALAMOUNT.HasValue && opp.ACTUALAMOUNT.Value != _default ? opp.ACTUALAMOUNT : opp.SALESPOTENTIAL.HasValue ? (decimal)opp.SALESPOTENTIAL.Value : _default)).GetValueOrDefault(_default),
                              GoingAheadNum = opps.Where(opp =>  opp.SALESCYCLE == "Going Ahead").Count(),
                              GoodPotentialMoney = opps.Where(opp => opp.SALESCYCLE == "Good Potential").Sum(opp => (opp.ACTUALAMOUNT.HasValue && opp.ACTUALAMOUNT.Value != _default ? opp.ACTUALAMOUNT : opp.SALESPOTENTIAL.HasValue ? (decimal)opp.SALESPOTENTIAL.Value : _default)).GetValueOrDefault(_default),
                              GoodPotentialNum = opps.Where(opp =>  opp.SALESCYCLE == "Good Potential").Count(),
                              LeadMoney = opps.Where(opp => opp.SALESCYCLE == "Lead").Sum(opp => (opp.ACTUALAMOUNT.HasValue && opp.ACTUALAMOUNT.Value != _default ? opp.ACTUALAMOUNT : opp.SALESPOTENTIAL.HasValue ? (decimal)opp.SALESPOTENTIAL.Value : _default)).GetValueOrDefault(_default),
                              LeadNum = opps.Where(opp =>  opp.SALESCYCLE == "Lead").Count(),
                              PriceOnlyMoney = opps.Where(opp => opp.SALESCYCLE == "Price Only").Sum(opp => (opp.ACTUALAMOUNT.HasValue && opp.ACTUALAMOUNT.Value != _default ? opp.ACTUALAMOUNT : opp.SALESPOTENTIAL.HasValue ? (decimal)opp.SALESPOTENTIAL.Value : _default)).GetValueOrDefault(_default),
                              PriceOnlyNum = opps.Where(opp =>  opp.SALESCYCLE == "Price Only").Count(),
                              ProvisionalMoney = opps.Where(opp => opp.SALESCYCLE == "Provisional").Sum(opp => (opp.ACTUALAMOUNT.HasValue && opp.ACTUALAMOUNT.Value != _default ? opp.ACTUALAMOUNT : opp.SALESPOTENTIAL.HasValue ? (decimal)opp.SALESPOTENTIAL.Value : _default)).GetValueOrDefault(_default),
                              ProvisionalNum = opps.Where(opp =>  opp.SALESCYCLE == "Provisional").Count()
                          };
    

    您可以做很多事情:

  • 筛选索引:根据Opportunities表中围绕值“open”的记录细分,您可以在“open”上创建筛选索引。如果“打开”和“关闭”(或其他任何值)的数量大致相等,那么过滤索引将使TSQL只查看具有“打开”的记录。过滤索引只存储满足谓词的数据;在本例中,您要加入的任何值为“open”的对象。这样,它就不必扫描其他索引中可能有“打开”记录的索引

  • 摘要/汇总表:创建一个包含您要查找的值的汇总表;在本例中,您正在查找总和和计数——为什么不创建一个只包含一行这些计数的表呢?您可以使用存储过程/代理作业使其保持最新。如果查询允许,还可以尝试创建索引视图;我将在下面讨论这个问题。汇总表;实际上,您需要运行一个存储过程来计算这些字段并定期更新它们(例如,根据负载情况,每隔几分钟或每分钟更新一次),然后将这些结果写入一个新表;这将是您的汇总表。然后,您的结果就像select语句一样简单。这将是非常快的,以每几分钟计算这些总和的负载为代价。根据记录的数量,这可能会有问题

  • 索引视图:可能是解决此类问题的“正确”方法,以及我们讨论的行数(在我的案例中,我是针对数十万行的案例进行研究的)

  • 过滤索引 您还可以为这些状态中的每一个创建一个过滤索引(这有点滥用;但它会起作用),然后简单地在求和/计数时,它只需要依赖与它要查找的状态匹配的索引

    要创建筛选索引,请执行以下操作:

    CREATE NONCLUSTERED INDEX FI_OpenStatus_Opportunities
        ON dbo.Opportunities (AccountManagerId, Status, ActualAmount)
        WHERE status = 'OPEN';
    GO
    
    同样,对于您的总和和计数(每列一个):

    (其余部分依此类推)

    我不是说这是你最好的主意;但这是一个想法。它是否好取决于它在您的工作负载环境中的表现(我无法回答)

    索引视图 您还可以创建包含此汇总信息的索引视图;这是一个稍微先进一点,取决于你

    为此:

      CREATE VIEW [SalesCycle_Summary] WITH SCHEMABINDING AS
        SELECT AccountManagerID, Status, SUM(ActualAmount) AS MONETARY
        ,COUNT_BIG(Status) as Counts 
    FROM [DBO].Opportunities
    GROUP BY AccountManagerID, Status
    GO
    
    
    -- Create clustered index on the view; making it an indexed view
    CREATE UNIQUE CLUSTERED INDEX IDX_SalesCycle_Summary ON [SalesCycle_Summary] (AccountManagerId);
    
    然后(取决于您的设置),您可以直接加入到该索引视图,或者通过提示包含它(尝试使用前者)

    最后,如果这些都不起作用(关于索引视图有一些问题——我已经6个月没有使用它们了,所以我不太记得具体的问题了),您可以创建一个CTE并完全抛弃Linq To SQL

    这个答案有点超出范围(因为我已经给出了两种方法,它们需要您进行大量调查)

    要了解这些方法的作用,请执行以下操作:

  • 从Linq To SQL语句()中获取生成的SQL

  • 打开SSMS并在查询窗口中启用以下选项:

    • 将统计信息IO设置为ON
    • 设置统计时间
    • 选中“显示实际查询计划”和“显示预计查询计划”的框
    • 将生成的SQL复制到其中;运行它
  • 在继续之前,请修复索引的所有问题。如果您得到丢失的索引警告;调查并解决这些问题,然后重新运行基准测试

  • 这些起始数字是你的基准

    • Statistics IO告诉您查询进行的逻辑和物理读取的数量(越低越好——首先关注读取数量高的区域)
    • 统计时间告诉您运行查询和向SSMS显示查询结果所用的时间(请确保启用
      SET NOCOUNT
      ,以免影响结果)
    • 实际的查询计划会准确地告诉您它在使用什么,SQL Server认为您缺少什么索引,以及其他会影响结果的问题,如隐式转换或错误的统计信息,所以我不会在这里重复答案
    • 估计的查询计划会告诉您SQL Server认为将要发生的事情——这些事情并不总是与实际的查询计划相同——并且您希望确保在调查中考虑到差异

    这里没有“简单”的答案;答案取决于您的数据、数据使用情况以及您可以对基础架构进行的更改。一旦您在SSMS中运行它,您将看到有多少是Linq到SQL的开销,以及有多少是查询本身

    您可以做很多事情:

  • 筛选索引:根据Opportunities表中围绕“open”值的记录细分,您可以创建
      CREATE VIEW [SalesCycle_Summary] WITH SCHEMABINDING AS
        SELECT AccountManagerID, Status, SUM(ActualAmount) AS MONETARY
        ,COUNT_BIG(Status) as Counts 
    FROM [DBO].Opportunities
    GROUP BY AccountManagerID, Status
    GO
    
    
    -- Create clustered index on the view; making it an indexed view
    CREATE UNIQUE CLUSTERED INDEX IDX_SalesCycle_Summary ON [SalesCycle_Summary] (AccountManagerId);
    
     var allOpps = ctx.slx_Opportunities.Where(opp => opp.STATUS == "open").GroupBy(opp => opp.SALESCYCLE).ToList();
    
            var users = ctx.tbl_Bios.Where(bio => bio.SLXUID != null).ToList().Select(bio => new UserStats
            {
                LeadNum = allOpps.Single(group => group.Key == "Lead").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Count(),
                LeadMoney = allOpps.Single(group => group.Key == "Lead").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Sum(opp =>  opp.SALESPOTENTIAL.GetValueOrDefault(_default)),
                GoingAheadNum = allOpps.Single(group => group.Key == "Going Ahead").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Count(),
                GoingAheadMoney = allOpps.Single(group => group.Key == "Going Ahead").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Sum(opp => opp.SALESPOTENTIAL.GetValueOrDefault(_default)),
                EnquiryNum = allOpps.Single(group => group.Key == "Enquiry").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Count(),
                EnquiryMoney = allOpps.Single(group => group.Key == "Enquiry").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Sum(opp => opp.SALESPOTENTIAL.GetValueOrDefault(_default)),
                GoodPotentialNum = allOpps.Single(group => group.Key == "Good Potential").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Count(),
                GoodPotentialMoney = allOpps.Single(group => group.Key == "Good Potential").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Sum(opp => opp.SALESPOTENTIAL.GetValueOrDefault(_default)),
                PriceOnlyNum = allOpps.Single(group => group.Key == "Price Only").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Count(),
                PriceOnlyMoney = allOpps.Single(group => group.Key == "Price Only").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Sum(opp => opp.SALESPOTENTIAL.GetValueOrDefault(_default)),
                ProvisionalNum = allOpps.Single(group => group.Key == "Provisional Booking").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Count(),
                ProvisionalMoney = allOpps.Single(group => group.Key == "Provisional Booking").Where(opp => opp.ACCOUNTMANAGERID == bio.SLXUID).Sum(opp => opp.SALESPOTENTIAL.GetValueOrDefault(_default)),
                Name = bio.FirstName + " " + bio.SurName
            }).ToList();