2016-08-18 44 views
10

Mam następujący ogólny queryable (który może już pozycje Stosowanej):łączenia wyrażeń zamiast korzystania z wielu zapytań w Entity Framework

IQueryable<TEntity> queryable = DBSet<TEntity>.AsQueryable(); 

Wtedy nie ma klasy Provider, który wygląda tak:

public class Provider<TEntity> 
{ 
    public Expression<Func<TEntity, bool>> Condition { get; set; } 

    [...] 
} 

Condition można określić na przykład w następujący sposób:

Condition = entity => entity.Id == 3; 

Teraz chcę zaznaczyć wszystkie Provider wystąpień, które mają Condition że spełniony jest co najmniej przez jeden podmiot w DBSet:

List<Provider> providers = [...]; 
var matchingProviders = providers.Where(provider => queryable.Any(provider.Condition)) 

problem z tym: Zaczynam kwerendy dla każdej instancji w Provider lista. Wolałbym użyć pojedynczego zapytania, aby osiągnąć ten sam wynik. Ten temat jest szczególnie ważny z powodu wątpliwej wydajności. Jak uzyskać te same wyniki za pomocą pojedynczego zapytania i poprawić wydajność przy użyciu instrukcji Linq lub Expression Trees?

+0

To zależy w dużym stopniu od tego, czym jest 'provider.Condition'. Jeśli można to przetłumaczyć przez EF na kwerendę sql, wówczas możesz zebrać wszystkie warunki i użyć ich w jednym zapytaniu - na odwrocie 'queryable.Any (provider.Condition)'. – Will

+0

Wszystkie warunki, których chcę użyć, mogą zostać przetłumaczone na zapytania SQL przez EF –

+0

** To zależy w dużym stopniu od tego, co 'provider.Condition' jest **. Jeśli można to przetłumaczyć przez EF na kwerendę sql, wówczas możesz zebrać wszystkie warunki i użyć ich w jednym zapytaniu - na odwrocie 'queryable.Any (provider.Condition)'. – Will

Odpowiedz

4

Interesujące wyzwanie. Jedynym sposobem, widzę jest budowanie dynamicznie UNION ALL zapytanie tak:

SELECT TOP 1 0 FROM Table WHERE Condition[0] 
UNION ALL 
SELECT TOP 1 1 FROM Table WHERE Condition[1] 
... 
UNION ALL 
SELECT TOP 1 N-1 FROM Table WHERE Condition[N-1] 

a następnie użyj zwracanych liczb jako wskaźnik, aby uzyskać dostawcom ogłoszeń.

coś takiego:

var parameter = Expression.Parameter(typeof(TEntity), "e"); 
var indexQuery = providers 
    .Select((provider, index) => queryable 
     .Where(provider.Condition) 
     .Take(1) 
     .Select(Expression.Lambda<Func<TEntity, int>>(Expression.Constant(index), parameter))) 
    .Aggregate(Queryable.Concat); 

var indexes = indexQuery.ToList(); 
var matchingProviders = indexes.Select(index => providers[index]); 

Zauważ, że mogłem zbudowany kwerendy bez użycia Expression klasę zastępując powyższą Select z

.Select(_ => index) 

ale wprowadziłoby niepotrzebne parametr zapytania SQL dla każdego indeks.

+0

Czy to jednak nie jest to samo? Wciąż jest wiele zapytań, tylko tym razem są one ZWIĄZANE ze sobą. – DavidG

+0

@DavidG Tak. Technicznie jest to pojedyncze zapytanie SQL, ale nie ma gwarancji, że będzie ono bardziej wydajne niż wykonywanie wielu zapytań. Chociaż teoretycznie optymalizator SQL może stworzyć skuteczny plan wykonania, więc to przynajmniej daje mu taką szansę :) –

+0

Uważam, że jest to bardzo ciekawe i musiałem to przetestować natychmiast, kod działa poprawnie i przegłosowałem również twoją odpowiedź. Niestety nadal wydaje się wolniejszy, zgodnie z niektórymi benchmarkami używam: 'Old Method: 148ms'

4

Oto kolejny (szalony) pomysł, który przyszedł mi do głowy. Należy pamiętać, że podobnie jak w przypadku mojej poprzedniej odpowiedzi, nie gwarantuje to lepszej wydajności (w rzeczywistości może być gorsza). Po prostu przedstawia sposób wykonania tego, o co prosisz, za pomocą pojedynczego zapytania SQL.

Tutaj utworzymy zapytanie, które zwraca pojedynczą wartość string o długości N składającej się ze znaków "0" i "1" z "1" oznaczającym dopasowanie (coś w rodzaju tablicy bitów ciągów znaków). Kwerenda będzie używać mojego ulubionego grupę o stałej techniki zbudować dynamicznie coś takiego:

var matchInfo = queryable 
    .GroupBy(e => 1) 
    .Select(g => 
     (g.Max(Condition[0] ? "1" : "0")) + 
     (g.Max(Condition[1] ? "1" : "0")) + 
      ... 
     (g.Max(Condition[N-1] ? "1" : "0"))) 
    .FirstOrDefault() ?? ""; 

A oto kod:

var group = Expression.Parameter(typeof(IGrouping<int, TEntity>), "g"); 

var concatArgs = providers.Select(provider => Expression.Call(
     typeof(Enumerable), "Max", new[] { typeof(TEntity), typeof(string) }, 
     group, Expression.Lambda(
      Expression.Condition(
       provider.Condition.Body, Expression.Constant("1"), Expression.Constant("0")), 
      provider.Condition.Parameters))); 

var concatCall = Expression.Call(
    typeof(string).GetMethod("Concat", new[] { typeof(string[]) }), 
    Expression.NewArrayInit(typeof(string), concatArgs)); 

var selector = Expression.Lambda<Func<IGrouping<int, TEntity>, string>>(concatCall, group); 

var matchInfo = queryable 
    .GroupBy(e => 1) 
    .Select(selector) 
    .FirstOrDefault() ?? ""; 

var matchingProviders = matchInfo.Zip(providers, 
    (match, provider) => match == '1' ? provider : null) 
    .Where(provider => provider != null) 
    .ToList(); 

Enjoy :)

PS: Moim zdaniem, to zapytanie będzie działać ze stałą prędkością (w odniesieniu do liczby i rodzaju warunków, tj.można uznać za O (N) w najlepszych, najgorszych i przeciętnych przypadkach, gdzie N jest liczbą rekordów w tabeli), ponieważ baza danych musi zawsze wykonywać pełne skanowanie tabeli. Wciąż będzie interesujące wiedzieć, jaka jest rzeczywista wydajność, ale najprawdopodobniej zrobienie czegoś takiego nie jest warte wysiłku.

Aktualizacja: chodzi o dobroci i zaktualizowanego Wymagania:

znaleźć szybką kwerendę czyta tylko rekord tabeli raz i kończy się zapytanie, czy są już spełnione wszystkie warunki

Nie ma standardowej konstrukcji SQL (nawet nie mówi się o translacji kwerend LINQ), która spełnia oba warunki. Konstrukty, które pozwalają na wczesny koniec, takie jak EXISTS, mogą być używane dla pojedynczego warunku, więc gdy wykonywane dla wielu warunków będą naruszać pierwszą regułę czytania rekordu tabeli tylko jeden raz. Podczas gdy konstrukcje, które używają agregatów jak w tej odpowiedzi, spełniają pierwszą regułę, ale aby wytworzyć zagregowaną wartość, muszą odczytać wszystkie rekordy, a zatem nie mogą wyjść wcześniej.

Wkrótce nie ma zapytania spełniającego oba wymagania. A co z częścią? szybka, to naprawdę zależy od wielkości danych oraz liczby i rodzaju warunków, indeksów tabel itd., Więc znowu nie ma po prostu "najlepszego" ogólnego rozwiązania dla wszystkich przypadków.

3

Na podstawie tego Post przez @Ivan stworzyłem wyrażenie, które jest nieco szybsze w niektórych przypadkach.

Używa Any zamiast Max, aby uzyskać pożądane wyniki.

var group = Expression.Parameter(typeof(IGrouping<int, TEntity>), "g"); 

var anyMethod = typeof(Enumerable) 
    .GetMethods() 
    .First(m => m.Name == "Any" && m.GetParameters() 
    .Count() == 2) 
    .MakeGenericMethod(typeof(TEntity)); 

var concatArgs = Providers.Select(provider => 
    Expression.Call(anyMethod, group, 
    Expression.Lambda(provider.Condition.Body, provider.Condition.Parameters))); 

var convertExpression = concatArgs.Select(concat => 
    Expression.Condition(concat, Expression.Constant("1"), Expression.Constant("0"))); 

var concatCall = Expression.Call(
    typeof(string).GetMethod("Concat", new[] { typeof(string[]) }), 
    Expression.NewArrayInit(typeof(string), convertExpression)); 

var selector = Expression.Lambda<Func<IGrouping<int, TEntity>, string>>(concatCall, group); 

var matchInfo = queryable 
    .GroupBy(e => 1) 
    .Select(selector) 
    .First(); 

var MatchingProviders = matchInfo.Zip(Providers, 
    (match, provider) => match == '1' ? provider : null) 
    .Where(provider => provider != null) 
    .ToList(); 
+1

Miło, że znalazłeś tę alternatywę samemu! (+1) Rozważałem to, ale nie lubiłem wygenerowanego SQL. Oczywiście ładniej wyglądający SQL nie zawsze jest lepszy :) –

1

Podejście próbowałem tutaj było stworzenie Conditions oraz gniazdo je w jeden Expression. Jeśli jeden z Conditions zostanie spełniony, otrzymamy indeks dla Provider.

private static Expression NestedExpression(
    IEnumerable<Expression<Func<TEntity, bool>>> expressions, 
    int startIndex = 0) 
{ 
    var range = expressions.ToList(); 
    range.RemoveRange(0, startIndex); 

    if (range.Count == 0) 
     return Expression.Constant(-1); 

    return Expression.Condition(
     range[0].Body, 
     Expression.Constant(startIndex), 
     NestedExpression(expressions, ++startIndex)); 
} 

Ponieważ Expressions nadal mogą korzystać z różnych ParameterExpressions, potrzebujemy ExpressionVisitor przepisać ci:

private class PredicateRewriterVisitor : ExpressionVisitor 
{ 
    private readonly ParameterExpression _parameterExpression; 

    public PredicateRewriterVisitor(ParameterExpression parameterExpression) 
    { 
     _parameterExpression = parameterExpression; 
    } 

    protected override Expression VisitParameter(ParameterExpression node) 
    { 
     return _parameterExpression; 
    } 
} 

Dla przepisanie musimy jedynie wywołanie tej metody:

private static Expression<Func<T, bool>> Rewrite<T>(
    Expression<Func<T, bool>> exp, 
    ParameterExpression parameterExpression) 
{ 
    var newExpression = new PredicateRewriterVisitor(parameterExpression).Visit(exp); 
    return (Expression<Func<T, bool>>)newExpression; 
} 

samo zapytanie i wybór instancji Provider działa w następujący sposób:

var parameterExpression = Expression.Parameter(typeof(TEntity), "src"); 
var conditions = Providers.Select(provider => 
    Rewrite(provider.Condition, parameterExpression) 
); 

var nestedExpression = NestedExpression(conditions); 
var lambda = Expression.Lambda<Func<TEntity, int>>(nestedExpression, parameterExpression); 

var matchInfo = queryable.Select(lambda).Distinct(); 
var MatchingProviders = Providers.Where((provider, index) => matchInfo.Contains(index)); 

Uwaga: Innym rozwiązaniem, które nie jest bardzo szybki, a także

+0

Również to nie przyniesie poprawnych wyników w niektórych przypadkach (na przykład, tylko 1 rekord pasujący do wszystkich filtrów). Więc jaki jest wniosek? Zrobiłem kilka testów z rekordami 1M i 10 warunkami. Wyniki różnią się w zależności od warunków, ale ogólnie rzecz biorąc, zapytania adaptacyjne (które mogą zwracać wyniki wcześniej, niezależnie od tego, czy wymagają wielu odczytów rekordów), działają lepiej. W moich testach zapytanie unijne jest najszybsze w przypadku przeciętnym, a następnie w oryginale (wiele zapytań), a następnie w odpowiedzi na pytanie "Any". Najgorsze jest zapytanie "Max" (szczególnie jeśli warunki zawierają zmienne). –

+0

Masz rację. Dla mojego przypadku użycia nadal wydaje się, że wersja z wielokrotnym żądaniem z tego pytania jest nadal najszybsza ze wszystkich. a następnie twoja metoda unii. Jeśli wszystko inne zawiedzie, jutro nagrodzę cię nagrodą. –

+0

Tu nie chodzi o nagrodę. Jak wspomniałem w pierwszym poście, jest to interesująca kwestia i naprawdę chciałem naprawdę poprawić. –

1

Oto kolejny widok na problem, który nie ma nic wspólnego z wyrażeń.

Ponieważ głównym celem jest poprawienie wydajności, jeśli próby uzyskania wyniku za pomocą pojedynczego zapytania nie pomogą, możemy spróbować zwiększyć prędkość, równolegle z wykonaniem oryginalnego rozwiązania wieloprocesorowego.

Ponieważ jest to naprawdę LINQ to Objects zapytania (które wewnętrznie wykonuje kilka zapytań EF), teoretycznie powinna być prosta sprawa przekształcając go w PLINQ zapytania poprzez wstawienie AsParallel tak (non robocza):

var matchingProviders = providers 
    .AsParallel() 
    .Where(provider => queryable.Any(provider.Condition)) 
    .ToList(); 

Jednak okazuje się, że EF DbContext nie jest dobrze dostosowany do dostępu wielowątkowego, a powyższe po prostu generuje błędy środowiska wykonawczego. Musiałem więc odwołać się do TPL, używając jednego z przeciążeń, które pozwala nam dostarczyć stan lokalny, który przydzielałem kilku instancjom DbContext podczas wykonywania.

Ostateczny kod pracy wygląda następująco:

var matchingProviders = new List<Provider<TEntity>>(); 
Parallel.ForEach(providers, 
    () => new 
    { 
     context = new MyDbContext(), 
     matchingProviders = new List<Provider<TEntity>>() 
    }, 
    (provider, state, data) => 
    { 
     if (data.context.Set<TEntity>().Any(provider.Condition)) 
      data.matchingProviders.Add(provider); 
     return data; 
    }, 
    data => 
    { 
     data.context.Dispose(); 
     if (data.matchingProviders.Count > 0) 
     { 
      lock (matchingProviders) 
       matchingProviders.AddRange(data.matchingProviders); 
     } 
    } 
); 

Jeśli masz multi core CPU (co jest normalne w dzisiejszych czasach) i dobry serwer bazy danych, to powinno dać poprawę Szukasz dla.