LINQ to Entities相当于sql“TOP(n)WITH TIES”

我最近在sql server中一直在寻找LINQ等同于WITH TIES ,我遇到了一些事情,这些事情无法发挥作用。

我知道这个问题之前被问过并且有一个已接受的答案,但它并不像领带那样有效 。 使用GroupBy()的解决方案不会出现TOP(3) WITH TIES预期结果,考虑到由{3 2 2 1 1 0}组成的数据集,结果集将是{3 2 2 1 1} ,其应该是{3 2 2}

使用以下示例数据(取自此问题) :

 CREATE TABLE Person ( Id int primary key, Name nvarchar(50), Score float ) INSERT INTO Person VALUES (1, 'Tom',8.9) INSERT INTO Person VALUES (2, 'Jerry',8.9) INSERT INTO Person VALUES (3, 'Sharti',7) INSERT INTO Person VALUES (4, 'Mamuzi',9) INSERT INTO Person VALUES (5, 'Kamala',9) 

传统的OrderByDescending(p => p.Score).Take(3)将导致: MamuziKamalaTom Jerry )之一应该包括两者

我知道没有内置的等价物,我找到了实现它的方法。 我不知道这是否是最好的方式,并开放替代解决方案。

 var query = (from q in list.OrderByDescending(s => s.Score).Take(3).Select(s => s.Score).Distinct() from i in list where q == i.Score select i).ToList(); 

编辑:

@Zefnus

我不确定你想要它的顺序,但是要更改顺序,你可以在select iToList()之间放置一个OrderBy(s => s.Score

我没有办法检查我的linq子句会产生什么sql语句。 但我认为你的答案要好得多。 你的问题也非常好。 我从没想过在linq中有关系。 ;)

基本上它只需要从第一个列表中获得前3个分数,并将它们与整个列表进行比较,并且我只获取那些等于第一个列表的分数的分数。

不要在触及数据库的任何东西上使用IEnumerable

针对LinqToSql和LinqToEntities的解决方案不应该使用IEnumerable 。 您当前的自我答案将导致从数据库中选择每个人,然后使用LinqToObjects在内存中查询。

要创建一个转换为SQL并由数据库执行的解决方案,您必须使用IQueryable和Expressions 。

 public static class QueryableExtensions { public static IQueryable TopWithTies(this IQueryable source, Expression> topBy, int topCount) { if (source == null) throw new ArgumentNullException("source"); if (topBy == null) throw new ArgumentNullException("topBy"); if (topCount < 1) throw new ArgumentOutOfRangeException("topCount", string.Format("topCount must be greater than 0, was {0}", topCount)); var topValues = source.OrderBy(topBy) .Select(topBy) .Take(topCount); var queryableMaxMethod = typeof(Queryable).GetMethods() .Single(mi => mi.Name == "Max" && mi.GetParameters().Length == 1 && mi.IsGenericMethod) .MakeGenericMethod(typeof(TComparand)); var lessThanOrEqualToMaxTopValue = Expression.Lambda>( Expression.LessThanOrEqual( topBy.Body, Expression.Call( queryableMaxMethod, topValues.Expression)), new[] { topBy.Parameters.Single() }); var topNRowsWithTies = source.Where(lessThanOrEqualToMaxTopValue) .OrderBy(topBy); return topNRowsWithTies; } public static IQueryable TopWithTiesDescending(this IQueryable source, Expression> topBy, int topCount) { if (source == null) throw new ArgumentNullException("source"); if (topBy == null) throw new ArgumentNullException("topBy"); if (topCount < 1) throw new ArgumentOutOfRangeException("topCount", string.Format("topCount must be greater than 0, was {0}", topCount)); var topValues = source.OrderByDescending(topBy) .Select(topBy) .Take(topCount); var queryableMinMethod = typeof(Queryable).GetMethods() .Single(mi => mi.Name == "Min" && mi.GetParameters().Length == 1 && mi.IsGenericMethod) .MakeGenericMethod(typeof(TComparand)); var greaterThanOrEqualToMinTopValue = Expression.Lambda>( Expression.GreaterThanOrEqual( topBy.Body, Expression.Call(queryableMinMethod, topValues.Expression)), new[] { topBy.Parameters.Single() }); var topNRowsWithTies = source.Where(greaterThanOrEqualToMinTopValue) .OrderByDescending(topBy); return topNRowsWithTies; } } 

这将创建以下forms的查询:

 SELECT [t0].[Id], [t0].[Name], [t0].[Score] FROM [Person] AS [t0] WHERE [t0].[Score] >= (( SELECT MIN([t2].[Score]) FROM ( SELECT TOP (3) [t1].[Score] FROM [Person] AS [t1] ORDER BY [t1].[Score] DESC ) AS [t2] )) ORDER BY [t0].[Score] DESC 

该查询仅比基线查询差50%:

 SELECT TOP (3) WITH TIES [t0].[Id], [t0].[Name], [t0].[Score] FROM [Person] AS [t0] ORDER BY [t0].[Score] desc 

使用由原始5条记录和另外10000条记录组成的数据集,所有记录的得分均小于原始记录,这些记录或多或少都是即时的(小于20毫秒)。

IEnumerable方法花了整整2分钟


如果表达式构建和reflection看起来很可怕,那么使用连接可以实现同样的目的:

 public static IQueryable TopWithTiesDescendingJoin(this IQueryable source, Expression> topBy, int topCount) { if (source == null) throw new ArgumentNullException("source"); if (topBy == null) throw new ArgumentNullException("topBy"); if (topCount < 1) throw new ArgumentOutOfRangeException("topCount", string.Format("topCount must be greater than 0, was {0}", topCount)); var orderedByValue = source.OrderByDescending(topBy); var topNValues = orderedByValue.Select(topBy).Take(topCount).Distinct(); var topNRowsWithTies = topNValues.Join(source, value => value, topBy, (x, row) => row); return topNRowsWithTies.OrderByDescending(topBy); } 

使用以下查询作为结果(具有大致相同的性能):

 SELECT [t3].[Id], [t3].[Name], [t3].[Score] FROM ( SELECT DISTINCT [t1].[Score] FROM ( SELECT TOP (3) [t0].[Score] FROM [Person] AS [t0] ORDER BY [t0].[Score] DESC ) AS [t1] ) AS [t2] INNER JOIN [Person] AS [t3] ON [t2].[Score] = [t3].[Score] ORDER BY [t3].[Score] DESC 

另一个解决方案 – 可能不如 其他解决方案 那么高效 – 是获得TOP(3) 分数并获得TOP(3)包含得分值的行。

我们可以使用Contains()如下;

 orderedPerson = datamodel.People.OrderByDescending(p => p.Score); topPeopleList = ( from p in orderedPerson let topNPersonScores = orderedPerson.Take(n).Select(p => p.Score).Distinct() where topNPersonScores.Contains(p.Score) select p ).ToList(); 

这个实现有什么好处,它的扩展方法 TopWithTies()可以很容易地实现;

 public static IEnumerable TopWithTies(this IEnumerable enumerable, Func selector, int n) { IEnumerable orderedEnumerable = enumerable.OrderByDescending(selector); return ( from p in orderedEnumerable let topNValues = orderedEnumerable.Take(n).Select(selector).Distinct() where topNValues.Contains(selector(p)) select p ); } 

我想也许你可以这样做:

 OrderByDescending(p => p.Score).Skip(2).Take(1) 

计算此元素的出现次数,然后:

 OrderByDescending(p => p.Score).Take(2 + "The select with the number of occurrences for the third element") 

我认为这可能有效;)这只是一个想法!

我找到了一个解决方案,使用.Skip(n-1).Take(1)N行的Score字段值(本例中为第3行.Skip(n-1).Take(1)选择.Skip(n-1).Take(1)并选择得分值大于或等于所有的行,如下所示:

 qryPeopleOrderedByScore = datamodel.People.OrderByDescending(p => p.Score); topPeopleList = ( from p in qryPeopleOrderedByScore let lastPersonInList = qryPeopleOrderedByScore.Skip(2).Take(1).FirstOrDefault() where lastPersonInList == null || p.Score >= lastPersonInList.Score select p ).ToList();