Programowanie

Optymalizacja zapytań LINQ w Entity Framework Core

Ostatnio natrafiłem na problem dotyczący zapytania LINQ wykonywanego na bazie za pomocą EF Core. Teoretycznie z poziomu LINQ wszystko wyglądało OK. A jednak zapytanie wykonywało się na tyle długo, że dostawało time out. Oryginalnie zapytanie wyglądało następująco:


public Dictionary<string, DateTime>; Execute(ICollection<string> orderIds)
{

return context.Transactions
.AsNoTracking()
.Where(t => orderIds.Contains(t.OrderId))
.GroupBy(t => t.OrderId)
.ToDictionary(x => x.Key, x => x.Max(t => t.Date));
}

Generowany SQL wyglądał jak mały potworek. Zawierał wszystkie pola z tabeli Transactions, a jest ich aż 44!

SELECT [t].[TransactionId], [t].[OrderId], [t].[Date], 
[t].[Field3], [t].[Field4], [t].[Field5], [t].[Field6], [t].[Field7], [t].[Field8], 
[t].[Field9], [t].[Field10], [t].[Field11], [t].[Field12], [t].[Field13], [t].[Field14], [t].[Field15], [t].[Field16], [t].[Field17], [t].[Field18], [t].[Field19], 
[t].[Field20], [t].[Field21], [t].[Field22], [t].[Field23], [t].[Field24], [t].[Field25], [t].[Field26], [t].[Field27], [t].[Field28], [t].[Field29], [t].[Field30], [t].[Field31], [t].[Field32], [t].[Field33], [t].[Field34], [t].[Field35], [t].[Field36], [t].[Field37], [t].[Field38], [t].[Field39], [t].[Field40], [t].[Field41], [t].[Field42], [t].[Field43], [t].[Field44], [t].[Field45] 

FROM [biz].[Transaction] AS [t] WHERE [t].[OrderId] IN ( /* lista orderIds */ ) ORDER BY [t].[ExternalOrderId]

Rozwiązanie

Po co nam wszystkie pola, skoro do stworzenia dictionary i tak używamy tylko dwóch z nich. Pierwszą rzeczą było więc dodanie select’a tylko na dwa pola:


public Dictionary<string, DateTime?> Execute(ICollection<string> orderIds)
{
    return context.Transactions
                          .AsNoTracking()
                          .Select(x => new { x.OrderId, x.Date})
                          .Where(t => orderIds.Contains(t.OrderId))
                          .GroupBy(t => t.OrderId)
                          .ToDictionary(x => x.Key, x => x.Max(t => t.Date));
}

Po dokładniejszym przyjrzeniu możemy się domyśleć, że oryginalnym zamiarem było zrobienie GROUP BY. Jednak w finalnym zapytaniu go nie widzimy. Jest to pewna… hmm, ułomność EF Core. GroupBy wywoływany jest już na odebranych danych. Zróbmy więc poprawnego GROUP BY w SQLu za pomocą LINQ. To będzie wymagało trochę gimnastyki:

 public Dictionary Execute(ICollection orderIds)
 {
 return context.Transactions
               .AsNoTracking()
               .Where(t => orderIds.Contains(t.OrderId))
               .GroupBy(t => new { t.OrderId, t.Date } )
               .Select(x => new { x.Key.OrderId, x.Key.Date })
               .ToList()
               .GroupBy(t => t.OrderId )
               .ToDictionary(x => x.Key, x => x.Max(t => t.Date));
 }
 

Na początku konstruuję zapytanie LINQ które przekształcane jest w porządane przez nas zapytanie SQL:

SELECT [t].[OrderId], [t].[Date] FROM [biz].[Transaction] AS [t] 
WHERE [t].[OrderId] IN ( /* lista orderIds */ ) 
GROUP BY [t].[ExternalOrderId], [t].[CustodianTreatmentDate]

Lepiej niż na początku, prawda? Następnie Za pomocą .ToList() wykonuję faktyczne zapytanie na bazie. Następnie działam już z otrzymanymi danymi w pamięci. Grupuję po OrderId (może być kilka dat dla jednego OrderId) a następnie przepisuję wynik do Dictionary, gdzie klucz to OrderId a wartość to max z Date (czyli najnowsza data). Jaki jest zysk. Już pokazuję. Po lewej zapytanie które było na początku (już z SELECT’em tylko dwóch potrzebnych pól). Po prawej zaś wynik końcowego zapytania:

Idnew queryold query
110133642019-06-0610133642019-06-06
210133652019-06-0610133652019-06-06
310168682019-06-2410168682019-06-24
410253402019-08-1310168682019-06-24
510265422019-08-2110253402019-08-13
610265542019-08-2110253402019-08-13
710265422019-08-21
810265542019-08-21
910265542019-08-21
1010265542019-08-21
1110265542019-08-21
1210265542019-08-21

Podsumowanie

Jak widać różnica jest kolosalna. A zakładając, że na taką samą datę i OrderId możemy mieć nawet i 100 wpisów, a pobieramy 1000 Transakcji, musimy przesłać aż 1000*100=100 000 rekordów. Dzięki temu, że shackowaliśmy EF Core, generuje już poprawne zapytanie i dzięki temu dostaniemy tylko 1000 wierszy bez duplikatów.

Zostaw odpowiedź

Twój adres email nie zostanie upubliczniony.* Pola wymagane *

This site uses Akismet to reduce spam. Learn how your comment data is processed.