AKTUALIZACJA 3: Zgodnie z tym ogłoszeniem , zespół EF zajął się tym problemem w EF6 alfa 2.
UPDATE 2: Stworzyłem sugestię rozwiązania tego problemu. Aby zagłosować na to, przejdź tutaj .
Rozważmy bazę danych SQL z jedną bardzo prostą tabelą.
CREATE TABLE Main (Id INT PRIMARY KEY)
Zapełniam tabelę 10 000 rekordów.
WITH Numbers AS
(
SELECT 1 AS Id
UNION ALL
SELECT Id + 1 AS Id FROM Numbers WHERE Id <= 10000
)
INSERT Main (Id)
SELECT Id FROM Numbers
OPTION (MAXRECURSION 0)
Buduję model EF dla tabeli i uruchamiam następującą kwerendę w LINQPad (używam trybu „C # Instrukcje”, więc LINQPad nie tworzy zrzutu automatycznie).
var rows =
Main
.ToArray();
Czas wykonania ~ 0,07 sekundy. Teraz dodaję operator Contains i ponownie uruchamiam zapytanie.
var ids = Main.Select(a => a.Id).ToArray();
var rows =
Main
.Where (a => ids.Contains(a.Id))
.ToArray();
Czas wykonania w tym przypadku to 20,14 sekundy (288 razy wolniej)!
Na początku podejrzewałem, że wykonanie T-SQL wyemitowanego dla zapytania trwa dłużej, więc próbowałem wyciąć go i wkleić z okienka SQL LINQPad do SQL Server Management Studio.
SET NOCOUNT ON
SET STATISTICS TIME ON
SELECT
[Extent1].[Id] AS [Id]
FROM [dbo].[Primary] AS [Extent1]
WHERE [Extent1].[Id] IN (1,2,3,4,5,6,7,8,...
Wynik był taki
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 88 ms.
Następnie podejrzewałem, że LINQPad powoduje problem, ale wydajność jest taka sama, niezależnie od tego, czy uruchamiam go w LINQPad, czy w aplikacji konsoli.
Wygląda więc na to, że problem tkwi gdzieś w Entity Framework.
Czy ja tu robię coś złego? To krytyczna czasowo część mojego kodu, więc czy jest coś, co mogę zrobić, aby przyspieszyć działanie?
Używam Entity Framework 4.1 i Sql Server 2008 R2.
AKTUALIZACJA 1:
W poniższej dyskusji pojawiło się kilka pytań dotyczących tego, czy opóźnienie wystąpiło podczas budowania przez EF zapytania początkowego, czy podczas analizowania otrzymanych danych. Aby to przetestować, uruchomiłem następujący kod,
var ids = Main.Select(a => a.Id).ToArray();
var rows =
(ObjectQuery<MainRow>)
Main
.Where (a => ids.Contains(a.Id));
var sql = rows.ToTraceString();
co zmusza EF do generowania zapytania bez wykonywania go w bazie danych. W rezultacie ten kod wymagał ~ 20 sekund do uruchomienia, więc wydaje się, że prawie cały czas zajmuje tworzenie początkowego zapytania.
CompiledQuery to ratunek? Nie tak szybko ... CompiledQuery wymaga, aby parametry przekazywane do zapytania były typami podstawowymi (int, string, float itd.). Nie akceptuje tablic ani IEnumerable, więc nie mogę go używać jako listy identyfikatorów.
źródło
var qry = Main.Where (a => ids.Contains(a.Id)); var rows = qry.ToArray();
sprawdzić, która część zapytania zajmuje trochę czasu?parent._recompileRequired = () => true;
dzieje się to dla wszystkich zapytań zawierających parametr IEnumerable <T>. Gwizd!Odpowiedzi:
AKTUALIZACJA: Po dodaniu InExpression w EF6 wydajność przetwarzania Enumerable.Contains znacznie się poprawiła. Podejście opisane w tej odpowiedzi nie jest już konieczne.
Masz rację, że większość czasu zajmuje przetwarzanie zapytania. Model dostawcy EF nie zawiera obecnie wyrażenia, które reprezentuje klauzulę IN, dlatego dostawcy ADO.NET nie mogą natywnie obsługiwać IN. Zamiast tego implementacja Enumerable.Contains tłumaczy to na drzewo wyrażeń OR, czyli na coś, co w C # wygląda tak:
new []{1, 2, 3, 4}.Contains(i)
... wygenerujemy drzewo DbExpression, które można przedstawić w następujący sposób:
((1 = @i) OR (2 = @i)) OR ((3 = @i) OR (4 = @i))
(Drzewa wyrażeń muszą być zbalansowane, ponieważ gdybyśmy mieli wszystkie OR na jednym długim grzbiecie, byłoby większe prawdopodobieństwo, że odwiedzający wyrażenie uderzy w przepełnienie stosu (tak, faktycznie trafiliśmy to w naszych testach))
Później wysyłamy takie drzewo do dostawcy ADO.NET, który może mieć możliwość rozpoznania tego wzorca i zredukowania go do klauzuli IN podczas generowania kodu SQL.
Kiedy dodaliśmy obsługę Enumerable.Cains in EF4, pomyśleliśmy, że warto to zrobić bez konieczności wprowadzania obsługi wyrażeń IN w modelu dostawcy i szczerze mówiąc, 10000 to znacznie więcej niż liczba elementów, do których przewidywaliśmy, że klienci przejdą Enumerable.Contains. To powiedziawszy, rozumiem, że jest to irytujące i że manipulowanie drzewami wyrażeń sprawia, że rzeczy są zbyt drogie w twoim konkretnym scenariuszu.
Rozmawiałem o tym z jednym z naszych programistów i wierzymy, że w przyszłości moglibyśmy zmienić implementację, dodając pierwszorzędne wsparcie dla IN. Upewnię się, że zostanie to dodane do naszych zaległości, ale nie mogę obiecać, kiedy to się uda, biorąc pod uwagę wiele innych ulepszeń, które chcielibyśmy wprowadzić.
Do obejść już zasugerowanych w wątku dodałbym:
Rozważ utworzenie metody, która zrównoważy liczbę obiegów bazy danych w obie strony z liczbą elementów przekazanych do Contains. Na przykład w moich własnych testach zaobserwowałem, że przetwarzanie i wykonywanie na lokalnym wystąpieniu SQL Servera kwerendy zawierającej 100 elementów zajmuje 1/60 sekundy. Jeśli możesz napisać zapytanie w taki sposób, że wykonanie 100 zapytań ze 100 różnymi zestawami identyfikatorów dałoby wynik równoważny zapytaniu z 10 000 elementów, to wyniki można uzyskać w ciągu około 1,67 sekundy zamiast 18 sekund.
Różne rozmiary fragmentów powinny działać lepiej w zależności od zapytania i opóźnienia połączenia z bazą danych. W przypadku niektórych zapytań, tj. Jeśli przekazana sekwencja ma duplikaty lub jeśli Enumerable.Contains jest używane w stanie zagnieżdżonym, można uzyskać zduplikowane elementy w wynikach.
Oto fragment kodu (przepraszam, jeśli kod użyty do podzielenia danych wejściowych na fragmenty wygląda na zbyt skomplikowany. Są prostsze sposoby osiągnięcia tego samego, ale próbowałem wymyślić wzorzec, który zachowuje przesyłanie strumieniowe dla sekwencji i Nie mogłem znaleźć czegoś podobnego w LINQ, więc prawdopodobnie przesadziłem tę część :)):
Stosowanie:
var list = context.GetMainItems(ids).ToList();
Metoda dla kontekstu lub repozytorium:
public partial class ContainsTestEntities { public IEnumerable<Main> GetMainItems(IEnumerable<int> ids, int chunkSize = 100) { foreach (var chunk in ids.Chunk(chunkSize)) { var q = this.MainItems.Where(a => chunk.Contains(a.Id)); foreach (var item in q) { yield return item; } } } }
Metody rozszerzające do wycinania wyliczalnych sekwencji:
public static class EnumerableSlicing { private class Status { public bool EndOfSequence; } private static IEnumerable<T> TakeOnEnumerator<T>(IEnumerator<T> enumerator, int count, Status status) { while (--count > 0 && (enumerator.MoveNext() || !(status.EndOfSequence = true))) { yield return enumerator.Current; } } public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> items, int chunkSize) { if (chunkSize < 1) { throw new ArgumentException("Chunks should not be smaller than 1 element"); } var status = new Status { EndOfSequence = false }; using (var enumerator = items.GetEnumerator()) { while (!status.EndOfSequence) { yield return TakeOnEnumerator(enumerator, chunkSize, status); } } } }
Mam nadzieję że to pomoże!
źródło
!(status.EndOfSequence = true)
w metodzie TakeOnEnumerator <T>: Tak więc efektem ubocznym tego przypisania wyrażenia będzie zawsze! True, nie wpływając tym samym na ogólne wyrażenie. Zasadniczo oznacza tostats.EndOfSequence
jakotrue
tylko wtedy, gdy pozostały elementy do pobrania, ale dotarłeś do końca wyliczania.Enumerable.Contains
znacznie się poprawiła w EF 6 w porównaniu z poprzednimi wersjami EF. Ale, niestety, w naszych przypadkach użycia nadal jest daleki od zadowalającego / gotowego do produkcji.Jeśli znajdziesz problem z wydajnością, który blokuje cię, nie próbuj spędzać czasu na jego rozwiązywaniu, ponieważ najprawdopodobniej nie odniesiesz sukcesu i będziesz musiał skontaktować się bezpośrednio z SM (jeśli masz wsparcie premium) i to wieczność.
Użyj obejścia i obejścia w przypadku problemu z wydajnością, a EF oznacza bezpośredni SQL. Nie ma w tym nic złego. Globalny pomysł, że używanie EF = nieużywanie już SQL jest kłamstwem. Masz SQL Server 2008 R2, więc:
Include
logikę w optymalny sposóbSqlDataReader
do uzyskiwania wyników i konstruowania jednostekJeśli wydajność jest dla Ciebie krytyczna, nie znajdziesz lepszego rozwiązania. Tej procedury nie można zamapować i wykonać przez EF, ponieważ bieżąca wersja nie obsługuje parametrów wycenianych w tabeli ani wielu zestawów wyników.
źródło
Udało nam się rozwiązać problem EF Contains, dodając tabelę pośrednią i dołączając do tej tabeli z zapytania LINQ, które wymagało użycia klauzuli Contains. Dzięki takiemu podejściu udało nam się osiągnąć niesamowite rezultaty. Mamy duży model EF i ponieważ „Zawiera” nie jest dozwolone podczas wstępnej kompilacji zapytań EF, uzyskiwaliśmy bardzo niską wydajność dla zapytań korzystających z klauzuli „Contains”.
Przegląd:
Tworzenie tabeli w SQL Server - na przykład
HelperForContainsOfIntType
zeHelperID
zGuid
danych typu orazReferenceID
zint
kolumnami danych typu. W razie potrzeby utwórz różne tabele z identyfikatorami referencyjnymi różnych typów danych.Utwórz Entity / EntitySet dla
HelperForContainsOfIntType
i inne takie tabele w modelu EF. W razie potrzeby utwórz różne Entity / EntitySet dla różnych typów danych.Utwórz metodę pomocniczą w kodzie .NET, która pobiera dane wejściowe
IEnumerable<int>
i zwraca plikGuid
. Ta metoda generuje nowyGuid
i wstawia wartości zIEnumerable<int>
doHelperForContainsOfIntType
wraz z wygenerowanym plikiemGuid
. Następnie metoda zwraca to nowo wygenerowaneGuid
do obiektu wywołującego. Aby szybko wstawić doHelperForContainsOfIntType
tabeli, utwórz procedurę składowaną, która pobiera listę wartości i wykonuje wstawianie. Zobacz Parametry wartościowane w tabeli w programie SQL Server 2008 (ADO.NET) . Utwórz różnych pomocników dla różnych typów danych lub utwórz ogólną metodę pomocnika do obsługi różnych typów danych.Utwórz skompilowaną kwerendę EF, która jest podobna do poniższej:
static Func<MyEntities, Guid, IEnumerable<Customer>> _selectCustomers = CompiledQuery.Compile( (MyEntities db, Guid containsHelperID) => from cust in db.Customers join x in db.HelperForContainsOfIntType on cust.CustomerID equals x.ReferenceID where x.HelperID == containsHelperID select cust );
Wywołaj metodę pomocniczą z wartościami, które mają być użyte w
Contains
klauzuli, i pobierzGuid
do użycia w zapytaniu. Na przykład:var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 }); var result = _selectCustomers(_dbContext, containsHelperID).ToList();
źródło
Edytowanie mojej pierwotnej odpowiedzi - istnieje możliwe obejście, w zależności od złożoności Twoich encji. Jeśli znasz plik sql generowany przez EF w celu wypełnienia jednostek, możesz wykonać go bezpośrednio przy użyciu DbContext.Database.SqlQuery . Myślę, że w EF 4 można użyć ObjectContext.ExecuteStoreQuery , ale nie próbowałem tego.
Na przykład, używając kodu z mojej oryginalnej odpowiedzi poniżej, aby wygenerować instrukcję sql za pomocą a
StringBuilder
, mogłem wykonać następujące czynnościvar rows = db.Database.SqlQuery<Main>(sql).ToArray();
a całkowity czas zmienił się z około 26 sekund do 0,5 sekundy.
Będę pierwszym, który powie, że jest brzydki i mam nadzieję, że pojawi się lepsze rozwiązanie.
aktualizacja
Po dłuższym namyśle zdałem sobie sprawę, że jeśli używasz sprzężenia do filtrowania wyników, EF nie musi tworzyć tak długiej listy identyfikatorów. Może to być skomplikowane w zależności od liczby jednoczesnych zapytań, ale uważam, że do ich wyodrębnienia można użyć identyfikatorów użytkowników lub identyfikatorów sesji.
Aby to przetestować, utworzyłem
Target
tabelę z tym samym schematem coMain
. Następnie użyłem a,StringBuilder
aby utworzyćINSERT
polecenia do zapełnieniaTarget
tabeli partiami po 1000, ponieważ jest to najwięcej, jakie SQL Server zaakceptuje w jednymINSERT
. Bezpośrednie wykonanie instrukcji sql było znacznie szybsze niż przechodzenie przez EF (około 0,3 sekundy w porównaniu z 2,5 sekundy) i uważam, że byłoby w porządku, ponieważ schemat tabeli nie powinien się zmieniać.Wreszcie, wybranie przy użyciu a
join
spowodowało znacznie prostsze zapytanie i zostało wykonane w mniej niż 0,5 sekundy.ExecuteStoreCommand("DELETE Target"); var ids = Main.Select(a => a.Id).ToArray(); var sb = new StringBuilder(); for (int i = 0; i < 10; i++) { sb.Append("INSERT INTO Target(Id) VALUES ("); for (int j = 1; j <= 1000; j++) { if (j > 1) { sb.Append(",("); } sb.Append(i * 1000 + j); sb.Append(")"); } ExecuteStoreCommand(sb.ToString()); sb.Clear(); } var rows = (from m in Main join t in Target on m.Id equals t.Id select m).ToArray(); rows.Length.Dump();
I sql wygenerowane przez EF dla złączenia:
SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] INNER JOIN [dbo].[Target] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
(oryginalna odpowiedź)
To nie jest odpowiedź, ale chciałem podzielić się dodatkowymi informacjami, a komentarz jest o wiele za długi. Udało mi się odtworzyć Twoje wyniki i muszę dodać kilka innych rzeczy:
SQL Profiler pokazuje, że opóźnienie występuje między wykonaniem pierwszego zapytania (
Main.Select
) a drugimMain.Where
zapytaniem, więc podejrzewałem, że problem dotyczy wygenerowania i wysłania zapytania o takim rozmiarze (48 980 bajtów).Jednak dynamiczne zbudowanie tej samej instrukcji sql w T-SQL zajmuje mniej niż 1 sekundę i pobranie jej
ids
zMain.Select
instrukcji, zbudowanie tej samej instrukcji sql i wykonanie jej przy użyciuSqlCommand
0,112 sekundy, a to łącznie z czasem na zapisanie zawartości w konsoli .W tym momencie podejrzewam, że EF wykonuje pewną analizę / przetwarzanie dla każdego z 10 000
ids
podczas budowania zapytania. Chciałbym móc podać ostateczną odpowiedź i rozwiązanie :(.Oto kod, który wypróbowałem w SSMS i LINQPad (proszę nie krytykować zbyt ostro, spieszy mi się, próbując wyjść z pracy):
declare @sql nvarchar(max) set @sql = 'SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] WHERE [Extent1].[Id] IN (' declare @count int = 0 while @count < 10000 begin if @count > 0 set @sql = @sql + ',' set @count = @count + 1 set @sql = @sql + cast(@count as nvarchar) end set @sql = @sql + ')' exec(@sql)
var ids = Mains.Select(a => a.Id).ToArray(); var sb = new StringBuilder(); sb.Append("SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] WHERE [Extent1].[Id] IN ("); for(int i = 0; i < ids.Length; i++) { if (i > 0) sb.Append(","); sb.Append(ids[i].ToString()); } sb.Append(")"); using (SqlConnection connection = new SqlConnection("server = localhost;database = Test;integrated security = true")) using (SqlCommand command = connection.CreateCommand()) { command.CommandText = sb.ToString(); connection.Open(); using(SqlDataReader reader = command.ExecuteReader()) { while(reader.Read()) { Console.WriteLine(reader.GetInt32(0)); } } }
źródło
Nie jestem zaznajomiony z Entity Framework, ale czy perf jest lepsze, jeśli wykonasz następujące czynności?
Zamiast tego:
var ids = Main.Select(a => a.Id).ToArray(); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
co powiesz na to (zakładając, że identyfikator jest liczbą int):
var ids = new HashSet<int>(Main.Select(a => a.Id)); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
źródło
Naprawiono to w Entity Framework 6 Alpha 2: http://entityframework.codeplex.com/SourceControl/changeset/a7b70f69e551
http://blogs.msdn.com/b/adonet/archive/2012/12/10/ef6-alpha-2-available-on-nuget.aspx
źródło
Cacheable alternatywa dla Contains?
To tylko mnie ugryzło, więc dodałem moje dwa pensy do łącza Sugestie funkcji Entity Framework.
Problem na pewno dotyczy generowania kodu SQL. Mam klienta, na którym dane generowanie zapytania trwało 4 sekundy, ale wykonanie było 0,1 sekundy.
Zauważyłem, że podczas korzystania z dynamicznego LINQ i OR generowanie sql trwało równie długo, ale generowało coś, co można było buforować . Więc po ponownym uruchomieniu spadło do 0,2 sekundy.
Zwróć uwagę, że kod SQL był nadal generowany.
Jeszcze coś do rozważenia, jeśli możesz znieść początkowe trafienie, liczba twoich tablic nie zmienia się zbytnio i często wykonuje zapytanie. (Testowane w LINQ Pad)
źródło
Problem dotyczy generowania kodu SQL Entity Framework. Nie może buforować zapytania, jeśli jednym z parametrów jest lista.
Aby EF buforował zapytanie, możesz przekonwertować swoją listę na ciąg i wykonać .Contains w ciągu.
Na przykład ten kod działałby znacznie szybciej, ponieważ EF mógłby buforować zapytanie:
var ids = Main.Select(a => a.Id).ToArray(); var idsString = "|" + String.Join("|", ids) + "|"; var rows = Main .Where (a => idsString.Contains("|" + a.Id + "|")) .ToArray();
Kiedy to zapytanie zostanie wygenerowane, prawdopodobnie zostanie wygenerowane przy użyciu Like zamiast In, więc przyspieszy C #, ale może potencjalnie spowolnić działanie SQL. W moim przypadku nie zauważyłem żadnego spadku wydajności w wykonywaniu mojego SQL, a C # działał znacznie szybciej.
źródło