2013-01-17 17 views
5

Zostałem poproszony o wygenerowanie raportu, który jest sterowany przez dość złożone zapytanie SQL przeciwko bazie danych SQL Server. Ponieważ strona raportu została już używając Entity Framework 4.1, pomyślałem, że spróbuję napisać kwerendę za pomocą EF i LINQ:Czy istnieje sposób na optymalizację tego zapytania LINQ do Entities?

var q = from r in ctx.Responses 
        .Where(x => ctx.Responses.Where(u => u.UserId == x.UserId).Count() >= VALID_RESPONSES) 
        .GroupBy(x => new { x.User.AwardCity, x.Category.Label, x.ResponseText }) 
     orderby r.FirstOrDefault().User.AwardCity, r.FirstOrDefault().Category.Label, r.Count() descending 
     select new 
     { 
      City = r.FirstOrDefault().User.AwardCity, 
      Category = r.FirstOrDefault().Category.Label, 
      Response = r.FirstOrDefault().ResponseText, 
      Votes = r.Count() 
     }; 

To zapytanie pokrywa się głosy, ale tylko od użytkowników, którzy dostarczyli pewna liczba wymagane minimum głosów.

To podejście było całkowitą katastrofą z punktu widzenia wydajności, więc przełączyliśmy się na ADO.NET i zapytanie zostało wykonane bardzo szybko. Spojrzałem na wygenerowany przez LINQ SQL przy użyciu programu SQL Profiler i chociaż wyglądał on okropnie jak zwykle, nie widziałem żadnych wskazówek, jak zoptymalizować instrukcję LINQ, aby była bardziej wydajna.

Oto wersja prosta TSQL:

WITH ValidUsers(UserId) 
AS 
(
    SELECT UserId 
    FROM Responses 
    GROUP BY UserId 
    HAVING COUNT(*) >= 103 
) 
SELECT d.AwardCity 
    , c.Label 
    , r.ResponseText 
    , COUNT(*) AS Votes 
FROM ValidUsers u 
JOIN Responses r ON r.UserId = u.UserId 
JOIN Categories c ON r.CategoryId = c.CategoryId 
JOIN Demographics d ON r.UserId = d.Id 
GROUP BY d.AwardCity, c.Label, r.ResponseText 
ORDER BY d.AwardCity, s.SectionName, COUNT(*) DESC 

Co Zastanawiam się: czy ta kwerenda po prostu zbyt skomplikowane dla EF i LINQ aby skutecznie obsłużyć lub zostały pominięte trick?

+0

Zgaduję, że przyczyną są wszystkie błędy FirstOrDefaults. Czy próbowałeś dodać 'let response = r.First()' przed groupby? Lub zamiana Select i OrderBy? Podoba mi się ten http://stackoverflow.com/a/5013740/736079 – jessehouwing

+2

Czy istnieje właściwość nawigacji, taka jak 'User.Responses'? –

+0

@jessehouwing za pomocą odpowiedzi let pomaga znacznie, chociaż wersja LINQ jest nadal znacznie wolniejsza niż ADO.NET. Jeśli wpiszesz to jako odpowiedź, to przynajmniej ją przegłosuję. Mam problem ze strategią zamiany Jona Skeeta i kolejności, głównie nie mogę wymyślić, jak uzyskać liczenie z tym konstruktem. –

Odpowiedz

4

Korzystanie let w celu zmniejszenia liczba r.First() prawdopodobnie poprawi wydajność. To chyba jeszcze za mało.

var q = from r in ctx.Responses 
       .Where() 
       .GroupBy() 
    let response = r.First() 
    orderby response.User.AwardCity, response.Category.Label, r.Count() descending 
    select new 
    { 
     City = response.User.AwardCity, 
     Category = response.Category.Label, 
     Response = response.ResponseText, 
     Votes = r.Count() 
    }; 
+0

Zaznaczam to jako odpowiedź, ponieważ jego efekt jest w przybliżeniu równoznaczny z rozwiązaniem właściwości nawigacji zaproponowanym przez @GertArnold, ale Gert nie opublikował swojego komentarza jako odpowiedzi (przepraszam, Gert, zabiorę się za ciebie). Należy pamiętać, że nawet przy obu optymalizacji ADO.NET jest jeszcze szybszy, ale zmieniony LINQ jest o rząd wielkości szybszy niż był. –

+1

Możesz także głosować w górę komentarzy. – jessehouwing

1

Może ta zmiana poprawić wydajność, usuwając powstały zagnieżdżony SQL SELECT w klauzuli WHERE

najpierw uzyskać głosy każdego użytkownika i umieścić je w Dictionary

var userVotes = ctx.Responses.GroupBy(x => x.UserId) 
          .ToDictionary(a => a.Key.UserId, b => b.Count()); 

var cityQuery = ctx.Responses.ToList().Where(x => userVotes[x.UserId] >= VALID_RESPONSES) 
       .GroupBy(x => new { x.User.AwardCity, x.Category.Label, x.ResponseText }) 
       .Select(r => new 
         { 
          City = r.First().User.AwardCity, 
          Category = r.First().Category.Label, 
          Response = r.First().ResponseText, 
          Votes = r.Count() 
         }) 
       .OrderByDescending(r => r.City, r.Category, r.Votes()); 
+0

Próbowałem już tego podejścia. Oto błąd: LINQ to Entities nie rozpoznaje metody "Int32 get_Item (Int32)" metoda, a ta metoda nie może być przetłumaczona na wyrażenie store –

+0

Myślę, że przegapiłem to, aby dodać ToList() po ctx.Response, w wyniku czego ctx.Responses.ToList(). Gdzie (... –

+0

Myślę, że podstawowy problem polega na tym, że sterownik EF LINQ nie może używać odwołania do zewnętrznego słownika podczas budowania encji SQL. i wkleiłem twoje rozwiązanie dosłownie, wszystko, co zrobiłem, to naprawienie składni pierwszej lambdy ToDictionary –

Powiązane problemy