Linq To Entities - Любой VS Первый VS Существует
Я использую Entity Framework и мне нужно проверить, существует ли продукт с именем = "xyz"...
Я думаю, что я могу использовать Any(), Exists() или First().
Какой вариант лучше всего подходит для такой ситуации? Какой из них имеет лучшую производительность?
Благодарю вас,
Miguel
4 ответа
Любой переводится как "существует" на уровне базы данных. Первый переводит в Select Top 1 ... Между ними Exists будет выполнять First, потому что фактический объект не нужно извлекать, только логическое значение результата.
По крайней мере, вы не спрашивали о.Where(x => x.Count() > 0), который требует, чтобы весь набор совпадений был оценен и повторен, прежде чем вы сможете определить, что у вас есть одна запись. Любые замыкания запроса могут быть и значительно быстрее.
Ладно, я не собираюсь взвешивать это, но ответ Диего достаточно усложняет ситуацию, так что я думаю, что некоторые дополнительные объяснения в порядке.
В большинстве случаев, .Any()
будет быстрее Вот несколько примеров.
Workflows.Where(w => w.Activities.Any())
Workflows.Where(w => w.Activities.Any(a => a.Title == "xyz"))
В приведенных выше двух примерах Entity Framework создает оптимальный запрос. .Any()
Вызов является частью предиката, и Entity Framework хорошо справляется с этим. Однако, если мы сделаем результат .Any()
часть набора результатов, как это:
Workflows.Select(w => w.Activities.Any(a => a.Title == "xyz"))
... внезапно Entity Framework решает создать две версии условия, поэтому запрос выполняет вдвое больше работы, чем действительно необходимо. Тем не менее, следующий запрос не лучше:
Workflows.Select(w => w.Activities.Count(a => a.Title == "xyz") > 0)
Учитывая приведенный выше запрос, Entity Framework все равно создаст две версии условия, плюс ему также потребуется, чтобы SQL Server выполнил фактический подсчет, что означает, что он не попадет в короткое замыкание, как только найдет элемент.
Но если вы просто сравниваете эти два запроса:
Activities.Any(a => a.Title == "xyz")
Activities.Count(a => a.Title == "xyz") > 0
... который будет быстрее? Это зависит.
Первый запрос создает неэффективный запрос с двойным условием, что означает, что он займет вдвое больше времени, чем нужно.
Второй запрос вынуждает базу данных проверять каждый элемент в таблице без короткого замыкания, что означает, что она может занять до N
в разы дольше, чем нужно, в зависимости от того, сколько предметов нужно оценить, прежде чем найти совпадение. Давайте предположим, что в таблице 10000 предметов:
- Если ни один элемент в таблице не соответствует условию, этот запрос займет примерно половину времени в качестве первого запроса.
- Если первый элемент в таблице соответствует условию, этот запрос займет примерно в 5000 раз больше, чем первый запрос.
- Если один элемент в таблице совпадает, этот запрос будет в среднем в 2500 раз длиннее, чем первый запрос.
- Если запрос может использовать индекс на
Title
и ключевые столбцы, этот запрос займет примерно половину времени, как первый запрос.
Итак, в заключение, если вы:
- Использование
Entity Framework 4 (поскольку более новые версии могут улучшить структуру запроса)Entity Framework 6.1 или более раннейверсии(поскольку в 6.1.1 есть исправление для улучшения запроса), И - Запросы непосредственно к таблице (в отличие от выполнения подзапроса), И
- Использование результата напрямую (в отличие от того, что он является частью предиката), И
- Или:
- У вас есть хорошие индексы для таблицы, к которой вы обращаетесь, ИЛИ
- Вы ожидаете, что предмет не будет найден в большинстве случаев
ТОГДА можно ожидать .Any()
взять вдвое больше, чем .Count()
, Например, запрос может занять 100 миллисекунд вместо 50. Или 10 вместо 5.
В ЛЮБОМ ДРУГИХ ОБСТОЯТЕЛЬСТВАХ .Any()
должно быть по крайней мере так же быстро, и, возможно, на несколько порядков быстрее, чем .Count()
,
В любом случае, до тех пор, пока вы не определите, что это на самом деле источник плохой производительности вашего продукта, вам следует больше заботиться о том, что легко понять. .Any()
более четко и кратко излагайте то, что вы действительно пытаетесь понять, так что придерживайтесь этого.
Можно подумать Any()
дает лучшие результаты, потому что это переводит на EXISTS
запрос... но EF ужасно сломан, генерируя это (отредактировано):
SELECT
CASE WHEN ( EXISTS (SELECT
1 AS [C1]
FROM [MyTable] AS [Extent1]
WHERE Condition
)) THEN cast(1 as bit) WHEN ( NOT EXISTS (SELECT
1 AS [C1]
FROM [MyTable] AS [Extent2]
WHERE Condition
)) THEN cast(0 as bit) END AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
Вместо:
SELECT
CASE WHEN ( EXISTS (SELECT
1 AS [C1]
FROM [MyTable] AS [Extent1]
WHERE Condition
)) THEN cast(1 as bit)
ELSE cast(0 as bit) END AS [C1]
FROM ( SELECT 1 AS X ) AS [SingleRowTable1]
... фактически удваивает стоимость запроса (для простых запросов; еще хуже для сложных)
Я нашел использование .Count(condition) > 0
быстрее почти всегда (стоимость точно такая же, как и EXISTS
запрос)
Хорошо, я решил попробовать это сам. Имейте в виду, я использую поставщика OracleManagedDataAccess с OracleEntityFramework, но я предполагаю, что он создает совместимый SQL.
Я обнаружил, что First() был быстрее Any() для простого предиката. Я покажу два запроса в EF и созданный SQL. Имейте в виду, это упрощенный пример, но вопрос о том, существует ли какой-либо, существует или сначала был быстрее для простого предиката.
var any = db.Employees.Any(x => x.LAST_NAME.Equals("Davenski"));
Так к чему это разрешается в базе данных?
SELECT
CASE WHEN ( EXISTS (SELECT
1 AS "C1"
FROM "MYSCHEMA"."EMPLOYEES" "Extent1"
WHERE ('Davenski' = "Extent1"."LAST_NAME")
)) THEN 1 ELSE 0 END AS "C1"
FROM ( SELECT 1 FROM DUAL ) "SingleRowTable1"
Это создает оператор CASE. Как мы знаем, ЛЮБОЙ - это просто синтетический сахар. Он разрешается в запрос EXISTS на уровне базы данных. Это происходит, если вы также используете ЛЮБОЙ на уровне базы данных. Но, похоже, это не самый оптимизированный SQL для этого запроса. В приведенном выше примере конструкция EF Any() здесь не нужна и просто усложняет запрос.
var first = db.Employees.Where(x => x.LAST_NAME.Equals("Davenski")).Select(x=>x.ID).First();
Это разрешается в базе данных как:
SELECT
"Extent1"."ID" AS "ID"
FROM "MYSCHEMA"."EMPLOYEES" "Extent1"
WHERE ('Davenski' = "Extent1"."LAST_NAME") AND (ROWNUM <= (1) )
Теперь это выглядит как более оптимизированный запрос, чем исходный запрос. Зачем? Он не использует оператор CASE... THEN.
Я запускал эти тривиальные примеры несколько раз, и ПОЧТИ в каждом случае (не каламбур) First() был быстрее.
Вдобавок я запустил необработанный SQL-запрос, думая, что это будет быстрее:
var sql = db.Database.SqlQuery<int>("SELECT ID FROM MYSCHEMA.EMPLOYEES WHERE LAST_NAME = 'Davenski' AND ROWNUM <= (1)").First();
Производительность была на самом деле самой медленной, но похожей на конструкцию Any EF.
Размышления:
- EF Any не совсем соответствует тому, как вы можете использовать Any в базе данных. Я мог бы написать более оптимизированный запрос в Oracle с ЛЮБОЙ, чем то, что было создано EF без оператора CASE THEN.
- ВСЕГДА проверяйте созданный вами SQL в файле журнала или в окне вывода отладки.
- Если вы собираетесь использовать ЛЮБОЙ, помните, что это синтаксический сахар для EXISTS. Oracle также использует SOME, что аналогично ANY. Обычно вы собираетесь использовать его в предикате как замену IN. В этом случае он генерирует серию операторов OR в вашем предложении WHERE. Настоящая сила ANY или EXISTS заключается в том, что вы используете подзапросы и просто проверяете СУЩЕСТВОВАНИЕ связанных данных.
Вот пример, когда ЛЮБОЙ действительно имеет смысл. Я проверяю НАЛИЧИЕ связанных данных. Я не хочу получать все записи из связанной таблицы. Здесь я хочу узнать, есть ли в опросах с комментариями.
var b = db.Survey.Where(x => x.Comments.Any()).ToList();
Это сгенерированный SQL:
SELECT
"Extent1"."SURVEY_ID" AS "SURVEY_ID",
"Extent1"."SURVEY_DATE" AS "SURVEY_DATE"
FROM "MYSCHEMA"."SURVEY" "Extent1"
WHERE ( EXISTS (SELECT
1 AS "C1"
FROM "MYSCHEMA"."COMMENTS" "Extent2"
WHERE ("Extent1"."SURVEY_ID" = "Extent2"."SURVEY_ID")
))
Это оптимизированный SQL! Я считаю, что EF прекрасно справляется с созданием SQL. Но вы должны понимать, как конструкции EF сопоставляются с конструкциями БД, иначе вы можете создавать неприятные запросы.
И, вероятно, лучший способ получить количество связанных данных - выполнить явную загрузку с помощью счетчика запросов на сборку. Это намного лучше, чем примеры, приведенные в предыдущих сообщениях. В этом случае вы не загружаете связанные объекты, вы просто получаете счетчик. Здесь я просто пытаюсь узнать, сколько комментариев у меня есть для конкретного опроса.
var d = db.Survey.Find(1);
var e = db.Entry(d).Collection(f => f.Comments)
.Query()
.Count();
Any()
а также First()
используется с IEnumerable
что дает вам гибкость для ленивой оценки вещей. тем не мение Exists()
Требуется список.
Я надеюсь, что это прояснит для вас и поможет вам решить, какой из них использовать.