Zapytanie Entity Framework jest powolne, ale ten sam kod SQL w SqlQuery jest szybki

95

Widzę naprawdę dziwne perf związane z bardzo prostym zapytaniem używającym Entity Framework Code-First z .NET Framework w wersji 4. Zapytanie LINQ2Entities wygląda następująco:

 context.MyTables.Where(m => m.SomeStringProp == stringVar);

Wykonanie tego zajmuje ponad 3000 milisekund. Wygenerowany SQL wygląda bardzo prosto:

 SELECT [Extent1].[ID], [Extent1].[SomeStringProp], [Extent1].[SomeOtherProp],
 ...
 FROM [MyTable] as [Extent1]
 WHERE [Extent1].[SomeStringProp] = '1234567890'

To zapytanie jest uruchamiane niemal natychmiast po uruchomieniu za pośrednictwem Management Studio. Kiedy zmieniam kod C #, aby użyć funkcji SqlQuery, działa w 5-10 milisekund:

 context.MyTables.SqlQuery("SELECT [Extent1].[ID] ... WHERE [Extent1].[SomeStringProp] = @param", stringVar);

Tak więc, dokładnie ten sam kod SQL, wynikowe encje są śledzone w obu przypadkach, ale różnica między nimi jest szalona. Co daje?

Brian Sullivan
źródło
2
Spodziewam się, że widzisz opóźnienia w inicjalizacji - prawdopodobnie wyświetl kompilację. Zobacz MSDN:Performance Considerations for Entity Framework 5
Nicholas Butler
Próbowałem wstępnie generować widoki, ale to nie pomaga. Uruchom również inne zapytanie EF przed powolnym, aby wykluczyć inicjalizację. Nowe zapytanie działało szybko, wolne nadal działało wolno, mimo że rozgrzanie kontekstu nastąpiło podczas pierwszego zapytania.
Brian Sullivan
1
@marc_s - nie, SqlQuery zwróci w pełni zmaterializowane i śledzone zmiany wystąpienia jednostki. Zobacz msdn.microsoft.com/en-us/library/…
Brian Sullivan
Czy wygenerowany kod SQL dla zapytania EF faktycznie zawiera wartość parametru lub używa parametru? Nie powinno to wpływać na szybkość zapytań dla pojedynczego zapytania, ale może z czasem spowodować rozdęcie planu zapytania na serwerze.
Jim Wooley
Czy próbowałeś uruchomić to samo zapytanie dwa razy / wiele razy? Ile czasu minęło przy drugim uruchomieniu? Czy wypróbowałeś to w .NET Framework 4.5 - są pewne ulepszenia wydajności związane z EF w .NET Framework 4.5, które mogą pomóc.
Paweł

Odpowiedzi:

97

Znalazłem to. Okazuje się, że jest to kwestia typów danych SQL. SomeStringPropKolumna w bazie danych varchar, ale zakłada, że EF NET rodzaje strun są nvarchars. Wynikowy proces tłumaczenia podczas zapytania DB o wykonanie porównania zajmuje dużo czasu. Myślę, że EF Prof trochę mnie tu sprowadził na manowce, dokładniejsza reprezentacja wykonywanego zapytania wyglądałaby następująco:

 SELECT [Extent1].[ID], [Extent1].[SomeStringProp], [Extent1].[SomeOtherProp],
 ...
 FROM [MyTable] as [Extent1]
 WHERE [Extent1].[SomeStringProp] = N'1234567890'

Zatem wynikowa poprawka polega na dodaniu adnotacji do modelu w pierwszej kolejności, wskazując poprawny typ danych SQL:

public class MyTable
{
    ...

    [Column(TypeName="varchar")]
    public string SomeStringProp { get; set; }

    ...
}
Brian Sullivan
źródło
1
Niezłe śledztwo. Twoje zapytanie cierpiało z powodu „niejawnej konwersji”, co zostało wyjaśnione tutaj: brentozar.com/archive/2012/07/…
Jaime
Zaoszczędził mi kilka godzin debugowania. To był właśnie problem.
Cody,
1
W moim przypadku używam EDMX ze starszą bazą danych, która używa varchardo wszystkiego i rzeczywiście na tym polegał problem. Zastanawiam się, czy mogę zrobić EDMX, aby wziąć pod uwagę varchar dla wszystkich kolumn łańcuchowych.
Alisson
1
Świetny człowiek do znalezienia. ale @Jaime, co powinniśmy zrobić w pierwszej kolejności do bazy danych, ponieważ wszystko (np. adnotacja danych w modelach db) znika po aktualizacji modelu EF z bazy danych.
Nauman Khan
Ustawiam to na chwilę jako moją stronę domową, abym mógł na chwilę przeżyć ekscytację związaną z ponownym znajdowaniem tak świetnej odpowiedzi. Dziękuję Ci!!!
OJisBad
44

Powodem spowolnienia moich zapytań wykonanych w EF było porównanie skalarów nieprzekraczających wartości zerowej ze skalarami dopuszczającymi wartość null:

long? userId = 10; // nullable scalar

db.Table<Document>().Where(x => x.User.Id == userId).ToList() // or userId.Value
                                ^^^^^^^^^    ^^^^^^
                                Type: long   Type: long?

To zapytanie zajęło 35 sekund. Ale taka drobna refaktoryzacja:

long? userId = 10;
long userIdValue = userId.Value; // I've done that only for the presentation pursposes

db.Table<Document>().Where(x => x.User.Id == userIdValue).ToList()
                                ^^^^^^^^^    ^^^^^^^^^^^
                                Type: long   Type: long

daje niesamowite rezultaty. Ukończenie zajęło tylko 50 ms. Możliwe, że jest to błąd w EF.

cryss
źródło
13
To takie dziwne
Daniel Cardenas,
1
O MÓJ BOŻE. Może się to najwyraźniej zdarzyć również wtedy, gdy używanie interfejsów IUserId.Id powodowało u mnie problem, ale najpierw działa mapowanie identyfikatora na liczbę całkowitą ... czy muszę teraz sprawdzać wszystkie zapytania w mojej aplikacji 100 000 wierszy?
Dirk Boer
czy ten błąd został zgłoszony? Nadal jest w najnowszej wersji 6.2.0
Dirk Boer
2
Ten sam problem występuje również w EF Core. Dzięki za znalezienie tego!
Yannickv
Inną sugestią jest przetworzenie zmiennej przed umieszczeniem jej w wyrażeniu LINQ. W przeciwnym razie wygenerowany plik sql będzie znacznie dłuższy i wolniejszy. Doświadczyłem, mając Trim () i ToLower () wewnątrz wyrażenia LINQ, co mnie wkurza.
samheihey
4

Miałem ten sam problem (zapytanie jest szybkie, gdy jest wykonywane z menedżera SQL), ale po wykonaniu z EF limit czasu wygasa.

Okazuje się, że jednostka (która została utworzona z widoku) miała nieprawidłowe klucze encji. Tak więc jednostka miała zduplikowane wiersze z tymi samymi kluczami i myślę, że musiała grupować w tle.

Vladimir Gedgafov
źródło
3

Natknąłem się również na to ze złożonym zapytaniem ef. Jedną z poprawek, które zmniejszyło 6-sekundowe zapytanie ef do wygenerowanego przez nie drugiego zapytania sql, było wyłączenie leniwego ładowania.

Aby znaleźć to ustawienie (ef 6), przejdź do pliku .edmx i spójrz na Właściwości -> Generowanie kodu -> Lazy Loading Enabled. Ustawiono na fałsz.

Ogromna poprawa wydajności dla mnie.

user2622095
źródło
4
To fajne, ale nie ma to nic wspólnego z pytaniem o plakaty.
Jace Rhea
2

Ja też miałem ten problem. Okazuje się, że winowajcą w moim przypadku było sniffowanie parametrów SQL-Server .

Pierwszą wskazówką, że mój problem był w rzeczywistości spowodowany podsłuchiwaniem parametrów, było to, że uruchomienie zapytania z ustawieniem „set arithabort off” lub „set arithabort on” skutkowało drastycznie różnymi czasami wykonywania w Management Studio. Dzieje się tak, ponieważ ADO.NET domyślnie używa „set arithabort off”, a Management Studio domyślnie „set arithabort on”. Pamięć podręczna planu kwerend przechowuje różne plany w zależności od tego parametru.

Wyłączyłem buforowanie planu zapytania dla zapytania, korzystając z rozwiązania, które możesz znaleźć tutaj .

Oskar Sjöberg
źródło