LinqToSql查询返回时间太长,性能太长

时间:2015-02-16 14:21:17

标签: c# sql-server linq linq-to-sql

我正在做一个非常大的LinqToSql语句,它返回一个新对象。由于SQL方法的数量(主要是Sum和convert),SQL运行需要很长时间,因此加载网页需要很长时间(10-15秒)。虽然我可以使用AJAX或类似的CSS加载器。我首先想知道是否有一种简单的方法可以实现我想要从SQL数据库获得的东西。

我想:

  1. 返回给定字段不为空的所有用户
  2. 获取机会表中状态为“打开”且外键匹配的所有当前项目。 (在进行手动加入后)
  3. 在这些机会中,将多个字段的所有货币值的总和存储到我的班级
  4. 获取这些货币价值的数量。
  5. 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()
                          };
    

2 个答案:

答案 0 :(得分:4)

您可以做多件事:

  1. 已过滤的索引:根据“机会”表中围绕值“打开”的记录细分,您可以在“打开”上创建过滤索引。如果你有大约相等数量的'open'和'closed'(或其他任何值),那么过滤后的索引会让你的TSQL只查看具有'open'的记录。过滤的索引仅存储符合谓词的数据;在这种情况下,您加入的任何内容都具有“开放”值。这样,它就不必扫描其他索引中可能已经打开的记录。

  2. 摘要/汇总表:创建一个包含您正在寻找的值的汇总表;在这种情况下,您正在寻找Sums和count - 为什么不创建一个只有一行具有这些计数的表?您可以使用存储过程/代理作业来使其保持最新。如果您的查询允许,您还可以尝试创建索引视图;我会进入下面的那个。对于汇总表;你基本上运行一个存储过程来计算这些字段并定期更新它们(比如每隔几分钟或每分钟一次,具体取决于负载)并将这些结果写入新表;这将是你的Rollup表。然后您的结果就像选择语句一样简单。这将是非常快的,代价是每隔几分钟计算这些总和的负荷。根据记录的数量,这可能会有问题。

  3. 索引视图:可能是解决此类问题的“正确”方式,depending on your constraints,以及我们谈论的行数(在我的情况下;我;追求它的情况下,有成千上万的行)。

  4. 已过滤的索引

    你也可以为这些状态中的每一个创建一个过滤的索引(它有点滥用;但它会起作用),然后只是在它的求和/计数时,它只需要依赖与状态匹配的索引它正在寻找。

    创建过滤后的索引:

    CREATE NONCLUSTERED INDEX FI_OpenStatus_Opportunities
        ON dbo.Opportunities (AccountManagerId, Status, ActualAmount)
        WHERE status = 'OPEN';
    GO
    

    同样,您的金额和计数(每列一个):

    CREATE NONCLUSTERED INDEX FI_SalesCycleEnquiry_Status_Opportunities
        ON dbo.Opportunities (AccountManagerId, Status, SalesCycle, ActualAmount)
        WHERE status = 'OPEN' and SalesCycle = 'Enquiry'
    

    (等等)。

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

    索引视图

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

    要做到这一点:

      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。

    答案有点超出范围(因为我已经给出了两种方法,他们需要你进行大量的调查)。

    调查这些行为:

    1. 从Linq-To-SQL语句(here's how you do that)获取生成的SQL。

    2. 打开SSMS并在查询窗口中打开以下内容:

      • SET STATISTICS IO ON
      • SET STATISTICS TIME ON
      • 选中“显示实际查询计划”和“显示估计查询计划”
      • 的复选框
      • 将生成的SQL复制到其中;跑吧。
    3. 在继续之前修复索引的任何问题。如果您收到缺失指数警告;调查它们并解决它们,然后重新运行基准测试。

    4. 这些起始编号是您的基准。

      • 统计IO告诉您查询所进行的逻辑和物理读取次数(越低越好 - 专注于首先读取次数较多的区域)
      • 统计数据TIME会告诉您运行查询所需的时间并将结果显示给SSMS(确保转为SET NOCOUNT ON,这样您就不会影响结果)
      • 实际查询计划会准确地告诉您它正在使用什么,SQL Server认为您缺少哪些索引,以及其他问题,如隐式转换或可能会影响结果的错误统计信息。 Brent Ozar Unlimited has a great video on the subject,所以我不会在这里重现答案。
      • 估计的查询计划会告诉您SQL Server认为会发生什么 - 这些并不总是与实际查询计划相同 - 并且您希望确保考虑到调查中的差异。

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

答案 1 :(得分:1)

我在查询中先前将linq查询设置为本地,然后创建一个group by然后创建我的对象。由于返回的项目数量很少,我只能这样做,因此服务器可以轻松处理它们。其他任何遇到类似问题的人最好建议使用George Stocker的答案

我将查询更新为以下内容:

 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();