#c# #entity-framework #entity-framework-4 #linq-to-entities #code-first
#c# #entity-framework #entity-framework-4 #linq-to-entities #сначала код
Вопрос:
Сегодня я обнаружил, что Entity Framework добавляет ненужный подзапрос к SQL, который он генерирует. Я начал копаться в своем коде, пытаясь сузить круг источников, откуда он может взяться. Некоторое (долгое) время спустя я точно указал, что является причиной этого. Но сейчас я в еще большем замешательстве, чем когда начинал, поскольку понятия не имею, почему это вызывает такое.
По сути, я обнаружил, что в определенных сценариях простое преобразование константы в переменную может изменить SQL, который генерирует Entity Framework. Я сократил все до минимума и упаковал это в маленькое консольное приложение:
using System;
using System.Data.Entity;
using System.Linq;
class Program
{
private static readonly BlogContext _db = new BlogContext();
static void Main(string[] args)
{
const string email = "foo@bar.com";
var comments = from c in _db.Comments
where c.Email == email
select c;
var result = (from p in _db.Posts
join c in comments on p.PostId equals c.PostId
orderby p.Title
select new { p.Title, c.Content });
Console.WriteLine(result);
}
}
public class BlogContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Comment> Comments { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
}
public class Comment
{
public int CommentId { get; set; }
public int PostId { get; set; }
public string Email { get; set; }
public string Content { get; set; }
}
Это показывает следующий вывод, который является идеальным:
SELECT
[Extent1].[PostId] AS [PostId],
[Extent1].[Title] AS [Title],
[Extent2].[Content] AS [Content]
FROM [dbo].[Posts] AS [Extent1]
INNER JOIN [dbo].[Comments] AS [Extent2] ON [Extent1].[PostId] = [Extent2].[PostId]
WHERE N'foo@bar.com' = [Extent2].[Email]
ORDER BY [Extent1].[Title] ASC
Теперь, если я создам email
переменную:
/*const*/ string email = "foo@bar.com";
Результат радикально меняется:
SELECT
[Project1].[PostId] AS [PostId],
[Project1].[Title] AS [Title],
[Project1].[Content] AS [Content]
FROM ( SELECT
[Extent1].[PostId] AS [PostId],
[Extent1].[Title] AS [Title],
[Extent2].[Content] AS [Content]
FROM [dbo].[Posts] AS [Extent1]
INNER JOIN [dbo].[Comments] AS [Extent2] ON [Extent1].[PostId] = [Extent2].[PostId]
WHERE [Extent2].[Email] = @p__linq__0
) AS [Project1]
ORDER BY [Project1].[Title] ASC
В качестве дополнительного примечания LINQ к SQL, похоже, этого не делает. Я знаю, что, вероятно, это нормально игнорировать, поскольку обе команды возвращают одни и те же данные. Но мне чрезвычайно любопытно, почему это происходит. До сегодняшнего дня у меня всегда было (возможно, false?) создается впечатление, что всегда безопасно превращать константу в переменную, при условии, что значение остается неизменным (что в данном случае и происходит). Итак, я должен спросить…
Почему, казалось бы, незначительное изменение вызывает такую большую разницу в сгенерированном SQL?
Обновить:
Просто для ясности, мой вопрос не о том, что email
является жестко запрограммированным значением в первом запросе и переменной во втором (что имеет весь смысл в мире). Мой вопрос о том, почему переменная version приводит к дополнительному подзапросу.
Спасибо!
Комментарии:
1. Я бы не стал беспокоиться о форме запроса — SQL предназначен для выражения желаемых результатов, а не процесса, с помощью которого их можно достичь — и я был бы удивлен, если в плане запроса есть большая или какая-либо разница. Оптимизатор должен выдавать почти такой же план запроса.
Ответ №1:
Ответ довольно прост. Ваш запрос LINQ выражается с помощью деревьев выражений. Разница с постоянной переменной по сравнению с неконстантной заключается в ConstantExpression и ParameterExpression.
Когда вы используете const, ваш запрос LINQ использует ConstExpression
для этой переменной, а когда вы используете не const, он использует ParameterExpression
, которые интерпретируются по-разному средой выполнения EF.
Константа фактически означает, что значение никогда не изменится, и значение может быть встроено в запрос.
Комментарии:
1. И это напоминает мне, что мне еще многое предстоит узнать о LINQ в целом и деревьях выражений в частности. Большое спасибо за эти ссылки, Иван.
2. Вероятно, это действительно важно. Если вы говорите, что это константа, а это не так, то он будет генерировать и кэшировать совершенно новый запрос каждый раз в SQL server. С другой стороны, если это переменная, сам запрос может быть одним и тем же, несмотря на разные значения переменной, поэтому он будет сгенерирован и кэширован только один раз. Итак … если ЗАПРОС будет выполняться с разными значениями с течением времени, тогда оставьте его переменной. Если это действительно постоянное значение в вашем приложении, тогда используйте константу. Просто.
Ответ №2:
НЕ ответ на вопрос — просто контекст использования параметров.
Это связано с созданием запроса таким образом, что он будет повторно использовать существующие планы запросов.
Если вы введете переменную (в отличие от ссылки на параметр) в сгенерированный SQL, то SQL Server (и, вероятно, другие СУБД) не смогут повторно использовать тот же план при изменении переменной.
Для констант это не проблема, потому что вы знаете, что значение всегда одно и то же, но для переменных при каждом выполнении запроса SQL и, следовательно, план запроса будут немного отличаться.
Это может показаться не таким уж большим, но в SQL выделено только определенное количество места для планов запросов, поэтому наличие сотен / тысяч незначительных изменений в кэше — это, так сказать, настоящая «пустая трата места»!
Комментарии:
1. 1 Я думаю, что это и есть ответ на вопрос. Более того, это выглядит как очень четкое объяснение одной из самых больших загадок EF.
Ответ №3:
Действительно ли это большая разница в SQL? Внутренний запрос такой же, как исходный запрос, а внешний запрос — это просто оболочка над внутренним, которая не изменяет результирующий набор.
Если это не вызывает проблем, я лично не стал бы беспокоиться об этом. Различаются ли планы запросов между двумя вариантами запроса? Я предполагаю, что они идентичны.
Комментарии:
1. Спасибо, Уилл! Ну, в этом контексте разница, вероятно, незначительна. Но фактический код, над которым я работаю, — это уровень доступа к данным в гораздо большем приложении, которое использует IQueryables повсюду. Я просто боюсь, что если небольшое изменение, подобное этому, приведет к созданию дополнительного запроса, то, возможно, большие изменения в конечном итоге приведут к некоторой неприятной путанице в T-SQL в будущем. Основная причина, по которой меня это волнует, заключается в том, что я люблю использовать LINQ и планирую продолжать использовать исключительно его (без необходимости прибегать к sprocs, когда все становится беспорядочным.)
2. @Daniel — на самом деле вы не получаете здесь дополнительного запроса — у вас все еще есть только один запрос, хотя и выглядящий немного более сложным. У SQL Server не возникнет проблем с оптимизацией внешней части запроса — честно!
3. Да, я знаю, что к базе данных обращаются только один раз, и да, я действительно хочу верить, что SQL Server достаточно умен, чтобы понять, что внешний запрос избыточен. Я просто ничего не мог с собой поделать, и мне нужно было спросить и убедиться 🙂
4. Может повлиять на производительность. Я только что столкнулся с проблемой со скоростью выполнения довольно длинного запроса. Изменение a
string
наconst string
фактически привело к ускорению запроса в 10 раз. В обоих экземплярах был сгенерирован только один запрос.
Ответ №4:
Как говорили люди. Разница между обоими запросами минимальна.
Причина в том, что выражение, которое создается при создании вашего LINQ, отличается, когда вы используете variable и когда constant. И EF поймает это и сгенерирует ваш SQL соответствующим образом. Он знает, что он никогда не изменится, поэтому его можно жестко запрограммировать в запросе для (возможного) увеличения производительности.
Редактировать: Я не думаю, что на этот вопрос есть ответ, кроме «Вот как EF это делает». Но очень хорошо известно, что EF любит создавать много подвыборок. Это может привести ко многим подвыборам для более сложных запросов. Некоторые даже не используют EF из-за этого факта. Но это просто плата за использование такого инструмента, как EF. Вы теряете мелкозернистый контроль над чем-то, что может иметь большой прирост производительности. Почему вы используете .NET, когда вы можете использовать C и получить большую производительность? Зачем использовать C, когда вы можете использовать assembly, чтобы получить больший прирост производительности?
Единственный способ обезопасить себя и по-прежнему иметь возможность использовать EF с высоким уровнем абстракции — это часто использовать SQL profiller и проверять, нет ли запросов, которые занимают слишком много времени для реальных данных. И если вы их найдете, то либо преобразуйте их в прямой SQL, либо в хранимые процедуры.
Комментарии:
1. Спасибо Euphoric. Да, я заметил, что значение жестко запрограммировано при использовании константы. Это я понимаю, и это имеет смысл. Хотя мой вопрос был о вложенном запросе.