Linq To Sql 查询 return 次太性能太长

LinqToSql Query return time too performance too long

我正在做一个相当大的 LinqToSql 语句,returns 一个新对象。由于 SQL 方法(主要是求和和转换)的数量,SQL 花费了相当长的时间到 运行,因此加载网页需要很长时间(10-15 秒) ).虽然我可以使用 AJAX 或类似的 CSS 加载程序。我首先想知道是否有一种简单的方法可以实现我试图从 SQL 数据库中获得的内容。

我正在尝试:

  1. Return 给定字段不为空的所有用户
  2. 获取机会 table 中状态为 'open' 且外键匹配的所有当前项。 (进行手动加入后)
  3. 在这些机会中,将几个字段的所有货币价值的总和存储到我的 class
  4. 获取这些货币价值的计数。

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

您可以做很多事情:

  1. 过滤索引:根据机会 table 中围绕值 'open' 的记录细分,您可以创建一个'open' 上的筛选索引。如果 'open' 和 'closed'(或任何其他值)的数量大致相等,则过滤索引会让您的 TSQL 仅查看具有 'open' 的记录.过滤索引只存储满足谓词的数据;在这种情况下,您要加入的任何内容都具有 'open' 的值。这样它就不必扫描其他索引以查找可能包含 'open' 的记录。

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

  3. 索引视图:可能是 'right' 解决这样问题的方法,depending on your constraints,以及我们有多少行'重新谈论(在我的情况下;我在有数十万行的情况下进行了处理)。

过滤索引

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

创建过滤索引:

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

然后(取决于您的设置)您可以直接加入该索引视图,或通过提示包含它(尝试前者)。

最后,如果 none 有效(索引视图有一些陷阱——我已经有大约 6 个月没有使用它们了,所以我不太记得那个困扰我的具体问题),您始终可以创建一个 CTE 并完全放弃 Linq-To-SQL。

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

调查这些是如何做到的:

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

  2. 打开 SSMS 并在查询中启用以下内容 window:

    • SET STATISTICS IO ON
    • SET STATISTICS TIME ON
    • 选中 "display actual query plan" 和 "display estimated query plan"
    • 将生成的SQL复制进去; 运行它。
  3. 在继续之前解决索引的任何问题。如果您收到缺少索引警告;调查并解决这些问题,然后重新运行 基准。

这些起始数字是您的基准。

  • 统计 IO 告诉您查询进行的逻辑和物理读取次数(越低越好——首先关注读取次数多的区域)
  • Statistics TIME 告诉您查询花费了多少时间 运行 并将其结果显示到 SSMS(确保打开 SET NOCOUNT ON 这样您就不会影响结果)
  • 实际查询计划准确地告诉您它正在使用什么,SQL 服务器认为您丢失了哪些索引,以及会影响您的结果的其他问题,例如隐式转换或不良统计信息。 Brent Ozar Unlimited has a great video on the subject,所以我不会在这里重现答案。
  • 估计的查询计划告诉您 SQL 服务器认为会发生什么——这些并不总是与实际查询计划相同——并且您要确保考虑到你的调查。

这里没有'easy'个答案;答案取决于您的数据、您的数据使用情况,以及您可以对基础架构进行的更改。在 SSMS 中 运行 之后,您将看到其中有多少是 Linq-To-SQL 开销,以及查询本身有多少。

我在我的查询中早些时候将我的 linq 查询设为本地,进行分组,然后创建我的对象。我之所以能够这样做,是因为返回的物品数量很少,因此服务器可以轻松处理它们。最好建议其他遇到类似问题的人使用 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();