簡體   English   中英

LINQ to Entities相當於sql“TOP(n)WITH TIES”

[英]LINQ to Entities equivalent of sql “TOP(n) WITH TIES”

我最近一直在sql服務器中搜索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)將導致與:Mamuzi, 卡馬拉Tom( 傑里 )它應該包括一種 BOTH

我知道沒有內置的等價物,我找到了實現它的方法。 我不知道這是否是最好的方式,並開放替代解決方案。

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

該查詢僅比基線查詢差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);
}

使用以下查詢作為結果(具有大致相同的性能):

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

我想也許你可以這樣做:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM