Подтвердить что ты не робот

Должен ли порядок предложений запроса LINQ влиять на производительность платформы Entity Framework?

Я использую Entity Framework (сначала код) и поиск порядка, который я определяю в моих запросах LINQ, оказывает огромное влияние на производительность, например:

using (var db = new MyDbContext())
{
    var mySize = "medium";
    var myColour = "vermilion";
    var list1 = db.Widgets.Where(x => x.Colour == myColour && x.Size == mySize).ToList();
    var list2 = db.Widgets.Where(x => x.Size == mySize && x.Colour == myColour).ToList();
}

В тех случаях, когда предложение (редкое) цветовое предложение предшествует (общему) условию размера быстро, но наоборот наоборот медленнее. В таблице содержится несколько миллионов строк, и два рассматриваемых поля - nvarchar (50), поэтому не нормируются, но каждый индексируется. Поля указаны в первом порядке кода следующим образом:

    [StringLength(50)]
    public string Colour { get; set; }

    [StringLength(50)]
    public string Size { get; set; }

Я действительно должен был беспокоиться о таких вещах в моих запросах LINQ, я думал, что это была работа с базой данных?

Системные спецификации:

  • Visual Studio 2010
  • .NET 4
  • EntityFramework 6.0.0-beta1
  • Сервер SQL Server 2008 R2 (64 бит)

Обновление:

Правильно, для любых обжарок для наказания эффект можно воспроизвести, как показано ниже. Этот вопрос, судя по всему, чрезвычайно чувствителен к ряду факторов, поэтому, пожалуйста, воздержитесь от надуманного характера:

Установите EntityFramework 6.0.0-beta1 через nuget, затем сгенерируйте первый стиль кода с помощью:

public class Widget
{
    [Key]
    public int WidgetId { get; set; }

    [StringLength(50)]
    public string Size { get; set; }

    [StringLength(50)]
    public string Colour { get; set; }
}

public class MyDbContext : DbContext
{
    public MyDbContext()
        : base("DefaultConnection")
    {
    }

    public DbSet<Widget> Widgets { get; set; }
}

Создайте фиктивные данные со следующим SQL:


insert into gadget (Size, Colour)
select RND1 + ' is the name is this size' as Size,
RND2 + ' is the name of this colour' as Colour
from (Select top 1000000
CAST(abs(Checksum(NewId())) % 100 as varchar) As RND1,
CAST(abs(Checksum(NewId())) % 10000 as varchar) As RND2
from master..spt_values t1 cross join master..spt_values t2) t3

Добавьте по одному индексу для цвета и размера, затем выполните запрос:


string mySize = "99 is the name is this size";
string myColour = "9999 is the name of this colour";
using (var db = new WebDbContext())
{
    var list1= db.Widgets.Where(x => x.Colour == myColour && x.Size == mySize).ToList();
}
using (var db = new WebDbContext())
{
    var list2 = db.Widgets.Where(x => x.Size == mySize && x.Colour == myColour).ToList();
}

Проблема связана с тупой коллекцией сравнений NULL в сгенерированном SQL, как показано ниже.

exec sp_executesql N'SELECT 
[Extent1].[WidgetId] AS [WidgetId], 
[Extent1].[Size] AS [Size], 
[Extent1].[Colour] AS [Colour]
FROM [dbo].[Widget] AS [Extent1]
WHERE ((([Extent1].[Size] = @p__linq__0) 
AND ( NOT ([Extent1].[Size] IS NULL OR @p__linq__0 IS NULL))) 
OR (([Extent1].[Size] IS NULL) AND (@p__linq__0 IS NULL))) 
AND ((([Extent1].[Colour] = @p__linq__1) AND ( NOT ([Extent1].[Colour] IS NULL 
OR @p__linq__1 IS NULL))) OR (([Extent1].[Colour] IS NULL) 
AND (@p__linq__1 IS NULL)))',N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',
@p__linq__0=N'99 is the name is this size',
@p__linq__1=N'9999 is the name of this colour'
go

Изменение оператора равенства в LINQ to StartWith() заставляет проблему уйти, как и изменение одного из двух полей, которые не могут быть обнулены в базе данных.

Я отчаиваюсь!

Обновление 2:

Некоторая помощь для любых охотников за головами, проблема может быть воспроизведена на SQL Server 2008 R2 Web (64 бит) в чистой базе данных следующим образом:

CREATE TABLE [dbo].[Widget](
    [WidgetId] [int] IDENTITY(1,1) NOT NULL,
    [Size] [nvarchar](50) NULL,
    [Colour] [nvarchar](50) NULL,
 CONSTRAINT [PK_dbo.Widget] PRIMARY KEY CLUSTERED 
(
    [WidgetId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX IX_Widget_Size ON dbo.Widget
    (
    Size
    ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX IX_Widget_Colour ON dbo.Widget
    (
    Colour
    ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO


insert into Widget (Size, Colour)
select RND1 + ' is the name is this size' as Size,
RND2 + ' is the name of this colour' as Colour
from (Select top 1000000
CAST(abs(Checksum(NewId())) % 100 as varchar) As RND1,
CAST(abs(Checksum(NewId())) % 10000 as varchar) As RND2
from master..spt_values t1 cross join master..spt_values t2) t3
GO

а затем сравните относительную производительность следующих двух запросов (вам может потребоваться настроить значения параметров параметров, чтобы получить запрос, который возвращает пару строк, чтобы наблюдать эффект, то есть второй идентификатор запроса намного медленнее).

exec sp_executesql N'SELECT 
[Extent1].[WidgetId] AS [WidgetId], 
[Extent1].[Size] AS [Size], 
[Extent1].[Colour] AS [Colour]
FROM [dbo].[Widget] AS [Extent1]
WHERE ((([Extent1].[Colour] = @p__linq__0) 
AND ( NOT ([Extent1].[Colour] IS NULL 
OR @p__linq__0 IS NULL))) 
OR (([Extent1].[Colour] IS NULL) 
AND (@p__linq__0 IS NULL))) 
AND ((([Extent1].[Size] = @p__linq__1) 
AND ( NOT ([Extent1].[Size] IS NULL 
OR @p__linq__1 IS NULL))) 
OR (([Extent1].[Size] IS NULL) AND (@p__linq__1 IS NULL)))',
N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',
@p__linq__0=N'9999 is the name of this colour',
@p__linq__1=N'99 is the name is this size'
go

exec sp_executesql N'SELECT 
[Extent1].[WidgetId] AS [WidgetId], 
[Extent1].[Size] AS [Size], 
[Extent1].[Colour] AS [Colour]
FROM [dbo].[Widget] AS [Extent1]
WHERE ((([Extent1].[Size] = @p__linq__0) 
AND ( NOT ([Extent1].[Size] IS NULL 
OR @p__linq__0 IS NULL))) 
OR (([Extent1].[Size] IS NULL) 
AND (@p__linq__0 IS NULL))) 
AND ((([Extent1].[Colour] = @p__linq__1) 
AND ( NOT ([Extent1].[Colour] IS NULL 
OR @p__linq__1 IS NULL))) 
OR (([Extent1].[Colour] IS NULL) 
AND (@p__linq__1 IS NULL)))',
N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',
@p__linq__0=N'99 is the name is this size',
@p__linq__1=N'9999 is the name of this colour'

Вы также можете найти, как и я, что, если вы перезапустите фиктивную вставку данных, чтобы было теперь два миллиона строк, проблема исчезнет.

4b9b3361

Ответ 1

Ядро вопроса заключается не в том, "почему вопрос имеет дело с LINQ?". LINQ просто переводит буквально без переупорядочения. Реальный вопрос: "Почему два SQL-запроса имеют разную производительность?".

Я смог воспроизвести проблему, только вставив строки 100k. В этом случае срабатывает слабость оптимизатора: он не распознает, что он может выполнять поиск на Colour из-за сложного состояния. В первом запросе оптимизатор распознает шаблон и создает поиск индекса.

Нет семантической причины, почему это должно быть. Поиск по индексу возможен даже при поиске на NULL. Это слабость/ошибка в оптимизаторе. Вот два плана:

enter image description here

EF пытается помочь здесь, потому что предполагает, что и столбец, и переменная фильтра могут быть нулевыми. В этом случае он пытается дать вам соответствие (что в соответствии с семантикой С# правильное).

Я попытался отменить это, добавив следующий фильтр:

Colour IS NOT NULL AND @p__linq__0 IS NOT NULL
AND Size IS NOT NULL AND @p__linq__1 IS NOT NULL

Надеясь, что оптимизатор теперь использует эти знания для упрощения сложного выражения фильтра EF. Это не удалось. Если бы это сработало, то такой же фильтр мог бы быть добавлен в EF-запрос, обеспечивающий легкое исправление.

Вот исправления, которые я рекомендую в том порядке, в котором вы должны их попробовать:

  • Сделать базы данных столбцами не-null в базе данных
  • Сделайте столбцы не-null в модели данных EF, надеясь, что это не позволит EF создать сложное условие фильтра.
  • Создайте индексы: Colour, Size и/или Size, Colour. Они также устраняют проблему.
  • Убедитесь, что фильтрация выполнена в правильном порядке и оставить комментарий кода
  • Попробуйте использовать INTERSECT/Queryable.Intersect для объединения фильтров. Это часто приводит к различным формам плана.
  • Создайте встроенную функцию таблицы, которая выполняет фильтрацию. EF может использовать такую ​​функцию как часть более крупного запроса
  • Сбросьте исходный SQL
  • Используйте план-план для изменения плана.

Все это обходные решения, а не исправления основных причин.

В конце концов, меня не устраивают как SQL Server, так и EF. Оба продукта должны быть исправлены. Увы, их, вероятно, не будет, и вы не можете дождаться этого.

Вот скрипты индекса:

CREATE NONCLUSTERED INDEX IX_Widget_Colour_Size ON dbo.Widget
    (
    Colour, Size
    ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
CREATE NONCLUSTERED INDEX IX_Widget_Size_Colour ON dbo.Widget
    (
   Size, Colour
    ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

Ответ 2

Трудно узнать, вызвана ли эта проблема запуском бета-версии EF, но SQL, сгенерированный для простого запроса LINQ, неожиданно бесполезен с точки зрения оптимизатора запросов SQL Server.

Общие конструкции SQL для обработки сопоставимых сравнений с нулевым значением имеют определенную поддержку оптимизатора запросов SQL Server. Одна (из многих) совместимых форм SQL-запросов:

(x = y) OR (x IS NULL AND y IS NULL)

SQL-код, созданный EF, следует следующему шаблону:

((x = y) AND NOT (x IS NULL OR y IS NULL)) OR (x IS NULL AND y IS NULL)

Дополнительное предложение AND NOT является избыточным; однако он достаточно непрозрачен, чтобы избежать согласования упрощенного оптимизатора запросов. Ирония заключается в том, что многие правила упрощения в оптимизаторе запросов существуют исключительно для поддержки сгенерированного кода.

Если EF сгенерировал код, соответствующий первой форме SQL (например):

SELECT
    W.WidgetId, 
    W.Size, 
    W.Colour 
FROM dbo.Widget AS W
WHERE 
    (
        (W.Size = @p__linq__0) 
        OR 
        (W.Size IS NULL AND @p__linq__0 IS NULL)
    )
    AND
    (
        (W.Colour = @p__linq__1) 
        OR 
        (W.Colour IS NULL AND @p__linq__1 IS NULL)
    );

... план выполнения будет простым пересечением индексов, используя один предикат равенства NULL -aware для каждого поиска:

Index intersection plan

Этот же эффективный план генерируется, если порядок основных предикатов в выражении WHERE отменяется.

В этом случае проблему можно избежать, объявив столбцы в базе данных и/или EF-коде как NOT NULL (при условии, что данные фактически не содержат NULLs), но это не меняет того факта, что простой нулевое сравнение столбцов должно быть обработано лучше.

Если бы команды EF и SQL Server работали вместе над этим, либо EF генерировал бы код в более удобной для оптимизатора форме, либо специальная поддержка этого синтаксиса была бы добавлена ​​в оптимизатор SQL Server. Обратите внимание, что всегда будет больше способов написать одно и то же логическое требование в SQL, чем оптимизатор базы данных общего назначения может (или должен) ожидать. Во-первых, просто невозможно предугадать любую возможную конструкцию, а для другого вам не понравится время компиляции плана, если это произойдет.

Существует последняя функция SQL Server, которую я должен упомянуть, потому что это позор, что генераторы кода обычно не поддерживают его. Если сгенерированный запрос отмечен для перекомпиляции при каждом выполнении с помощью подсказки запроса OPTION (RECOMPILE), оптимизатор запросов может использовать Оптимизацию встраивания параметров для создания единовременного плана запроса для конкретных значений параметров.

Это мощная функция, позволяющая полностью удалить или переписать лишние части запроса перед оптимизацией. Например, просто добавив подсказку запроса к исходному сгенерированному SQL:

SELECT 
[Extent1].[WidgetId] AS [WidgetId], 
[Extent1].[Size] AS [Size], 
[Extent1].[Colour] AS [Colour]
FROM [dbo].[Widget] AS [Extent1]
WHERE ((([Extent1].[Size] = @p__linq__0) 
AND ( NOT ([Extent1].[Size] IS NULL 
OR @p__linq__0 IS NULL))) 
OR (([Extent1].[Size] IS NULL) 
AND (@p__linq__0 IS NULL))) 
AND ((([Extent1].[Colour] = @p__linq__1) 
AND ( NOT ([Extent1].[Colour] IS NULL 
OR @p__linq__1 IS NULL))) 
OR (([Extent1].[Colour] IS NULL) 
AND (@p__linq__1 IS NULL)))
OPTION (RECOMPILE);

... создает оптимальный план, соответствующий конкретным значениям параметров:

Recompiled plan

Существует плата за составление плана при каждом исполнении, но улучшенное качество плана часто возмещает эту стоимость во много раз. Если хороший план чувствительно зависит от переданных значений параметров, это может быть очень эффективным методом.

Ответ 3

Примечание. Я столкнулся с этим вопросом задолго до того, как другие уже предоставили в целом правильные ответы. Я решил опубликовать это как отдельный ответ только потому, что думаю, что обходной путь может быть полезным, и потому, что вы можете оценить, как лучше понять, почему EF ведет себя таким образом.

Краткий ответ:. Лучшим обходным решением для этой проблемы является установка этого флага в экземпляр DbContext:

context.Configuration.UseDatabaseNullSemantics = true;

Когда вы сделаете это, все лишние проверки нулей исчезнут, и ваши запросы будут выполняться быстрее, если это повлияет на эту проблему.

Длинный ответ: другие в этом потоке правы, что в EF6 мы внедрили лишние условия проверки нуля по умолчанию, чтобы компенсировать различия между семантикой нулевых сравнений в базе данных (трехзначная логика) и стандартное сопоставление нулевых значений в памяти. Целью этого является удовлетворение следующего очень популярного запроса:

Неправильная обработка нулевых переменных в разделе "где"

Пол Уайт также прав, что в следующем выражении часть "И НЕ" менее распространена для компенсации трехзначной логики:

((x = y) AND NOT (x IS NULL OR y IS NULL)) OR (x IS NULL AND y IS NULL)

Это дополнительное условие необходимо в общем случае, чтобы исключить, что результат из всего выражения равен NULL, например. предположим, что x = 1 и y = NULL. Тогда

(x = y) --> NULL 
(x IS NULL AND y IS NULL) --> false
NULL OR false --> NULL

Различие между NULL и false важно в том случае, если выражение сравнения отрицается в более поздней точке в составе выражения запроса, например:

NOT (false) --> true 
NOT (NULL) --> NULL

Также верно, что мы могли бы добавить smarts в EF, чтобы выяснить, когда этот дополнительный термин не нужен (например, если мы знаем, что выражение не отрицается в предикате запроса) и оптимизировать его из запрос.

Кстати, мы отслеживаем эту проблему в следующей ошибке EF в codeplex:

[Производительность] Уменьшите дерево выражений для сложных запросов в случае семантики сравнения С# null

Ответ 4

Linq-to-SQL будет генерировать эквивалентный SQL-запрос для вашего кода Linq. Это означает, что он будет фильтроваться в том же порядке, который вы указали. На самом деле у него нет способа узнать, что будет быстрее, если вы не протестируете его.

В любом случае ваша первая фильтрация будет работать на весь набор данных и поэтому будет медленной. Однако...

  • Если вы сначала фильтруете редкое состояние, тогда он может сократить всю таблицу до небольшого набора результатов. Тогда ваша вторая фильтрация имеет только небольшой набор для работы, что не займет много времени.
  • Если вы сначала фильтруете общее условие, тогда набор данных, оставшихся после этого, по-прежнему довольно большой. Следовательно, вторая фильтрация работает с большим набором данных и поэтому занимает немного больше времени.

Итак, редкий первый означает медленный + быстрый, в то время как обычный сначала означает медленный + медленный. Единственный способ для Linq-to-SQL оптимизировать это различие для вас - сначала сделать запрос, чтобы проверить, какой из двух условий более редки, но это означает, что сгенерированный SQL будет либо отличаться каждый раз, когда вы его запускаете (и поэтому его нельзя было кэшировать, чтобы ускорить его) или будет значительно сложнее, чем то, что вы написали в Linq (чего не хотели дизайнеры Linq-SQL, вероятно, потому, что он мог сделать отладку кошмаром для пользователя).

Там вам ничего не мешает вам сделать эту оптимизацию; добавьте запрос заранее, чтобы подсчитать и посмотреть, какой из двух фильтров будет создавать меньший результирующий набор для работы второго фильтра. Для небольших баз данных это будет медленнее почти в каждом случае, потому что вы делаете полный дополнительный запрос, но если ваша база данных достаточно большая, и ваш запрос проверки является умным, он может оказаться быстрее в среднем. Кроме того, возможно, будет возможно определить, сколько из них должно быть выполнено условие А, чтобы оно было быстрее, независимо от того, сколько у вас объектов состояния B, а затем просто подсчитайте условие A, которое поможет быстрее выполнить запрос проверки.

Ответ 5

При настройке SQL-запросов, конечно, важно, в каком порядке вы фильтруете свои результаты. Почему вы ожидаете, что Linq-to-SQL никогда не пострадает от порядка фильтрации?