强制 LINQ-to-Entities + SQL Server 2014 有条件地 运行 子查询
forcing LINQ-to-Entities + SQL Server 2014 to conditionally run a subquery
有没有办法说服 LINQ-to-Entities(针对 SQL Server 2014)仅在特定条件为真时才 运行 子查询?在我们当前的代码中,LINQ 发出的 SQL 在所有情况下都会 运行 子查询,即使只有少数行满足条件。
我想要的是 SQL 看起来像这样的伪 SQL:
SELECT CASE WHEN condition THEN (subquery SQL) ELSE NULL END
但是 LINQ 生成的是这个伪 SQL:
CASE WHEN condition THEN sub.results ELSE NULL END
.... (more SQL here)
OUTER APPLY (
subquery SQL
) sub
这是 C# 子查询的简化版本:
let lastSale = storeReportSettings.ShowRunningTotal
? salesTable
.Where(arg =>
arg.StoreId == Stores.StoreId &&
arg.SaleDate <= endDate)
.OrderByDescending(arg => arg.SaleDate)
.Select(arg => arg.RunningTotal)
.FirstOrDefault()
: (int?)null
在上面的示例中,reportSettings.ShowRunningTotal
对于大多数商店都是错误的,而对于其中的一些商店则是正确的。获取 运行ning 总数的子查询很昂贵。
所以目标是避免运行除了需要它的行之外使用该子查询。
但是上面的 LINQ 生成的 SQL 看起来像这样:
SELECT
... (lots of SQL here) ...
CASE WHEN ([Filter1].[ShowRunningTotal] = 1) THEN [Limit1].[RunningTotal] END AS [C1],
... (lots more SQL here) ...
OUTER APPLY (SELECT TOP (1) [Project1].[RunningTotal] AS [RunningTotal]
FROM ( SELECT
[Extent13].[RunningTotal] AS [RunningTotal],
[Extent13].[SaleDate] AS [SaleDate]
FROM [dbo].[Sales] AS [Extent13]
WHERE ([Extent13].[StoreID] = [Filter1].[StoreId1])
AND ([Extent13].[SaleDate] <= @p__linq__1)
) AS [Project1]
ORDER BY [Project1].[SaleDate] DESC ) AS [Limit1]
不是阻止子查询运行对条件为假的行进行查询,而是SQL运行对每一行进行子查询。然后在 I/O 损坏完成后,它会过滤掉不需要 运行ning 总数的行。
我想要的是SQL这样的:
OUTER APPLY (
SELECT CASE WHEN ([Filter1].[ShowRunningTotal] = 0) THEN NULL ELSE
(
SELECT TOP (1) [Project1].[RunningTotal] AS [RunningTotal]
FROM ( SELECT
[Extent13].[RunningTotal] AS [RunningTotal],
[Extent13].[SaleDate] AS [SaleDate]
FROM [dbo].[Sales] AS [Extent13]
WHERE ([Extent13].[StoreID] = [Filter1].[StoreId1])
AND ([Extent13].[SaleDate] <= @p__linq__1)
) AS [Project1]
ORDER BY [Project1].[SaleDate] DESC
) END AS [RunningTotal]
) AS [Limit1]
如何更改我的 LINQ 查询以像上面那样发出 SQL,或任何其他 SQL 如果条件为假,将始终避免 运行 子查询?
显然,我可以将其拆分为两个 LINQ 查询——一个用于需要 运行ning 总计的行,另一个用于其余——然后合并结果。但这涉及很多我宁愿避免的重构。
我尝试删除条件运算符并将测试推入 Where()
。但是,尽管这种方法在其他情况下可能会有帮助,但在这里没有帮助,因为 SQL 服务器仍然 运行 在该子查询所依赖的覆盖索引上进行低效索引查找。 (ON (SaleDate, StoreId) INCLUDE (RunningTotal)
这使得寻找特定商店非常昂贵,因为 SQL 必须读取多年的行。)这似乎是 SQL 服务器问题,其中 predicate pushdown 不是'足够聪明,可以避免索引查找。不幸的是,我无法添加更好的覆盖索引——底层 table 的索引由于各种原因不容易修改。
我可以将子查询从 let(又名 OUTER APPLY)转换为左外连接,但这将涉及相当复杂的重构,我担心这可能会以较少的谓词结束table 查询计划在某些情况下可能会产生更差的性能。
所以我真正喜欢的是说服 LINQ 在 CASE 内部(或功能等效的结果)而不是在 CASE 外部发出子查询的方法。有什么建议吗?
只需在子查询中嵌入条件,使其在 ShowRunningTotal = false 的情况下短路。例如
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;
namespace ConsoleApp6
{
[Table("Customers")]
public class Customer
{
public int CustomerID { get; set; }
public string Name { get; set; }
public bool ShowRunningTotal { get; set; }
public ICollection<SalesOrders> Orders { get; } = new HashSet<SalesOrders>();
}
public class SalesOrders
{
public int Id { get; set; }
public float Amount { get; set; }
public DateTime SaleDate { get; set; }
public int CustomerId { get; set; }
virtual public Customer Customer { get; set; }
}
class Db : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<SalesOrders> SalesOrders { get; set; }
}
class Program
{
static void Main()
{
Database.SetInitializer(new DropCreateDatabaseAlways<Db>());
using (var db = new Db())
{
var q = from c in db.Customers
select new
{
c.CustomerID,
LastSale = c.Orders
.Where(o => c.ShowRunningTotal)
.OrderByDescending(o => o.SaleDate)
.FirstOrDefault()
};
Console.WriteLine(q.ToString());
}
Console.ReadKey();
}
}
}
转换为
SELECT
[Extent1].[CustomerID] AS [CustomerID],
[Limit1].[Id] AS [Id],
[Limit1].[Amount] AS [Amount],
[Limit1].[SaleDate] AS [SaleDate],
[Limit1].[CustomerId] AS [CustomerID1]
FROM [dbo].[Customers] AS [Extent1]
OUTER APPLY (SELECT TOP (1) [Project1].[Id] AS [Id], [Project1].[Amount] AS [Amount], [Project1].[SaleDate] AS [SaleDate], [Project1].[CustomerId] AS [CustomerId]
FROM ( SELECT
[Extent2].[Id] AS [Id],
[Extent2].[Amount] AS [Amount],
[Extent2].[SaleDate] AS [SaleDate],
[Extent2].[CustomerId] AS [CustomerId]
FROM [dbo].[SalesOrders] AS [Extent2]
WHERE ([Extent1].[CustomerID] = [Extent2].[CustomerId]) AND ([Extent1].[ShowRunningTotal] = 1)
) AS [Project1]
ORDER BY [Project1].[SaleDate] DESC ) AS [Limit1]
有没有办法说服 LINQ-to-Entities(针对 SQL Server 2014)仅在特定条件为真时才 运行 子查询?在我们当前的代码中,LINQ 发出的 SQL 在所有情况下都会 运行 子查询,即使只有少数行满足条件。
我想要的是 SQL 看起来像这样的伪 SQL:
SELECT CASE WHEN condition THEN (subquery SQL) ELSE NULL END
但是 LINQ 生成的是这个伪 SQL:
CASE WHEN condition THEN sub.results ELSE NULL END
.... (more SQL here)
OUTER APPLY (
subquery SQL
) sub
这是 C# 子查询的简化版本:
let lastSale = storeReportSettings.ShowRunningTotal
? salesTable
.Where(arg =>
arg.StoreId == Stores.StoreId &&
arg.SaleDate <= endDate)
.OrderByDescending(arg => arg.SaleDate)
.Select(arg => arg.RunningTotal)
.FirstOrDefault()
: (int?)null
在上面的示例中,reportSettings.ShowRunningTotal
对于大多数商店都是错误的,而对于其中的一些商店则是正确的。获取 运行ning 总数的子查询很昂贵。
所以目标是避免运行除了需要它的行之外使用该子查询。
但是上面的 LINQ 生成的 SQL 看起来像这样:
SELECT
... (lots of SQL here) ...
CASE WHEN ([Filter1].[ShowRunningTotal] = 1) THEN [Limit1].[RunningTotal] END AS [C1],
... (lots more SQL here) ...
OUTER APPLY (SELECT TOP (1) [Project1].[RunningTotal] AS [RunningTotal]
FROM ( SELECT
[Extent13].[RunningTotal] AS [RunningTotal],
[Extent13].[SaleDate] AS [SaleDate]
FROM [dbo].[Sales] AS [Extent13]
WHERE ([Extent13].[StoreID] = [Filter1].[StoreId1])
AND ([Extent13].[SaleDate] <= @p__linq__1)
) AS [Project1]
ORDER BY [Project1].[SaleDate] DESC ) AS [Limit1]
不是阻止子查询运行对条件为假的行进行查询,而是SQL运行对每一行进行子查询。然后在 I/O 损坏完成后,它会过滤掉不需要 运行ning 总数的行。
我想要的是SQL这样的:
OUTER APPLY (
SELECT CASE WHEN ([Filter1].[ShowRunningTotal] = 0) THEN NULL ELSE
(
SELECT TOP (1) [Project1].[RunningTotal] AS [RunningTotal]
FROM ( SELECT
[Extent13].[RunningTotal] AS [RunningTotal],
[Extent13].[SaleDate] AS [SaleDate]
FROM [dbo].[Sales] AS [Extent13]
WHERE ([Extent13].[StoreID] = [Filter1].[StoreId1])
AND ([Extent13].[SaleDate] <= @p__linq__1)
) AS [Project1]
ORDER BY [Project1].[SaleDate] DESC
) END AS [RunningTotal]
) AS [Limit1]
如何更改我的 LINQ 查询以像上面那样发出 SQL,或任何其他 SQL 如果条件为假,将始终避免 运行 子查询?
显然,我可以将其拆分为两个 LINQ 查询——一个用于需要 运行ning 总计的行,另一个用于其余——然后合并结果。但这涉及很多我宁愿避免的重构。
我尝试删除条件运算符并将测试推入 Where()
。但是,尽管这种方法在其他情况下可能会有帮助,但在这里没有帮助,因为 SQL 服务器仍然 运行 在该子查询所依赖的覆盖索引上进行低效索引查找。 (ON (SaleDate, StoreId) INCLUDE (RunningTotal)
这使得寻找特定商店非常昂贵,因为 SQL 必须读取多年的行。)这似乎是 SQL 服务器问题,其中 predicate pushdown 不是'足够聪明,可以避免索引查找。不幸的是,我无法添加更好的覆盖索引——底层 table 的索引由于各种原因不容易修改。
我可以将子查询从 let(又名 OUTER APPLY)转换为左外连接,但这将涉及相当复杂的重构,我担心这可能会以较少的谓词结束table 查询计划在某些情况下可能会产生更差的性能。
所以我真正喜欢的是说服 LINQ 在 CASE 内部(或功能等效的结果)而不是在 CASE 外部发出子查询的方法。有什么建议吗?
只需在子查询中嵌入条件,使其在 ShowRunningTotal = false 的情况下短路。例如
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;
namespace ConsoleApp6
{
[Table("Customers")]
public class Customer
{
public int CustomerID { get; set; }
public string Name { get; set; }
public bool ShowRunningTotal { get; set; }
public ICollection<SalesOrders> Orders { get; } = new HashSet<SalesOrders>();
}
public class SalesOrders
{
public int Id { get; set; }
public float Amount { get; set; }
public DateTime SaleDate { get; set; }
public int CustomerId { get; set; }
virtual public Customer Customer { get; set; }
}
class Db : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<SalesOrders> SalesOrders { get; set; }
}
class Program
{
static void Main()
{
Database.SetInitializer(new DropCreateDatabaseAlways<Db>());
using (var db = new Db())
{
var q = from c in db.Customers
select new
{
c.CustomerID,
LastSale = c.Orders
.Where(o => c.ShowRunningTotal)
.OrderByDescending(o => o.SaleDate)
.FirstOrDefault()
};
Console.WriteLine(q.ToString());
}
Console.ReadKey();
}
}
}
转换为
SELECT
[Extent1].[CustomerID] AS [CustomerID],
[Limit1].[Id] AS [Id],
[Limit1].[Amount] AS [Amount],
[Limit1].[SaleDate] AS [SaleDate],
[Limit1].[CustomerId] AS [CustomerID1]
FROM [dbo].[Customers] AS [Extent1]
OUTER APPLY (SELECT TOP (1) [Project1].[Id] AS [Id], [Project1].[Amount] AS [Amount], [Project1].[SaleDate] AS [SaleDate], [Project1].[CustomerId] AS [CustomerId]
FROM ( SELECT
[Extent2].[Id] AS [Id],
[Extent2].[Amount] AS [Amount],
[Extent2].[SaleDate] AS [SaleDate],
[Extent2].[CustomerId] AS [CustomerId]
FROM [dbo].[SalesOrders] AS [Extent2]
WHERE ([Extent1].[CustomerID] = [Extent2].[CustomerId]) AND ([Extent1].[ShowRunningTotal] = 1)
) AS [Project1]
ORDER BY [Project1].[SaleDate] DESC ) AS [Limit1]