将“SELECT TOP (1) WITH TIES”转换为 EF Core

Converting `SELECT TOP (1) WITH TIES` to EF Core

T-SQL 查询版本

让我们用一些数据设置一个简单的 table:

DROP TABLE IF EXISTS #OrdersTable

CREATE TABLE #OrdersTable
(
    Id int,
    Custid int
);

INSERT INTO #OrdersTable (Id, Custid) VALUES (1, 71);
INSERT INTO #OrdersTable (Id, Custid) VALUES (2, 71);
INSERT INTO #OrdersTable (Id, Custid) VALUES (3, 71);
INSERT INTO #OrdersTable (Id, Custid) VALUES (4, 72);
INSERT INTO #OrdersTable (Id, Custid) VALUES (5, 72);
INSERT INTO #OrdersTable (Id, Custid) VALUES (6, 72);
INSERT INTO #OrdersTable (Id, Custid) VALUES (7, 73);
INSERT INTO #OrdersTable (Id, Custid) VALUES (8, 74);
INSERT INTO #OrdersTable (Id, Custid) VALUES (9, 74);

在这种情况下,客户 71 和 72 各有 3 个订单。客户 73 有 1 个订单。客户 74 有 2 个订单。

假设我们想知道订单数量最多的客户。

以下查询:

SELECT TOP (1) WITH TIES Custid
FROM #OrdersTable
GROUP BY Custid
ORDER BY COUNT(*) DESC;

结果如下:

Custid
-----------
71
72

(2 rows affected)

直接转换为 EF Core

鉴于以下 class:

public class Order
{
    public int Id { get; set; }
    public int Custid { get; set; }
}

和以下数据:

var OrdersTable = new List<Order>()
{
    new Order(){ Id = 1, Custid = 71},
    new Order(){ Id = 2, Custid = 71},
    new Order(){ Id = 3, Custid = 71},
    new Order(){ Id = 4, Custid = 72},
    new Order(){ Id = 5, Custid = 72},
    new Order(){ Id = 6, Custid = 72},
    new Order(){ Id = 7, Custid = 73},
    new Order(){ Id = 8, Custid = 74},
    new Order(){ Id = 9, Custid = 74},
};

这是查询到 EF Core 的简单转换:

var n = OrdersTable.GroupBy(order => order.Custid).Select(grouping => grouping.Count()).Max();

var custids = OrdersTable.GroupBy(order => order.Custid).Where(grouping => grouping.Count() == n).Select(grouping => grouping.Key);

使用以下显示数据:

foreach (var custid in custids)
    Console.WriteLine(custid);

我们得到:

71
72

问题

让我们将 T-SQL 和 EF Core 版本并排放置。 T-SQL:

SELECT TOP (1) WITH TIES Custid
FROM #OrdersTable
GROUP BY Custid
ORDER BY COUNT(*) DESC;

EF 核心:

var n = OrdersTable.GroupBy(order => order.Custid).Select(grouping => grouping.Count()).Max();

var custids = OrdersTable.GroupBy(order => order.Custid).Where(grouping => grouping.Count() == n).Select(grouping => grouping.Key);

我的问题是,是否有更有效的方法在 EF Core 中实现此查询?

完整节目

演示上述查询的完整 C# 控制台程序:

using System;
using System.Collections.Generic;
using System.Linq;

namespace EfCoreTop1Ties
{
    public class Order
    {
        public int Id { get; set; }
        public int Custid { get; set; }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            var OrdersTable = new List<Order>()
            {
                new Order(){ Id = 1, Custid = 71},
                new Order(){ Id = 2, Custid = 71},
                new Order(){ Id = 3, Custid = 71},
                new Order(){ Id = 4, Custid = 72},
                new Order(){ Id = 5, Custid = 72},
                new Order(){ Id = 6, Custid = 72},
                new Order(){ Id = 7, Custid = 73},
                new Order(){ Id = 8, Custid = 74},
                new Order(){ Id = 9, Custid = 74},
            };

            var n = OrdersTable.GroupBy(order => order.Custid).Select(grouping => grouping.Count()).Max();

            var custids = OrdersTable.GroupBy(order => order.Custid).Where(grouping => grouping.Count() == n).Select(grouping => grouping.Key);

            foreach (var custid in custids)
                Console.WriteLine(custid);
        }
    }
}

您应该可以通过一个查询来做到这一点:

var n = OrdersTable
    .GroupBy(order => order.Custid)
    .Select(grouping => new { CustomerId = grouping.Key, OrderCount = grouping.Count() })
    .OrderByDescending(g => g.OrderCount)
    .ToList();

var maxCount = n.First().OrderCount;
var custIds = n.Where(g => g.OrderCount == maxCount)
    .Select(g => g.CustomerId)
    .ToList();

在 OrdersTable 是 DbContext DBSet 的情况下,这将导致对数据库进行 1 次查询。 returning 项与该计数匹配的检查是在内存中根据结果完成的,无需 return 行程。

如果您正在处理一个特别大的数据集,您可以考虑一些合理的假设,例如,如果有成千上万的客户,那么 100 或 1000 可能会“绑定”最大数量的订单。

var n = OrdersTable
    .GroupBy(order => order.Custid)
    .Select(grouping => new { CustomerId = grouping.Key, OrderCount = grouping.Count() })
    .OrderByDescending(g => g.OrderCount)
    .Take(100)
    .ToList();

if (n.All(g => g.OrderCount == n.First().OrderCount)
   // Redo query with larger threshold.

如果所有 returned 行恰好具有相同的订单计数,它将再次 运行 查询。您可能希望以不同的方式处理这种情况,例如当所有客户的订单均为 0 时。 (如果在这种情况下您有选择要比较的客户的标准,例如 city/state 等)

要涵盖非常大的数据 table,另一种选择是查询最大计数,前提是您的实体配置了导航属性,以便您的客户实体可以与其订单集合相关联:

var maxOrderCount = dbContext.Customers
    .OrderByDescending(x => x.Orders.Count)
    .Select(x => x.Orders.Count)
    .First();

var customers = dbContext.Customers
    .Where(x => x.Orders.Count == maxOrderCount)
    .ToList();

如果您只需要客户 ID,请在 ToList() 之前添加 .Select(x => x.CustomerId)。它 运行 有两个查询,但它们相当简单,仅 return 所需的数据而不是潜在的所有客户 IDs/data。