2017-07-09 12 views
0

特定の条件が満たされている場合にのみサブクエリを実行するLINQ-to-Entities(SQL Server 2014に対して)を説得する方法はありますか?現行のコードでは、LINQによって発行されるSQLは、たとえ少数の行だけが条件を満たすとしても、すべての場合においてサブクエリを実行します。強制的にLINQ-to-Entities + SQL Server 2014で条件付きでサブクエリを実行する

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#でのサブクエリの簡易版です

私が欲しいのは、この疑似-SQLのように見えるSQLです:上記の例で

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はほとんどの店のための虚偽と真実のためにありますそれらのいくつか。実行中の合計をフェッチするサブクエリは高価です。

目的は、必要な行を除いて、そのサブクエリを実行しないようにすることです。

しかし、上記の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の損傷が行われた後、実行中の合計を必要としない行は除外されます。私が望んで

は次のように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を放出するクエリ、または常に条件がfalseの場合、サブクエリを実行しないようになる他のSQLを変更することができますか?

明らかに、私はこれを2つのLINQクエリに分割することができます.1つは実行中の合計と残りのものを必要とする行で、もう1つは結果を結合します。しかし、これは私が避けることを好むリファクタリングがたくさん含まれています。

代わりに条件演算子を削除して、テストをWhere()にプッシュしようとしました。しかし、この方法は他の場合に役立つかもしれませんが、ここでは役に立ちませんでした。なぜなら、SQL Serverはこのサブクエリが依存するカバリングインデックスに対して非効率的なインデックスシークを実行しているからです。 (これはON (SaleDate, StoreId) INCLUDE (RunningTotal)なので、特定のストアを探すのは非常に高価です.SQLは何年もの行を読み込む必要があります)。これはSQL Serverの問題です。predicate pushdownがインデックスシークを避けるほどスマートではありません。残念ながら、私はより良い基礎となる表のインデックスをindex--カバーするさまざまな理由で変更することは容易ではありません追加することはできません。

私は、let(別名OUTER APPLY)を左外部結合に変換できますが、それはかなり複雑なリファクタを必要とします。その結果、予想外のクエリプランになる可能性がありますパフォーマンスが悪化するケースもあります。

それでは、私が実際に好むことCASE(または機能的に同等の結果)の内部ではなく、ケースの外側にサブクエリを発するようにLINQを説得する方法です。助言がありますか?

答えて

1

はちょうどそれがショート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] 
+0

ダビデに変換し、これは正確に正しいです。奇妙なことは、私がこの正確なアプローチを以前に試みたことであり、うまくいかないということです。少なくとも、私はそれができないと思った。私はちょうどそれをもう一度試して、ブール値を逆にしたことに気付きました。もちろん、I/Oは保存されませんでした。 SQL Serverが短絡を無視していると思うようになりました。ブールテストをWhere()で反転させると、I/Oは短絡のために90%低下しました。バグに関しては、SQL Serverのバグではなくバグです。 ;-) –

+0

あなたは間違いを犯していると思っていたので、正しい解決策を取ってみるべきです - 本当にトラブルシューティングを行います。私はいつもそうしています。 –

関連する問題