2015-09-23 11 views
10

Ich habe wirklich schwer Zeit, eine meiner Entity Framework generierten Abfragen in meiner Anwendung zu tunen. Es ist sehr einfache Abfrage, aber aus irgendeinem Grund verwendet EF mehrere innere Unterabfragen, die schrecklich in DB anstelle von Joins durchzuführen scheinen.LINQ und Entity Framework - Unterabfragen zu vermeiden

Hier ist meine LINQ-Code:

Projects.Select(proj => new ProjectViewModel() 
       { 
        Name = proj.Name, 
        Id = proj.Id, 
        Total = proj.Subvalue.Where(subv => 
         subv.Created >= startDate 
         && subv.Created <= endDate 
         && 
         (subv.StatusId == 1 || 
         subv.StatusId == 2)) 
         .Select(c => c.SubValueSum) 
         .DefaultIfEmpty() 
         .Sum() 
       }) 
       .OrderByDescending(c => c.Total) 
       .Take(10); 

EF erzeugt wirklich komplexe Abfrage mit mehreren Unterabfragen, die schreckliche Abfrageleistung wie diese hat:

SELECT TOP (10) 
[Project3].[Id] AS [Id], 
[Project3].[Name] AS [Name], 
[Project3].[C1] AS [C1] 
FROM (SELECT 
    [Project2].[Id] AS [Id], 
    [Project2].[Name] AS [Name], 
    [Project2].[C1] AS [C1] 
    FROM (SELECT 
     [Extent1].[Id] AS [Id], 
     [Extent1].[Name] AS [Name], 
     (SELECT 
      SUM([Join1].[A1]) AS [A1] 
      FROM (SELECT 
       CASE WHEN ([Project1].[C1] IS NULL) THEN cast(0 as decimal(18)) ELSE [Project1].[SubValueSum] END AS [A1] 
       FROM (SELECT 1 AS X) AS [SingleRowTable1] 
       LEFT OUTER JOIN (SELECT 
        [Extent2].[SubValueSum] AS [SubValueSum], 
        cast(1 as tinyint) AS [C1] 
        FROM [dbo].[Subvalue] AS [Extent2] 
        WHERE ([Extent1].[Id] = [Extent2].[Id]) AND ([Extent2].[Created] >= '2015-08-01') AND ([Extent2].[Created] <= '2015-10-01') AND ([Extent2].[StatusId] IN (1,2))) AS [Project1] ON 1 = 1 
      ) AS [Join1]) AS [C1] 
     FROM [dbo].[Project] AS [Extent1] 
     WHERE ([Extent1].[ProjectCountryId] = 77) AND ([Extent1].[Active] = 1) 
    ) AS [Project2] 
) AS [Project3] 
ORDER BY [Project3].[C1] DESC; 

Die Ausführungszeit der Abfrage von EF erzeugt wird ~10 seconds . Aber wenn ich die Abfrage von Hand wie folgt schreibe:

select 
    TOP (10) 
    Proj.Id, 
    Proj.Name, 
    SUM(Subv.SubValueSum) AS Total 
from 
    SubValue as Subv 
left join 
    Project as Proj on Proj.Id = Subv.ProjectId 
where 
    Subv.Created > '2015-08-01' AND Subv.Created <= '2015-10-01' AND Subv.StatusId IN (1,2) 
group by 
    Proj.Id, 
    Proj.Name 
order by 
    Total DESC 

Die Ausführungszeit ist fast sofort; unter 30ms.

Das Problem liegt klar in meiner Fähigkeit, gute EF Abfragen mit LINQ zu schreiben, aber egal was ich versuche zu tun (Linqpad zum Testen verwenden) Ich kann einfach nicht ähnlich performant Abfrage mit LINQ\EF schreiben, wie ich mit der Hand schreiben kann. Ich habe die Tabelle SubValue Tabelle und Projekt Tabelle, aber das Ergebnis ist meist das gleiche: mehrere ineffiziente verschachtelte Unterabfragen anstelle von einem einzigen Join die Arbeit.

Wie kann ich eine Abfrage schreiben, die die oben gezeigte Hand SQL imitiert? Wie kann ich die tatsächliche Abfrage steuern, die von EF generiert wird? Und am wichtigsten: Wie kann ich Linq2SQL und Entity FrameworkJoins verwenden, wenn ich anstelle von verschachtelten Unterabfragen will.

+0

Mit ** ** LINQ Sie via Modem eine 'DataContext' verwenden. Sehen Sie sich die Eigenschaft 'DataContext.LoadOptions' [hier] an (https: // msdn.microsoft.com/en-us/library/system.data.linq.datacontext.loadoptions%28v=vs.110%29.aspx) .. –

+0

set context.ObjectTrackingEnabled = false, Sie werden eine große Verbesserung erhalten. –

+0

@JanUnld danke für den Vorschlag, aber ich bin mir nicht sicher, wie es bei diesem spezifischen Problem helfen würde? Kannst du es näher erklären? – veturi

Antwort

5

EF generiert SQL aus dem von Ihnen bereitgestellten LINQ-Ausdruck, und Sie können nicht erwarten, dass diese Konvertierung die Struktur dessen, was Sie in den Ausdruck eingeben, vollständig auflöst, um ihn zu optimieren. In Ihrem Fall haben Sie einen Ausdrucksbaum erstellt, der für jedes Projekt eine Navigationseigenschaft verwendet, um einige mit dem Projekt verknüpfte Unterwerte zu summieren. Dies führt zu verschachtelten Unterabfragen, wie Sie festgestellt haben.

Um das generierte SQL zu verbessern, müssen Sie das Navigieren von Projekt zu Subvalue vermeiden, bevor Sie alle Operationen auf subvalue ausführen. Dazu können Sie einen Join erstellen (was Sie auch in handgestalteter SQL machen):

var query = from proj in context.Project 
      join s in context.SubValue.Where(s => s.Created >= startDate && s.Created <= endDate && (s.StatusId == 1 || s.StatusId == 2)) on proj.Id equals s.ProjectId into s2 
      from subv in s2.DefaultIfEmpty() 
      select new { proj, subv } into x 
      group x by new { x.proj.Id, x.proj.Name } into g 
      select new { 
       g.Key.Id, 
       g.Key.Name, 
       Total = g.Select(y => y.subv.SubValueSum).Sum() 
      } into y 
      orderby y.Total descending 
      select y; 
var result = query.Take(10); 

Die Grundidee besteht darin, Projekte an Unterwerten zu verknüpfen, die durch eine where-Klausel eingeschränkt sind. Um einen linken Join auszuführen, benötigen Sie die DefaultIfEmpty(), aber das wissen Sie bereits.

Die verbundenen Werte (x) werden dann gruppiert und die Summierung von SubValueSum wird in jeder Gruppe durchgeführt.

Schließlich wird die Bestellung und TOP(10) angewendet.

Die generierte SQL enthält noch Subqueries aber ich würde erwarten, dass es effizienter im Vergleich zu SQL, indem Sie Ihre Abfrage generiert:

SELECT TOP (10) 
    [Project1].[Id] AS [Id], 
    [Project1].[Name] AS [Name], 
    [Project1].[C1] AS [C1] 
    FROM (SELECT 
     [GroupBy1].[A1] AS [C1], 
     [GroupBy1].[K1] AS [Id], 
     [GroupBy1].[K2] AS [Name] 
     FROM (SELECT 
      [Extent1].[Id] AS [K1], 
      [Extent1].[Name] AS [K2], 
      SUM([Extent2].[SubValueSum]) AS [A1] 
      FROM [dbo].[Project] AS [Extent1] 
      LEFT OUTER JOIN [dbo].[SubValue] AS [Extent2] ON ([Extent2].[Created] >= @p__linq__0) AND ([Extent2].[Created] <= @p__linq__1) AND ([Extent2].[StatusId] IN (1,2)) AND ([Extent1].[Id] = [Extent2].[ProjectId]) 
      GROUP BY [Extent1].[Id], [Extent1].[Name] 
     ) AS [GroupBy1] 
    ) AS [Project1] 
    ORDER BY [Project1].[C1] DESC 
+0

Nun, das war eine wirklich nette Antwort und erklärte einiges darüber, wie EF das SQL tatsächlich generiert. Vielen Dank! Die resultierende SQL läuft in der Tat viel schneller :) – veturi

+0

Als eine Anmerkung erzeugt LINQ to SQL viel effizienter SQL als EF. –

Verwandte Themen