EF高效取数(OrderBy+Distinct):优化10万条数据去重查询性能
嘿,这个问题我太熟悉了!你的代码现在跑20秒的核心原因很明确:OrderByDescending(u => u.Time).DistinctBy(u => u.ID) 是先把所有10万条数据全加载到内存里,再在本地做排序和去重操作——这就相当于把整个数据库表扛到程序里处理,IO和内存开销拉满,速度自然快不起来。
咱们直接上优化方案,核心思路就是:把计算逻辑推到数据库层面完成,只让数据库返回你真正需要的那部分数据,别让程序做无用功。
方案一:用EF Core窗口函数(推荐,性能最优)
从EF Core 3.0开始,已经支持直接用LINQ生成窗口函数SQL了,这是处理这类“分组取Top1”场景的最优解。我们可以用ROW_NUMBER()窗口函数,按ID分组,每个组内按Time降序排,然后只取行号为1的记录(也就是每个ID的最新登录记录)。
代码示例:
var usersFromDb = _Context.Users .Select(u => new { u.ID, u.Name, u.Client, u.Time, // 按ID分组,组内按Time倒序排,生成行号 RowNum = EF.Functions.RowNumber().Over( partitionBy: u.ID, orderBy: u.Time descending) }) .Where(x => x.RowNum == 1) // 只取每个组的第一条(最新的) .Select(x => new User { ID = x.ID, Name = x.Name, Client = x.Client, Time = x.Time }) .ToList();
这个方案的优势是:数据库会直接在服务器端完成分组、排序、筛选,最后只返回每个ID的最新记录,数据传输量直接从10万条降到你实际需要的唯一ID数量,速度会有质的提升。
方案二:分组取最大时间再关联(兼容旧版EF Core)
如果你的EF Core版本低于3.0,不支持窗口函数,可以用“分组取最大时间+表关联”的方式:先分组得到每个ID对应的最新登录时间,再通过ID和Time关联原表,拿到完整的用户数据。
代码示例:
// 先获取每个ID的最新登录时间 var latestTimePerUser = _Context.Users .GroupBy(u => u.ID) .Select(g => new { UserId = g.Key, LatestLoginTime = g.Max(u => u.Time) }); // 关联原表,拿到对应记录 var usersFromDb = _Context.Users .Join(latestTimePerUser, user => new { user.ID, user.Time }, latest => new { latest.UserId, latest.LatestLoginTime }, (user, latest) => user) .ToList();
这个方案同样是在数据库层面完成计算,避免了内存处理全表数据,只是性能比窗口函数略逊一点,但胜在兼容性好。
关键补充:加个复合索引!
不管用上面哪个方案,都一定要给Users表加一个覆盖复合索引,这能让数据库查询时完全不用扫描全表,直接从索引里取数据,性能再上一个台阶:
CREATE NONCLUSTERED INDEX IX_Users_ID_Time ON Users (ID, Time DESC) INCLUDE (Name, Client);
这个索引包含了查询需要的所有字段(ID、Time用来排序分组,Name、Client用来直接返回结果),数据库可以直接从索引里获取所有需要的数据,不用回表查询原数据,速度会快很多。
按照上面的方案优化后,10万条数据的查询应该能秒级返回,再也不用等20秒啦!
内容的提问来源于stack exchange,提问作者The Moon

