Count или Skip (1).Any(), где я хочу узнать, есть ли более 1 записи - Entity Framework

Я не уверен, когда, но я прочитал статью об этом, что указывает на то, что использование Skip(1).Any() лучше, чем Count() сострадание при использовании Entity Framework (возможно, я ошибаюсь). Я не уверен в этом, увидев сгенерированный код T-SQL.

Вот первый вариант:

int userConnectionCount = _dbContext.HubConnections.Count(conn => conn.UserId == user.Id);
bool isAtSingleConnection = (userConnectionCount == 1);

Это генерирует следующий код T-SQL, который является разумным:

SELECT 
[GroupBy1].[A1] AS [C1]
FROM ( SELECT 
  COUNT(1) AS [A1]
    FROM [dbo].[HubConnections] AS [Extent1]
    WHERE [Extent1].[UserId] = @p__linq__0
)  AS [GroupBy1]

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

bool isAtSingleConnection = !_dbContext
    .HubConnections.OrderBy(conn => conn.Id)
    .Skip(1).Any(conn => conn.UserId == user.Id);

Вот сформированный T-SQL для вышеуказанного запроса LINQ:

SELECT 
CASE WHEN ( EXISTS (SELECT 
    1 AS [C1]
    FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[UserId] AS [UserId]
        FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[UserId] AS [UserId], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
            FROM [dbo].[HubConnections] AS [Extent1]
        )  AS [Extent1]
        WHERE [Extent1].[row_number] > 1
    )  AS [Skip1]
    WHERE [Skip1].[UserId] = @p__linq__0
)) THEN cast(1 as bit) WHEN ( NOT EXISTS (SELECT 
    1 AS [C1]
    FROM ( SELECT [Extent2].[Id] AS [Id], [Extent2].[UserId] AS [UserId]
        FROM ( SELECT [Extent2].[Id] AS [Id], [Extent2].[UserId] AS [UserId], row_number() OVER (ORDER BY [Extent2].[Id] ASC) AS [row_number]
            FROM [dbo].[HubConnections] AS [Extent2]
        )  AS [Extent2]
        WHERE [Extent2].[row_number] > 1
    )  AS [Skip2]
    WHERE [Skip2].[UserId] = @p__linq__0
)) THEN cast(0 as bit) END AS [C1]
FROM  ( SELECT 1 AS X ) AS [SingleRowTable1];

Какой из них правильный? Существует ли большая разница в производительности между этими двумя?

Ответ 1

Производительность запроса зависит от многих вещей, таких как присутствующие индексы, фактических данных, от того, насколько устаревшие данные о данных присутствуют и т.д. Оптимизатор плана запросов SQL ищет эти разные показатели для создания эффективного запроса план. Таким образом, любой простой ответ, который говорит, что запрос 1 всегда лучше, чем запрос 2, или наоборот, было бы неверно.

Тем не менее, мой ответ ниже пытается объяснить позицию статей и как Skip(1).Any() может быть лучше (незначительно), чем делать Count() > 1. Второй запрос, хотя он имеет больший размер и в основном нечитаемый, выглядит так, что его можно было бы эффективно интерпретировать. Опять же, это зависит от вещей, упомянутых выше. Идея состоит в том, что количество строк, которые база данных должна анализировать для получения результата, больше в случае Count(). В случае count, предполагая, что требуемые индексы есть (кластерный индекс на Id, чтобы сделать OrderBy во втором случае эффективным), db должен пройти количество count count. Во втором случае он должен пройти максимум две строки, чтобы прийти к ответу.

Давайте научимся в нашем анализе и посмотрим, обладает ли моя теория выше всякой основы. Для этого я создаю фиктивную базу данных клиентов. Тип клиента выглядит следующим образом:

public class Customer
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

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

    for (int j = 0; j < 100; j++)
    {
        using (CustomersContext db = new CustomersContext())
        {
            Random r = new Random();
            for (int i = 0; i < 1000; i++)
            {
                Customer c = new Customer
                {
                    Name = Guid.NewGuid().ToString(),
                    Age = r.Next(0, 100)
                };
                db.Customers.Add(c);
            }
            db.SaveChanges();
        }
    }

Пример кода здесь.

Теперь запросы, которые я буду использовать, следующие:

db.Customers.Where(c => c.Age == 26).Count() > 1; // scenario 1

db.Customers.Where(c => c.Age == 26).OrderBy(c => c.ID).Skip(1).Any() // scenario 2

Я запустил SQL-профайлер, чтобы уловить планы запросов. Записанные планы выглядят следующим образом:

Сценарий 1:

Проверьте приведенную стоимость и фактическое количество строк для сценария 1 на приведенных выше изображениях. Scenario 1 - Estimated CostScenario 1 - Actual row count

Сценарий 2:

Проверьте примерную стоимость и фактическое количество строк для сценария 2 на приведенных ниже изображениях. Scenario 2 - Estimated CostScenario 2 - Actual row count

В соответствии с первоначальной догадкой оценочная стоимость и количество строк меньше в пропуске и в любом случае по сравнению с аргументом Count.

Вывод:

Весь этот анализ в стороне, как и многие другие, комментировали ранее, это не та оптимизация производительности, которую вы должны попытаться сделать в своем коде. Такие вещи, как эти, ухудшают читаемость с минимальным (я бы сказал, несуществующим) преимуществом. Я просто сделал этот анализ для удовольствия и никогда не использовал бы это как основу для выбора сценария 2. Я бы измерил и посмотрел, действительно ли делать Count(), чтобы изменить код для использования Skip().Any().

Ответ 2

Я прочитал статью об этом, которая указывает, что использование Skip(1).Any() лучше, чем Count().

Это утверждение верно для запроса LINQ to objects. В запросе LINQ to objects Skip(1).Any() нужно только попытаться получить первые два элемента последовательности, и он может игнорировать все элементы, которые появляются после него. Если последовательность включает довольно дорогостоящие операции (и надлежащим образом отменяет выполнение) или, что еще важнее, если последовательность бесконечна, это может быть большой проблемой. Для большинства запросов это будет иметь значение немного, но часто не много.

Для запроса LINQ, основанного на поставщике запросов, вряд ли это будет значительно отличаться. В частности, с EF, как вы видели, сгенерированный запрос не заметно отличается. Возможно ли, что это будет разница, конечно. Один случай может быть обработан лучше, чем другой поставщиком запроса, конкретные запросы могут быть лучше оптимизированы с конкретным рефактором, используемым тем или другим, и т.д.

Если кто-то предполагает, что существует существенное различие в EF-запросе между этими двумя, вероятность того, что они ошибочно применяют руководство, которое было разработано, чтобы просто применить к запросу LINQ to objects.

Ответ 3

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

Конечно, этот процесс может быть выполнен в субмиллисекундах в любом случае. Если у вас нет счетчика записей, превышающего 10 000 + записей, это действительно не имеет значения, если вам не нужно, чтобы он возвращался с определенным порогом. Не забывайте, что SQL Server будет кэшировать планы выполнения запросов. Если вы повторно запускаете тот же запрос, вы можете не увидеть разницу после запуска в первый раз, если данные не будут значительно изменены под ним.