Dlaczego operator Contains () tak dramatycznie obniża wydajność Entity Framework?

79

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.

Mikrofon
źródło
1
Czy próbowałeś var qry = Main.Where (a => ids.Contains(a.Id)); var rows = qry.ToArray();sprawdzić, która część zapytania zajmuje trochę czasu?
Andrew Cooper
to nie EF degraduje zapytanie, jest to rzeczywiste zapytanie, które próbujesz uruchomić; czy mógłbyś wyjaśnić, co próbujesz zrobić? być może jest lepsze podejście do twoich potrzeb
Kris Ivanov
@AndrewCooper Właśnie to wypróbowałem i ze względu na odroczone wykonanie pierwsza instrukcja (bez ToArray) jest wykonywana niemal natychmiastowo. Zapytanie, w tym filtrowanie Contains, nie jest faktycznie uruchamiane, dopóki nie wykonasz metody ToArray ().
Mike
5
Po prostu zaktualizuj to: EF6 alpha 2 zawiera ulepszenie, które przyspiesza tłumaczenie Enumerable.Contains. Zobacz ogłoszenie tutaj: blogs.msdn.com/b/adonet/archive/2012/12/10/… . Moje własne testy pokazują, że tłumaczenie list. Zawiera (x) dla listy zawierającej 100 000 elementów int zajmuje teraz mniej niż sekundę, a czas rośnie w przybliżeniu liniowo wraz z liczbą elementów na liście. Dziękujemy za Twoją opinię i pomoc w ulepszaniu EF!
divega
1
Uważaj na to ... zapytania z dowolnym parametrem IEnumerable nie mogą być buforowane, co może powodować poważne skutki uboczne, gdy plany zapytań są skomplikowane. Jeśli musisz wykonywać operacje wiele razy (np. Używając Contains do pobierania fragmentów danych), możesz mieć dość nieprzyjemne czasy rekompilacji zapytań! Sprawdź źródło dla siebie, a zobaczysz, że parent._recompileRequired = () => true;dzieje się to dla wszystkich zapytań zawierających parametr IEnumerable <T>. Gwizd!
wesoły

Odpowiedzi:

66

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!

divega
źródło
Aby wyjaśnić !(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 to stats.EndOfSequencejako truetylko wtedy, gdy pozostały elementy do pobrania, ale dotarłeś do końca wyliczania.
arviman
Być może wydajność przetwarzania Enumerable.Containsznacznie 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.
Nik
24

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:

  • Utwórz procedurę składowaną akceptującą parametr wyceniony w tabeli, aby przekazać Twoje identyfikatory
  • Pozwól, aby procedura składowana zwracała wiele zestawów wyników, aby emulować Includelogikę w optymalny sposób
  • Jeśli potrzebujesz jakiegoś złożonego tworzenia zapytań, użyj dynamicznego SQL wewnątrz procedury składowanej
  • Służy SqlDataReaderdo uzyskiwania wyników i konstruowania jednostek
  • Dołącz je do kontekstu i pracuj z nimi tak, jakby były ładowane z EF

Jeś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.

Ladislav Mrnka
źródło
@Laddislav Mrnka Napotkaliśmy podobny problem z wydajnością ze względu na list.Contains (). Spróbujemy stworzyć procedury poprzez przekazywanie identyfikatorów. Czy powinniśmy doświadczyć spadku wydajności, jeśli uruchomimy tę procedurę za pośrednictwem EF?
Kurubaran
9

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 HelperForContainsOfIntTypeze HelperIDz Guiddanych typu oraz ReferenceIDz intkolumnami danych typu. W razie potrzeby utwórz różne tabele z identyfikatorami referencyjnymi różnych typów danych.

  • Utwórz Entity / EntitySet dla HelperForContainsOfIntTypei 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 plik Guid. Ta metoda generuje nowy Guidi wstawia wartości z IEnumerable<int>do HelperForContainsOfIntTypewraz z wygenerowanym plikiem Guid. Następnie metoda zwraca to nowo wygenerowane Guiddo obiektu wywołującego. Aby szybko wstawić do HelperForContainsOfIntTypetabeli, 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 Containsklauzuli, i pobierz Guiddo użycia w zapytaniu. Na przykład:

    var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 });
    var result = _selectCustomers(_dbContext, containsHelperID).ToList();
    
Dhwanil Shah
źródło
Dzięki za to! Użyłem odmiany twojego rozwiązania, aby rozwiązać mój problem.
Mike,
5

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ści

var 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 Targettabelę z tym samym schematem co Main. Następnie użyłem a, StringBuilderaby utworzyć INSERTpolecenia do zapełnienia Targettabeli partiami po 1000, ponieważ jest to najwięcej, jakie SQL Server zaakceptuje w jednym INSERT. 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 joinspowodował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 drugim Main.Wherezapytaniem, 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 idsz Main.Selectinstrukcji, zbudowanie tej samej instrukcji sql i wykonanie jej przy użyciu SqlCommand0,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 idspodczas 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));
        }
    }
}
Jeff Ogata
źródło
Dziękuję za twoją pracę. Wiedząc, że udało ci się to odtworzyć, czuję się lepiej - przynajmniej nie jestem szalony! Niestety, twoje obejście nie pomaga w moim przypadku, ponieważ, jak można się domyślić, przykład, który tu podałem, został maksymalnie uproszczony, aby wyodrębnić problem. Moje rzeczywiste zapytanie obejmuje dość skomplikowany schemat .Include () znajduje się w kilku innych tabelach, a także kilka innych operatorów LINQ.
Mike
@Mike, dodałem kolejny pomysł, który sprawdzi się w przypadku złożonych podmiotów. Miejmy nadzieję, że nie byłoby to zbyt trudne do wdrożenia, gdybyś nie miał innego wyjścia.
Jeff Ogata
Zrobiłem kilka testów i myślę, że masz rację, że opóźnienie polega na utworzeniu kodu SQL przed jego wykonaniem. Zaktualizowałem moje pytanie o szczegóły.
Mike
@Mike, czy mogłeś spróbować dołączyć do identyfikatorów (zobacz aktualizację w mojej odpowiedzi)?
Jeff Ogata
Skończyło się na tym, że użyłem odmiany twojego podejścia do rozwiązania problemu z wydajnością. Okazało się, że jest dość brzydki, ale prawdopodobnie najlepsza opcja, dopóki (i jeśli) Microsoft nie rozwiąże tego problemu.
Mike
5

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();
Shiv
źródło
Nie wiem dlaczego i jak, ale zadziałało jak urok :) Dziękuję bardzo :)
Wahid Bitar
1
Wyjaśnieniem, dlaczego wydajność jest lepsza, jest int []. Zawiera wywołanie w pierwszym wywołaniu to O (n) - potencjalnie pełne skanowanie tablicy - podczas gdy wywołanie HashSet <int> .Contains to O (1). Zobacz stackoverflow.com/questions/9812020/…, aby uzyskać informacje o wydajności funkcji hashset.
Shiv
3
@Shiv Nie wierzę, że to prawda. EF pobierze dowolną kolekcję i przetłumaczy ją na język SQL. Rodzaj kolekcji nie powinien być problemem.
Rob
@Rob Jestem sceptyczny - nie potrafię wyjaśnić różnicy w wydajności, jeśli tak jest. Może trzeba będzie przeanalizować plik binarny, aby zobaczyć, co zrobił.
Shiv
1
HashSet nie jest IEnumerable. IEnumerables wywołujące .Contains in LINQ działają słabo (przynajmniej przed EF6.)
Jason Beck
2

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)

Dave
źródło
Głosuj również na to w witrynie codeplex < entityframework.codeplex.com/workitem/245 >
Dave,
2

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.

user2704238
źródło
1
Niezły pomysł, ale nie wykorzysta to żadnego indeksu w danej kolumnie.
wydający
Tak, to prawda, dlatego wspomniałem, że może to spowolnić wykonywanie SQL. Wydaje mi się, że jest to tylko potencjalna alternatywa, jeśli nie możesz użyć konstruktora predykatów i pracujesz z wystarczająco małym zestawem danych, więc możesz sobie pozwolić na nie używanie indeksu. Przypuszczam również, że powinienem był wspomnieć, że preferowaną opcją jest konstruktor predykatów
user2704238
1
Cóż za NIESAMOWITE rozwiązanie. Udało nam się wydłużyć czas wykonywania zapytań produkcyjnych z ~ 12600 milisekund do zaledwie ~ 18 milisekund. To jest OGROMNA poprawa. Dziękuję Ci bardzo !!!
Jacob