Linq To Sql 查询 return 次太性能太长
LinqToSql Query return time too performance too long
我正在做一个相当大的 LinqToSql 语句,returns 一个新对象。由于 SQL 方法(主要是求和和转换)的数量,SQL 花费了相当长的时间到 运行,因此加载网页需要很长时间(10-15 秒) ).虽然我可以使用 AJAX 或类似的 CSS 加载程序。我首先想知道是否有一种简单的方法可以实现我试图从 SQL 数据库中获得的内容。
我正在尝试:
- Return 给定字段不为空的所有用户
- 获取机会 table 中状态为 'open' 且外键匹配的所有当前项。 (进行手动加入后)
- 在这些机会中,将几个字段的所有货币价值的总和存储到我的 class
- 获取这些货币价值的计数。
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()
};
您可以做很多事情:
过滤索引:根据机会 table 中围绕值 'open' 的记录细分,您可以创建一个'open' 上的筛选索引。如果 'open' 和 'closed'(或任何其他值)的数量大致相等,则过滤索引会让您的 TSQL 仅查看具有 'open' 的记录.过滤索引只存储满足谓词的数据;在这种情况下,您要加入的任何内容都具有 'open' 的值。这样它就不必扫描其他索引以查找可能包含 'open' 的记录。
Summary/Rollup table:创建一个包含您要查找的值的 Rollup table;在这种情况下,您正在寻找总和和计数——为什么不创建一个 table,它只有一行具有这些计数?您可以使用存储的 Procedure/Agent 作业来使其保持最新。如果您的查询允许,您也可以尝试创建一个索引视图;我将在下面讨论。对于摘要table;您本质上是 运行 一个存储过程,它计算这些字段并定期更新它们(比如每隔几分钟一次或一分钟一次,具体取决于负载)并将这些结果写入新的 table;这将是您的 Rollup table。那么您的结果就像 select 语句一样简单。这将非常快,但以每隔几分钟计算这些总和的负载为代价。根据记录的数量,这可能会有问题。
索引视图:可能是 '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。
这个答案有点超出范围(因为我已经给出了两种方法,它们需要您进行大量调查)。
调查这些是如何做到的:
从您的 Linq-To-SQL 语句 (here's how you do that) 中获取生成的 SQL。
打开 SSMS 并在查询中启用以下内容 window:
SET STATISTICS IO ON
SET STATISTICS TIME ON
- 选中 "display actual query plan" 和 "display estimated query plan"
框
- 将生成的SQL复制进去; 运行它。
在继续之前解决索引的任何问题。如果您收到缺少索引警告;调查并解决这些问题,然后重新运行 基准。
这些起始数字是您的基准。
- 统计 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();
我正在做一个相当大的 LinqToSql 语句,returns 一个新对象。由于 SQL 方法(主要是求和和转换)的数量,SQL 花费了相当长的时间到 运行,因此加载网页需要很长时间(10-15 秒) ).虽然我可以使用 AJAX 或类似的 CSS 加载程序。我首先想知道是否有一种简单的方法可以实现我试图从 SQL 数据库中获得的内容。
我正在尝试:
- Return 给定字段不为空的所有用户
- 获取机会 table 中状态为 'open' 且外键匹配的所有当前项。 (进行手动加入后)
- 在这些机会中,将几个字段的所有货币价值的总和存储到我的 class
- 获取这些货币价值的计数。
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()
};
您可以做很多事情:
过滤索引:根据机会 table 中围绕值 'open' 的记录细分,您可以创建一个'open' 上的筛选索引。如果 'open' 和 'closed'(或任何其他值)的数量大致相等,则过滤索引会让您的 TSQL 仅查看具有 'open' 的记录.过滤索引只存储满足谓词的数据;在这种情况下,您要加入的任何内容都具有 'open' 的值。这样它就不必扫描其他索引以查找可能包含 'open' 的记录。
Summary/Rollup table:创建一个包含您要查找的值的 Rollup table;在这种情况下,您正在寻找总和和计数——为什么不创建一个 table,它只有一行具有这些计数?您可以使用存储的 Procedure/Agent 作业来使其保持最新。如果您的查询允许,您也可以尝试创建一个索引视图;我将在下面讨论。对于摘要table;您本质上是 运行 一个存储过程,它计算这些字段并定期更新它们(比如每隔几分钟一次或一分钟一次,具体取决于负载)并将这些结果写入新的 table;这将是您的 Rollup table。那么您的结果就像 select 语句一样简单。这将非常快,但以每隔几分钟计算这些总和的负载为代价。根据记录的数量,这可能会有问题。
索引视图:可能是 '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。
这个答案有点超出范围(因为我已经给出了两种方法,它们需要您进行大量调查)。
调查这些是如何做到的:
从您的 Linq-To-SQL 语句 (here's how you do that) 中获取生成的 SQL。
打开 SSMS 并在查询中启用以下内容 window:
SET STATISTICS IO ON
SET STATISTICS TIME ON
- 选中 "display actual query plan" 和 "display estimated query plan" 框
- 将生成的SQL复制进去; 运行它。
在继续之前解决索引的任何问题。如果您收到缺少索引警告;调查并解决这些问题,然后重新运行 基准。
这些起始数字是您的基准。
- 统计 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();