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

时间:2014-03-03 20:53:25

标签: c# sql .net linq entity-framework

我最近在 sql server 中搜索LINQ相当于WITH TIES的内容,我遇到了一些事情,这些事情无法发挥作用。

我知道this question之前曾被问过并且已经接受了答案,但是它与 的关系不起作用。使用GroupBy()的解决方案不会像TOP(3) WITH TIES预期的结果一样,考虑到由{3 2 2 1 1 0}组成的数据集,结果集将为{3 2 2 1 1},其中应为{3 2 2}

使用以下示例数据(取自this question)

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)会产生: Mamuzi Kamala 其中一个 Tom (< em>或 Jerry )应该包括 BOTH

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

5 个答案:

答案 0 :(得分:4)

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

我不确定您想要它的顺序,但是要更改顺序,您可以在选择 ToList()之间放置一个OrderBy(s =&gt; s.Score)

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

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

答案 1 :(得分:2)

不要对涉及数据库的任何内容使用IEnumerable<T>

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

要制作一个转换为SQL并由数据库执行的解决方案,您必须使用IQueryable<T>Expressions

public static class QueryableExtensions
{
    public static IQueryable<T> TopWithTies<T, TComparand>(this IQueryable<T> source, Expression<Func<T, TComparand>> 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<Func<T, bool>>(
            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<T> TopWithTiesDescending<T, TComparand>(this IQueryable<T> source, Expression<Func<T, TComparand>> 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<Func<T, bool>>(
            Expression.GreaterThanOrEqual(
                topBy.Body,
                Expression.Call(queryableMinMethod,
                                topValues.Expression)),
            new[] { topBy.Parameters.Single() });

        var topNRowsWithTies = source.Where(greaterThanOrEqualToMinTopValue)
                                     .OrderByDescending(topBy);
        return topNRowsWithTies;
    }
}

这将创建以下形式的查询:

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

That query仅比baseline query

差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<T>方法整整 2分钟

如果表达式构建和反射看起来很可怕,可以通过连接实现同样的目的:

public static IQueryable<T> TopWithTiesDescendingJoin<T, TComparand>(this IQueryable<T> source, Expression<Func<T, TComparand>> 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);
}

结果使用以下query(性能大致相同):

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

答案 2 :(得分:1)

另一个解决方案 - 可能效率不如 other solution - 是获取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<T> TopWithTies<T, TResult>(this IEnumerable<T> enumerable, Func<T, TResult> selector, int n)
{
    IEnumerable<T> orderedEnumerable = enumerable.OrderByDescending(selector);

    return
    (
        from p in orderedEnumerable
        let topNValues = orderedEnumerable.Take(n).Select(selector).Distinct()
        where topNValues.Contains(selector(p))
        select p
    );
}

答案 3 :(得分:0)

我想也许你可以这样做:

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")

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

答案 4 :(得分:0)

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

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();