【问题标题】:LINQ to Entities equivalent of sql "TOP(n) WITH TIES"LINQ to Entity 相当于 sql "TOP(n) WITH TIES"
【发布时间】:2014-04-05 02:49:44
【问题描述】:

我最近一直在 sql server 中搜索 LINQ 等价于 WITH TIES,我遇到了一些无法证明有用的东西。

我知道this question 之前被问过并且得到了接受的答案,但它与 tiedoes 的方式不同。考虑到由{3 2 2 1 1 0} 组成的数据集,使用GroupBy() 的解决方案不会像TOP(3) WITH TIES 的预期结果,结果集将是{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) 将产生:MamuziKamala其中一个 Tom Jerry) 其中应该包含BOTH

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

【问题讨论】:

  • 我认为你需要一个Where() 方法; Take() 方法是不确定的。
  • @JeremyHolovacs 我想没有办法只使用Take(),但是如何使用Where()
  • 其中一个链接不正确,刚刚更正了。
  • 好吧,我认为您可能需要获得前 3 名分数的不同列表,然后获得分数等于或大于前 3 名中最低分数的 Person 对象列表分数。它有点丑陋,SQL 方面,但我还没有看到一个 ORM 处理窗口函数具有任何程度的便利。
  • @JeremyHolovacs 我想 Pumpkin 的回答受到了您的评论的启发。我觉得它没有得到那个,至少没有我预期的那么多(:

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


【解决方案1】:
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() s.Score) /em>

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

基本上它只取第一个列表中的前 3 个分数并将它们与整个列表进行比较,并且 i 只取那些与第一个列表的分数相等的分数。

【讨论】:

  • 嘿南瓜,欢迎来到 SO。很好的答案,尽管结果是相反的,但效果很好。你能分享一些描述吗?
  • +1 表示附加值。谢谢南瓜,你的回答激发了我的新想法(检查我的新解决方案 - 2nd answer),以及 extension method 的实现
【解决方案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&lt;T&gt; 方法花了整整 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

【讨论】:

  • 感谢您的详细回答,我已经说过它效率不高,但我同意避免IEnumerable 并使用IQueryableExpression 是更好的方法.
【解决方案3】:

另一个解决方案 - 可能不如 other solution 高效 - 是获取TOP(3) Scores 并获取具有 Score 的行 em> 值包含在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
    );
}

【讨论】:

    【解决方案4】:

    我认为也许你可以这样做:

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

    我认为这也许可行 ;) 这只是一个想法!

    【讨论】:

    • + 1 对于.Skip(n - 1).Take(1) 的想法,以便获得最后一行奥斯卡,但您的答案不完整,这可能是有人投反对票的原因。
    • 是的……也许吧! ;) 但是感谢您的 +1!这就是为什么我把这只是一个想法放在最后! ;) 恭喜泽夫努斯!
    【解决方案5】:

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

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

    【讨论】:

    • 和我说的差不多 ;) 我很高兴你找到了解决方案!恭喜!
    • @OscarBralo 谢谢。使用 SkipTake 取最后一行的想法很好,但其余部分是通过使用 where 过滤得分值完成的
    猜你喜欢
    • 2012-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-16
    • 2017-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多