Почему оператор Contains () так резко снижает производительность Entity Framework?
обновление 3: Согласно объявление, это было рассмотрено командой EF в EF6 alpha 2.
обновление 2: я создал предложение, чтобы исправить эту проблему. Голосовать за него,иди сюда.
рассмотрим базу данных SQL с одной очень простой таблицей.
CREATE TABLE Main (Id INT PRIMARY KEY)
я заполняю таблицу с 10 000 записей.
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)
Я создаю модель EF для таблицы и Запускаю следующий запрос в LINQPad (я использую "C# Режим "операторы", поэтому LINQPad не создает дамп автоматически).
var rows =
Main
.ToArray();
время выполнения составляет ~0,07 секунды. Теперь я добавляю оператор Contains и повторно запускаю запрос.
var ids = Main.Select(a => a.Id).ToArray();
var rows =
Main
.Where (a => ids.Contains(a.Id))
.ToArray();
время выполнения для этого случая 20.14 секунд (288 раз медленнее)!
сначала я подозревал, что T-SQL, испускаемый для запроса, занимает больше времени, поэтому я попытался вырезать и вставить его из панели SQL LINQPad в Управление SQL Server Студия.
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,...
и в результате
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 88 ms.
далее я подозревал, что LINQPad вызывает проблему, но производительность одинакова, независимо от того, запускаю ли я его в LINQPad или в консольном приложении.
Итак, похоже, что проблема находится где-то в рамках Entity Framework.
Я делаю что-то не так здесь? Это важная часть моего кода, так что я могу сделать, чтобы ускорить производительность?
Я с помощью Entity Платформа 4.1 и Sql Server 2008 R2.
обновление 1:
в приведенном ниже обсуждении были некоторые вопросы о том, произошла ли задержка во время построения EF первоначального запроса или во время анализа данных, которые он получил обратно. Чтобы проверить это, я запустил следующий код,
var ids = Main.Select(a => a.Id).ToArray();
var rows =
(ObjectQuery<MainRow>)
Main
.Where (a => ids.Contains(a.Id));
var sql = rows.ToTraceString();
что заставляет EF генерировать запрос без его выполнения в базе данных. В результате этот код потребовал ~20 секунд для запуска, поэтому кажется, что почти все время, затраченное на построение исходного запроса.
CompiledQuery на помощь тогда? Не так быстро ... CompiledQuery требует, чтобы параметры, передаваемые в запрос, были фундаментальными типами (int, string, float и т. д.). Он не будет принимать массивы или IEnumerable, поэтому я не могу использовать его для списка идентификаторов.
8 ответов:
обновление: с добавлением выражения в EF6, производительность обработки Перечислима.Содержит значительно улучшилось. Подход, описанный в этом ответе, больше не нужен.
вы правы, что большую часть времени тратится на обработку перевода запроса. Поставщик модель эф в настоящее время не включает выражение, которое представляет собой в статье, поэтому ADO.NET провайдеры могут не поддерживать в собственном коде. Вместо реализации Перечислимый.Содержит переводит его в дерево выражений OR, т. е. для чего - то, что в C# выглядит так:
new []{1, 2, 3, 4}.Contains(i)
... мы создадим дерево DbExpression, которое может быть представлено следующим образом:
((1 = @i) OR (2 = @i)) OR ((3 = @i) OR (4 = @i))
(деревья выражений должны быть сбалансированы, потому что если бы у нас были все ORs на одном длинном позвоночнике, было бы больше шансов, что посетитель выражения попадет в переполнение стека (да, мы действительно попали в это в нашем тестировании))
мы позже отправить дерево, как это ADO.NET поставщик, который может иметь возможность распознавать этот шаблон и сводить его к предложению IN во время генерации SQL.
когда мы добавили поддержку Enumerable.Содержит в EF4, мы подумали, что было бы желательно сделать это без необходимости вводить поддержку выражений IN в модели поставщика, и, честно говоря, 10 000 намного больше, чем количество элементов, которые мы ожидали, что клиенты перейдут к перечисляемым.Содержит. Тем не менее, я понимаю, что это раздражает и то, что манипуляция деревьями выражений делает вещи слишком дорогими в вашем конкретном сценарии.
Я обсуждал это с одним из наших разработчиков, и мы считаем, что в будущем мы могли бы изменить реализацию, добавив первоклассную поддержку IN. Я позабочусь о том, чтобы это было добавлено к нашему отставанию, но я не могу обещать, когда это произойдет, учитывая, что есть много других улучшений, которые мы хотели бы сделать.
к уже предложенным обходным путям в потоке я бы добавил следующее:
рассмотрите возможность создания метода, который балансирует количество циклов базы данных с количеством элементов, которые вы передаете содержит. Например, в моем собственном тестировании я заметил, что вычисление и выполнение в локальном экземпляре SQL Server запроса со 100 элементами занимает 1/60 секунды. Если вы можете написать свой запрос таким образом, что выполнение 100 запросов с 100 различными наборами идентификаторов даст вам эквивалентный результат запроса с помощью 10 000 элементов, то вы можете получить результаты примерно за 1,67 секунды вместо 18 секунд.
различные размеры блоков должны работать лучше в зависимости от запроса и задержки подключения к базе данных. Для определенных запросов, т. е. если переданная последовательность имеет дубликаты или если она Перечислима.Содержит используется во вложенном состоянии, вы можете получить повторяющиеся элементы в результатах.
вот фрагмент кода (извините, если код, используемый для нарезки входных данных на куски, выглядит следующим образом немного слишком сложно. Есть более простые способы добиться того же самого, но я пытался придумать шаблон, который сохраняет потоковую передачу для последовательности, и я не мог найти ничего подобного в LINQ, поэтому я, вероятно, перестарался с этой частью :)):
использование:
var list = context.GetMainItems(ids).ToList();
метод для контекста или репозитория:
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; } } } }
методы расширения для нарезки перечислимых последовательностей:
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); } } } }
надеюсь, что это помогает!
Если вы обнаружите проблему производительности, которая блокирует для вас, не пытайтесь тратить годы на ее решение, потому что вы, скорее всего, не добьетесь успеха, и вам придется напрямую общаться с MS (если у вас есть поддержка premium), и это займет годы.
используйте обходной путь и обходной путь в случае проблемы с производительностью, а EF означает прямой SQL. В этом нет ничего плохого. Глобальная идея, что использование EF = больше не использует SQL, является ложью. У вас есть SQL Server 2008 R2 Итак:
- создать хранимую процедуру, принимающую возвращающий табличное значение параметр для передачи идентификаторов
- пусть ваша хранимая процедура возвращает несколько результирующих наборов для эмуляции
Include
логика оптимальным способом- Если вам нужно какое-то сложное построение запросов, используйте динамический SQL внутри хранимой процедуры
- использовать
SqlDataReader
получить результаты и построить свои лица- прикрепить их к контексту и работать с ними, как если бы они были загружены из Эф
Если производительность критична для вас, вы не найдете лучшего решения. Эта процедура не может быть сопоставлена и выполнена EF, поскольку текущая версия не поддерживает ни возвращающие табличное значение параметры, ни несколько результирующих наборов.
мы смогли решить проблему EF Contains, добавив промежуточную таблицу и присоединившись к этой таблице из запроса LINQ, который должен был использовать предложение Contains. Мы смогли добиться потрясающих результатов при таком подходе. У нас есть большая модель EF, и поскольку "содержит" не допускается при предварительной компиляции запросов EF, мы получаем очень низкую производительность для запросов, которые используют предложение "содержит".
общие сведения:
создать таблицу в SQL Server-for пример
HelperForContainsOfIntType
СHelperID
наGuid
тип данных иReferenceID
наint
столбцы типа данных. Создавать различные таблицы с ReferenceID различных типов данных по мере необходимости.создать объект / EntitySet для
HelperForContainsOfIntType
и другие такие таблицы в модели EF. При необходимости создайте разные сущности / EntitySet для разных типов данных.создать вспомогательный метод в коде .NET, который принимает ввод
IEnumerable<int>
и возвращаетGuid
. Этот метод генерирует новыйGuid
и вставляет значения изIEnumerable<int>
наHelperForContainsOfIntType
вместе с генерируемымGuid
. Далее метод возвращает это вновь сгенерированноеGuid
для звонящего. Для быстрой вставки вHelperForContainsOfIntType
таблица, создать хранимую процедуру, которая принимает ввод списка значений и делает вставку. Смотрите возвращающие табличное значение параметры в SQL Server 2008 (ADO.NET). создайте разные помощники для разных типов данных или создайте общий вспомогательный метод для обработки разных тип данных.создать EF скомпилированный запрос, который похож на что-то вроде ниже:
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 );
вызовите вспомогательный метод со значениями, которые будут использоваться в
Contains
предложение и получитьGuid
для использования в запросе. Например:var containsHelperID = dbHelper.InsertIntoHelperForContainsOfIntType(new int[] { 1, 2, 3 }); var result = _selectCustomers(_dbContext, containsHelperID).ToList();
редактирование моего исходного ответа - существует возможный обходной путь, в зависимости от сложности ваших объектов. Если вы знаете sql, который EF генерирует для заполнения ваших сущностей, вы можете выполнить его непосредственно с помощью DbContext.База данных.SQL-запрос. В EF 4, я думаю, вы могли бы использовать ObjectContext.ExecuteStoreQuery, но я не пробовал.
например, используя код из моего исходного ответа ниже, чтобы создать инструкцию sql с помощью
StringBuilder
, Я смог сделать следующееvar rows = db.Database.SqlQuery<Main>(sql).ToArray();
и общее время пошло от приблизительно 26 секунд до 0,5 секунды.
Я буду первым, чтобы сказать, что это некрасиво, и, надеюсь, лучшее решение представляет собой.
обновление
немного подумав, я понял, что если вы используете соединение для фильтрации результатов, EF не нужно создавать этот длинный список идентификаторов. Это может быть сложным в зависимости от количества одновременных запросов, но я считаю, что вы можете использовать идентификаторы пользователей или идентификаторы сеансов, чтобы изолировать их.
чтобы проверить это, я создал
Target
таблица с той же схемой, какMain
. Затем я использовалStringBuilder
создатьINSERT
команды для заполненияTarget
таблица в партиях по 1000, так как это самый SQL Server будет принимать в одномINSERT
. Непосредственное выполнение инструкций sql было намного быстрее, чем прохождение EF (около 0,3 секунды против 2,5 секунд), и я считаю, что все будет в порядке, так как схема таблицы не должно измениться.и, наконец, выбрав с помощью
join
привело к гораздо более простому запросу и выполняется менее чем за 0,5 секунды.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();
и sql, созданный EF для соединения:
SELECT [Extent1].[Id] AS [Id] FROM [dbo].[Main] AS [Extent1] INNER JOIN [dbo].[Target] AS [Extent2] ON [Extent1].[Id] = [Extent2].[Id]
(оригинал ответа)
это не ответ, но я хотел поделиться некоторой дополнительной информацией, и это слишком долго, чтобы вписаться в комментарий. Я был в состоянии воспроизвести свои результаты, и есть несколько других вещей, чтобы добавить:
SQL Profiler показывает задержку между выполнением первого запроса (
Main.Select
) и второйMain.Where
запрос, поэтому я подозревал, что проблема заключалась в создании и отправке запроса такого размера (48 980 байт).однако построение одного и того же оператора sql в T-SQL динамически занимает менее 1 секунды и принимает
ids
из своегоMain.Select
оператор, создавая тот же оператор sql и выполняя его с помощьюSqlCommand
заняло 0,112 секунды, и это в том числе время, чтобы записать содержимое на консоль.на данный момент я подозреваю, что EF делает некоторый анализ/обработку для каждого из 10 000
ids
как он строит запрос. Жаль, что я не могу дать окончательный ответ и решение :(.вот код, который я пробовал в SSMS и LINQPad (пожалуйста, не критикуйте слишком резко, я спешу, пытаясь уйти с работы):
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)); } } }
Я не знаком с Entity Framework, но лучше ли perf, если вы сделаете следующее?
вместо этого:
var ids = Main.Select(a => a.Id).ToArray(); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
Как насчет этого (предполагая, что ID является int):
var ids = new HashSet<int>(Main.Select(a => a.Id)); var rows = Main.Where (a => ids.Contains(a.Id)).ToArray();
Это было исправлено на 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
кэшируемая альтернатива Contains?
Это просто укусило меня, поэтому я добавил свои два пенса в ссылку Entity Framework Feature Suggestions.
проблема, безусловно, при создании SQL. У меня есть клиент на данные ВОЗ генерация запроса была 4 секунды, но выполнение было 0.1 секунд.
Я заметил, что при использовании dynamic LINQ и ORs поколение sql занимало столько же времени, но оно генерировало что-то, что может быть cached. Поэтому при повторном выполнении это было до 0,2 секунд.
обратите внимание, что SQL in все еще был сгенерирован.
просто что-то еще, чтобы рассмотреть, если вы можете переварить первоначального обращения, количество не сильно изменится, и выполнить много запросов. (Протестировано в LINQ Pad)
проблема заключается в создании SQL Entity Framework. Он не может кэшировать запрос, если один из параметров представляет собой список.
чтобы получить EF для кэширования запроса, вы можете преобразовать свой список в строку и сделать a .Содержит в строке.
Так, например, этот код будет работать намного быстрее, так как EF может кэшировать запрос:
var ids = Main.Select(a => a.Id).ToArray(); var idsString = "|" + String.Join("|", ids) + "|"; var rows = Main .Where (a => idsString.Contains("|" + a.Id + "|")) .ToArray();
когда этот запрос будет сгенерирован, он, скорее всего, будет сгенерирован с использованием Like вместо In, поэтому он ускорит ваш C#, но он может потенциально замедлить SQL. В моем случае я не заметил снижения производительности при выполнении SQL, и C# работал значительно быстрее.