11

我不确定什么时候,但我读到了一篇关于此的文章,它表明在使用实体框架时使用Skip(1).Any()Count()同情更好(我可能记错了)。在看到生成的 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);

这是为上述 LINQ 查询生成的 T-SQL:

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];

哪一个是正确的方法?这两者之间有很大的性能差异吗?

4

3 回答 3

10

查询性能取决于很多因素,例如存在的索引、实际数据、关于存在的数据的统计信息的陈旧程度等。SQL 查询计划优化器会查看这些不同的指标以提出有效的查询计划。因此,任何说查询 1 总是比查询 2 好或相反的直接答案都是不正确的。

也就是说,我在下面的回答试图解释文章的立场以及如何Skip(1).Any()(稍微)比做Count() > 1. 第二个查询虽然尺寸更大并且几乎不可读,但看起来可以以一种有效的方式进行解释。同样,这取决于上述内容。这个想法是,在Count(). 在计数情况下,假设存在所需的索引(Id 上的聚集索引以使 OrderBy 在第二种情况下有效),数据库必须经过计数行数。在第二种情况下,它必须经过最多两行才能得出答案。

让我们的分析更加科学,看看我的上述理论是否站得住脚。为此,我正在创建一个虚拟客户数据库。客户类型如下所示,

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 - 估计成本 场景 1 - 实际行数

场景二:

在下图中查看方案 2 的估计成本和实际行数。 方案 2 - 估计成本 场景 2 - 实际行数

根据最初的猜测,与 Count 情况相比,Skip 和任何情况下的估计成本和行数都更少。

结论:

除了所有这些分析之外,正如许多其他人之前评论的那样,这些不是您应该尝试在代码中进行的性能优化。像这样的事情会以非常小的(我会说不存在)性能优势损害可读性。我只是为了好玩而进行此分析,并且永远不会将其用作选择方案 2 的基础。我会衡量并查看执行 aCount()是否真的会伤害更改要使用的代码Skip().Any()

于 2013-04-24T19:18:17.183 回答
6

我读过一篇关于此的文章,它表明 的用法Skip(1).Any()优于Count().

该语句在 LINQ to objects 查询上是完全正确的。在 LINQ to objects 查询上,Skip(1).Any()只需要尝试获取序列的前两项,它可以忽略它之后的所有项。如果序列涉及相当昂贵的操作(并适当地推迟执行),或者更重要的是,如果序列是无限的,这可能是一件大事。对于大多数查询,它会有点重要,但通常不是很多。

对于基于查询提供程序的 LINQ 查询,它不太可能有显着差异。正如您所见,特别是使用 EF,生成的查询并没有明显不同。有没有可能有区别,当然。查询提供者可以比另一种情况更好地处理一种情况,通过其中一种或另一种使用的特定重构可以更好地优化特定查询,等等。

如果有人暗示这两者之间的 EF 查询存在重大差异,则很可能他们错误地应用了旨在仅适用于 LINQ to objects 查询的指南。

于 2013-04-24T19:21:18.113 回答
0

这肯定取决于您在表/数据集中的记录数。如果您有很多记录,那么对身份进行计数会非常快,因为它已编入索引,但跳过一条记录然后获取下一条记录会更快。

当然,在任何一种情况下,这个过程都可以在亚毫秒内完成。除非您的记录数超过 10,000 条以上,否则除非您需要它在特定阈值以下返回,否则这真的无关紧要。不要忘记 SQL Server 将缓存查询执行计划。如果您重新运行相同的查询,则在第一次运行后您可能看不到差异,除非数据在其下方发生了相当大的变化。

于 2013-04-24T19:12:37.707 回答