为什么添加两个 .OrderBy(或 .OrderByDescending)语句会以相反的顺序应用排序?

Why does adding two .OrderBy (or .OrderByDescending) statements apply the sorts in reverse order?

我在今天重构的一些代码中遇到了以下问题。

context.Entities.Where(x => x.ForeignKeyId == id)
    .OrderBy(x => x.FirstSortField)
    .OrderBy(x => x.SecondSortField);

最初,我取出 .OrderBy(x => x.FirstSortField) 以为第一个 OrderBy 语句将被第二个 OrderBy 替换。经过测试,我意识到它正在生成 SQL 作为 ORDER BY SecondSortField, FirstSortField.

因此,实际上等价于:

context.Entities.Where(x => x.ForeignKeyId == id)
    .OrderBy(x => x.SecondSortField)
    .ThenBy(x => x.FirstSortField);

谁能解释一下 EF6 这样做的原因?在我看来,将第一个排序字段替换为第二个排序字段会更直观。

其实很简单,也很有道理:

查询的第一部分

context.Entities.Where(x => x.ForeignKeyId == id)

会被翻译成 SQL 或多或少像这样

select * from Entities

添加第一个订单

.OrderBy(x => x.FirstSortField)

它会被翻译成这样

select * from (
   select * from Entities
)
order by FirstSortField

然后通过

添加第二个订单
.OrderBy(x => x.FirstSortField)
.OrderBy(x => x.SecondSortField)

将被翻译成:

select * from (
   select * from (
      select * from Entities
   )
   order by FirstSortField
)
order by SecondSortField

Entity framework 足够聪明,可以简化为

select * from Entities
order by SecondSortField, FirstSortField

这都是关于本地数据,但 EF 希望在构建表达式树和编写查询时执行逻辑等效。

你应该研究一下 stable sorting 的概念。当您使用稳定的排序算法时,相同项目的原始顺序将被保留。

所以假设您有这样的数据,其中明显的第一个 name/last 名称字段:

Brad Jones
Tom Smith
Sam Jones
Jim Doe
James Smith
Ryan Smith

如果您最初只按名字订购,您会得到:

Brad Jones
James Smith
Jim Doe
Ryan Smith
Sam Jones
Tom Smith

如果您现在使用此排序列表,并再次按姓氏排序,您会得到按两个字段排序的结果,其中后面的排序优先于前面的排序...但是你 只有在排序稳定的情况下才能保证准确的顺序:

Jim Doe
Brad Jones
Sam Jones
James Smith
Ryan Smith
Tom Smith

这给我们带来了一个问题,.Net 使用什么算法,它是否稳定。 To the documentation we go,我们在备注部分找到了这个:

This method performs a stable sort

具体算法这里没有记载。我相信它是 Quicksort,但将其排除在文档之外可能是有意为之,以允许维护人员在发现更好的东西时更新满足稳定性要求的最佳可用选项。

但是,这又是针对本地数据的。数据库将执行 SQL 告诉它们的操作。

我只能得出结论,我们实际上是在查看 LINQ-to-SQL。在第 5 版之前的 Linqpad 中,很容易犯这个错误,因为在创建新连接时很容易忽略 EF6 DbContext 驱动程序的选择。 (在Linqpad v6中这个选择更加显眼)。

我已经在 EF6、EF-core 3 和 5 以及 LINQ-to-SQL 中测试了报告的行为。 只有 在后者中我看到生成的 SQL 语句在 ORDER BY.

中有两列

声明...

Products.OrderBy(p => p.Description).OrderBy(p => p.LastSale)

...被 LINQ-to-SQL 翻译为:

SELECT [t0].[ID], [t0].[Description], [t0].[Discontinued], [t0].[LastSale]
FROM [Product] AS [t0]
ORDER BY [t0].[LastSale], [t0].[Description]

推理在 中进行了解释,归结为:LastSale 是主要的排序字段,因为它在某种程度上覆盖了第一个 OrderBy

所有 EF 查询只有 ORDER BY LastSale.

我必须说同意 EF 的实现。正如 解释的那样,两次连续排序的结果取决于排序算法。这意味着我们可以肯定地说的是 any LINQ 查询的结果将按 LastSale 排序并且 LastSale 组内的排序不确定.然后,IMO,SQL 翻译处理第二个 OrderBy 语句作为第一个语句的完全覆盖是一个更好的选择,因此很明显没有期望可以基于第一个语句。对我来说,它更直观。

信息是:按多个字段排序时要明确。使用 OrderBy - ThenBy。不要依赖数据库提供商对连续 OrderBy 语句的处理。