Dlaczego operator Contains() tak drastycznie obniża wydajność struktury encji?

Aktualizacja 3: zgodnie ztym ogłoszeniem , to zostało rozwiązane przez zespół EF w EF6 alpha 2.

Aktualizacja 2: stworzyłem sugestię, aby rozwiązać ten problem. Aby zagłosować na to, przejdź tutaj .

Rozważmy bazę danych SQL z jedną bardzo prostą tabelą.

CREATE TABLE Main (Id INT PRIMARY KEY)

Wypeł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ące zapytanie w LINQPad (używam trybu "C# Statements", więc LINQPad nie tworzy automatycznie zrzutu).

var rows = 
  Main
  .ToArray();

Czas wykonania wynosi ~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 tej sprawy wynosi 20.14 sekund (288 razy wolniej)!

Początkowo podejrzewałem, że wykonanie T-SQL emitowanego dla zapytania trwa dłużej, więc próbowałem wyciąć i wkleić go z panelu 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,...

I wynik był

SQL Server Execution Times:
  CPU time = 0 ms,  elapsed time = 88 ms.

Następna I podejrzewam, że to LINQPad był przyczyną problemu, ale wydajność jest taka sama, niezależnie od tego, czy uruchamiam go w Linqpadie, czy w aplikacji konsolowej.

Wydaje się więc, że problem jest gdzieś w ramach podmiotu.

Czy robię coś nie tak? Jest to krytyczna część mojego kodu, więc czy jest coś, co mogę zrobić, aby przyspieszyć wydajność?

Używam Entity Framework 4.1 i SQL Server 2008 R2.

UPDATE 1:

W poniższej dyskusji było kilka pytania o to, czy opóźnienie wystąpiło podczas tworzenia wstępnego zapytania przez EF, czy podczas przetwarzania 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 wygenerowania zapytania bez wykonywania go na bazie danych. Rezultatem było to, że ten kod wymagał ~20 secordów do uruchomienia, więc wygląda na to, że prawie cały czas zajmuje budowanie wstępnego zapytania.

CompiledQuery to the rescue then? Nie tak szybko ... CompiledQuery wymaga parametry przekazywane do zapytania są typami podstawowymi (int, string, float itd.). Nie akceptuje tablic ani liczb, więc nie mogę go użyć do listy identyfikatorów.

Author: Jacob, 2011-10-26

8 answers

Aktualizacja: z dodatkiem InExpression w EF6, wydajność przetwarzania wylicza.Zawiera znacznie poprawione. Podejście opisane w tej odpowiedzi nie jest już konieczne.

Masz rację, że większość czasu spędza na przetwarzaniu tłumaczenia zapytania. Model dostawcy EF nie zawiera obecnie wyrażenia reprezentującego klauzulę IN, dlatego ADO.NET dostawcy nie mogą wspierać natywnie. Zamiast tego implementacja Enumerable.Zawiera tłumaczy to na drzewo wyrażeń OR, tzn. na coś, co w C# wygląda tak:

new []{1, 2, 3, 4}.Contains(i)

... wygenerujemy drzewo DbExpression, które może być reprezentowane w następujący sposób:

((1 = @i) OR (2 = @i)) OR ((3 = @i) OR (4 = @i))

(drzewa wyrażeń muszą być zrównoważone, ponieważ gdybyśmy mieli wszystkie RNO na jednym długim kręgosłupie, byłoby więcej szans na to, że odwiedzający wyrażenie trafiłby w przepełnienie stosu (tak, faktycznie trafiliśmy to w naszych testach))

[[5]} później wysyłamy takie drzewo do ADO.NET provider, który może mieć możliwość rozpoznania tego wzorca i zredukowania go do klauzuli IN podczas generowania SQL.

Kiedy dodaliśmy wsparcie dla Enumerable.Zawiera w EF4, uznaliśmy, że pożądane jest, aby to zrobić bez konieczności wprowadzania wsparcia dla wyrażeń IN w modelu dostawcy, i szczerze mówiąc, 10,000 to znacznie więcej niż liczba elementów, które oczekiwaliśmy, że klienci przejdą do wyliczenia.Zawiera. To powiedziawszy, rozumiem, że jest to irytujące i że manipulacja wyrażenia drzewa sprawia, że rzeczy zbyt drogie w danym scenariuszu.

Omówiłem to z jednym z naszych programistów i wierzymy, że w przyszłości możemy zmienić implementację poprzez dodanie najwyższej klasy wsparcia dla IN. Upewnię się, że zostanie to dodane do naszych zaległości, ale nie mogę obiecać, kiedy to nastąpi, biorąc pod uwagę, że chcielibyśmy wprowadzić wiele innych ulepszeń.

Do obejść zaproponowanych już w wątku dodam:

Rozważ tworzenie metody, która równoważy liczbę przejazdów bazy danych z liczbą przekazywanych elementów. Na przykład, w moich własnych testach zauważyłem, że obliczanie i wykonywanie na lokalnej instancji SQL Server zapytania ze 100 elementami zajmuje 1/60 sekundy. Jeśli możesz napisać zapytanie w taki sposób, że wykonanie 100 zapytań z 100 różnymi zestawami identyfikatorów dałoby wynik równoważny zapytaniu z 10 000 elementami, to możesz uzyskać wyniki w Około 1.67 sekund zamiast 18 sekund.

Różne rozmiary bloków powinny działać lepiej w zależności od zapytania i opóźnienia połączenia z bazą danych. Dla pewnych zapytań, tj. jeśli przekazywana sekwencja ma duplikaty lub jeśli można ją wyliczyć.Contains jest używany w zagnieżdżonym stanie, w którym można uzyskać zduplikowane elementy w wynikach.

Oto fragment kodu (przepraszam, jeśli kod użyty do pokrojenia wejścia na kawałki wygląda trochę zbyt skomplikowanie. Są prostsze sposoby na osiągnięcie tego samego, ale byłem próbuję wymyślić wzór, który zachowuje streaming dla sekwencji i nie mogłem znaleźć czegoś takiego w LINQ, więc pewnie przesadziłem z tą częścią :)): {]}

Użycie:

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 rozszerzenia dla krojenia sekwencji wyliczeniowych:

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!
 66
Author: divega,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-22 22:01:02

Jeśli znajdziesz problem z wydajnością, który blokuje dla Ciebie, nie próbuj spędzać wieków na rozwiązywaniu go, ponieważ najprawdopodobniej nie odniesiesz sukcesu i będziesz musiał komunikować się z MS bezpośrednio (jeśli masz wsparcie premium) i to trwa wieki.

Użyj obejścia i obejścia w przypadku problemów z wydajnością, a EF oznacza direct SQL. Nie ma w tym nic złego. Globalny pomysł, że używanie EF = nie używanie już SQL jest kłamstwem. Masz SQL Server 2008 R2 więc:

  • Utwórz procedura składowana akceptująca parametr table valued do przekazania identyfikatorów
  • niech procedura składowana zwróci wiele zestawów wyników, aby emulować Include logikę w optymalny sposób
  • jeśli potrzebujesz skomplikowanego budowania zapytań użyj dynamicznego SQL wewnątrz procedury składowanej
  • Użyj SqlDataReader, Aby uzyskać wyniki i skonstruować swoje byty
  • dołącz je do kontekstu i pracuj z nimi tak, jakby zostały załadowane z EF

Jeśli wydajność jest dla Ciebie krytyczna, nie znajdziesz lepszego rozwiązania. Ta procedura nie może być mapowana i wykonywana przez EF, ponieważ aktualna wersja nie obsługuje ani parametrów wartości tabeli, ani wielu zestawów wyników.

 24
Author: Ladislav Mrnka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-15 14:41:15

Udało nam się rozwiązać problem EF Contains dodając tabelę pośrednią i łącząc ją z zapytania LINQ, które wymagało użycia klauzuli Contains. Udało nam się uzyskać niesamowite rezultaty dzięki takiemu podejściu. Mamy duży model EF i ponieważ "Contains" nie jest dozwolone podczas wstępnej kompilacji zapytań EF, otrzymaliśmy bardzo słabą wydajność dla zapytań, które używają klauzuli "Contains".

Przegląd:

  • Tworzenie tabeli w SQL Server-na przykład HelperForContainsOfIntType z HelperID z kolumn typu danych Guid i ReferenceID z kolumn typu danych int. Tworzenie różnych tabel z ReferenceID różnych typów danych w razie potrzeby.

  • Utwórz Entity / EntitySet dla HelperForContainsOfIntType i innych tego typu tabel w modelu EF. Utwórz różne Entity / EntitySet dla różnych typów danych w razie potrzeby.

  • Tworzenie metody pomocniczej w kodzie. NET, która pobiera dane wejściowe IEnumerable<int> i zwraca Guid. Metoda ta generuje nowy Guid i wstawia wartości z IEnumerable<int> do HelperForContainsOfIntType wraz z wygenerowanym Guid. Następnie Metoda zwraca nowo wygenerowaną Guid do wywołującego. Aby szybko wstawić do tabeli HelperForContainsOfIntType, Utwórz procedurę przechowywaną, która pobiera listę wartości i dokonuje wstawiania. Patrz parametry wartości tabeli w SQL Server 2008 (ADO.NET) . Utwórz różne helpery dla różnych typów danych lub utwórz ogólną metodę helpera do obsługi różnych typów danych.

  • Utwórz skompilowane zapytanie EF, które jest podobne do czegoś takiego jak poniżej:

    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łanie metody pomocniczej z wartościami do użycia w klauzuli Contains i uzyskanie Guid do użycia w zapytaniu. Na przykład:

    var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 });
    var result = _selectCustomers(_dbContext, containsHelperID).ToList();
    
 9
Author: Dhwanil Shah,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-15 12:30:14

Edytowanie mojej oryginalnej odpowiedzi - Istnieje możliwe obejście, w zależności od złożoności Twoich podmiotów. Jeśli znasz sql generowany przez EF w celu wypełnienia encji, możesz go wykonać bezpośrednio za pomocą DbContext.Baza danych.SqlQuery . W EF 4, myślę, że można użyć ObjectContext.ExecuteStoreQuery, ale nie próbowałem.

Na przykład, używając kodu z mojej oryginalnej Odpowiedzi poniżej do wygenerowania instrukcji sql przy użyciu StringBuilder, byłem w stanie zrobić po

var rows = db.Database.SqlQuery<Main>(sql).ToArray();

A łączny czas poszedł 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.

update

Po dłuższym namyśle zdałem sobie sprawę, że jeśli używasz join do filtrowania wyników, EF nie musi budować tak długiej listy identyfikatorów. Może to być skomplikowane w zależności od liczby jednoczesnych zapytań, ale wierzę, że można użyć identyfikatorów użytkowników lub identyfikatorów sesji żeby ich odizolować.

Aby to przetestować, stworzyłem Target tabelę z tym samym schematem co Main. Następnie użyłem StringBuilder do stworzenia INSERT poleceń wypełniających tabelę Target w partiach po 1000, ponieważ jest to większość SQL Server zaakceptuje w jednym INSERT. Bezpośrednie wykonywanie poleceń sql było znacznie szybsze niż przechodzenie przez EF (około 0,3 sekundy vs.2,5 sekundy), i uważam, że byłoby ok, ponieważ schemat tabeli nie powinien się zmieniać.

Na koniec wybranie za pomocą join spowodowało znacznie prostsze zapytanie i 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();
W tym celu należy wykonać następujące czynności:]}
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ę kilkoma dodatkowymi informacjami i jest to zdecydowanie za długie, aby zmieścić się w komentarzu. Udało mi się odtworzyć Twoje wyniki i mam kilka innych rzeczy do dodania:

SQL Profiler pokazuje opóźnienie pomiędzy wykonaniem pierwszego zapytania (Main.Select) a drugim Main.Where zapytanie, więc podejrzewałem, że problem polegał na wygenerowaniu i wysłaniu zapytania o takim rozmiarze(48,980 bajtów).

Jednak dynamiczne zbudowanie tego samego polecenia sql w T-SQL zajmuje mniej niż 1 sekundę, a pobranie ids z instrukcji Main.Select, zbudowanie tego samego polecenia sql i wykonanie go za pomocą SqlCommand Zajęło 0,112 sekundy, wliczając w to czas na zapisanie zawartości do konsoli.

W tym momencie podejrzewam, że EF robi jakąś analizę/przetwarzanie dla każdego z 10.000 ids jak buduje zapytanie. Szkoda, że nie mogę udzielić ostatecznej odpowiedzi i rozwiązania :(.

Oto kod, który próbowałem w SSMS i LINQPad (proszę nie krytykować zbyt surowo, spieszę 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));
        }
    }
}
 5
Author: Jeff Ogata,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-26 06:31:11

Nie jestem zaznajomiony z Entity Framework, ale czy perf jest lepszy, 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 ID jest int):

var ids = new HashSet<int>(Main.Select(a => a.Id));
var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
 5
Author: Shiv,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-06 05:37:48
 4
Author: Felipe Fujiy Pessoto,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-12-13 00:48:50

Cacheable alternatywa dla Contains?

To mnie ugryzło, więc dodałem moje dwa pensy do sugestii funkcji Entity Framework link.

Problem jest zdecydowanie podczas generowania SQL. Mam klienta na danych who generowanie zapytania było 4 sekundy, ale wykonanie było 0.1 sekundy.

Zauważyłem, że podczas używania dynamicznego LINQ i ORs generowanie sql trwało tak długo, ale wygenerowało coś, co może być buforowane . Więc kiedy wykonanie go ponownie trwało 0,2 sekundy.

Zauważ, że SQL in był nadal generowany.

Po prostu coś innego do rozważenia, jeśli możesz znieść początkowy hit, liczba tablic nie zmienia się zbyt wiele i uruchom zapytanie dużo. (Testowane w LINQ Pad)

 2
Author: Dave,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-04 13:57:53

Problem dotyczy generowania SQL przez Entity Framework. Nie może buforować zapytania, jeśli jeden z parametrów jest listą.

Aby uzyskać EF do buforowania zapytania można przekonwertować listę na ciąg znaków i zrobić .Zawiera na sznurku.

Więc na przykład ten kod będzie działał znacznie szybciej, ponieważ EF może 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();

Gdy to zapytanie zostanie wygenerowane, prawdopodobnie zostanie wygenerowane za pomocą Like zamiast In, więc przyspieszy to Twój C# , ale może potencjalnie spowolnić twój SQL. W moim przypadku nie zauważyłem żadnego spadku wydajności w moim wykonaniu SQL, A C# działał znacznie szybciej.

 2
Author: user2704238,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-30 17:40:29