EF Core实现高效Linq TakeDistinct方法获取前n个不同字段对应数据
优化方案1:用JOIN替代IN查询(推荐,兼容性最好性能最高)
你原来的Contains写法会被翻译成IN子查询,数据库大概率会对每一行外层查询重复执行子查询,数据量大的时候性能极差。改用JOIN写法可以让子查询仅执行一次,EF Core 3.1+都支持这种写法的SQL转换:
public static IQueryable<TSource> TakeDistinct<TSource, TKey>(this IQueryable<TSource> source, int takeCount, Expression<Func<TSource, TKey>> keySelector) { var topDistinctKeys = source.Select(keySelector).Distinct().Take(takeCount); return source.Join(topDistinctKeys, keySelector, key => key, (entity, _) => entity); }
生成的SQL示例(以SQL Server为例):
SELECT t.* FROM YourTable t INNER JOIN ( SELECT DISTINCT TOP(@takeCount) Job FROM YourTable ) d ON t.Job = d.Job
只要你对Key字段(示例中的Job)建了索引,这个语句的执行效率会非常高,子查询仅需要扫描索引就能拿到前N个不重复Key,无需全表扫描。
优化方案2:窗口函数实现(EF Core 8+适用,灵活性高)
如果你使用EF Core 8及以上版本,可以用DENSE_RANK窗口函数实现,适合需要自定义Key排序规则的场景(比如要按最新生成的Job取前N个,而非按Job值排序):
public static IQueryable<TSource> TakeDistinct<TSource, TKey>(this IQueryable<TSource> source, int takeCount, Expression<Func<TSource, TKey>> keySelector) { return source .Select(e => new { Entity = e, KeyRank = EF.Functions.DenseRank().Over(orderBy: keySelector) }) .Where(x => x.KeyRank <= takeCount) .Select(x => x.Entity); }
生成的SQL示例:
SELECT * FROM ( SELECT *, DENSE_RANK() OVER (ORDER BY Job) AS rk FROM YourTable ) t WHERE rk <= @takeCount
性能优化建议
- 必须给作为分组依据的Key字段添加索引,两种方案都可以直接走索引查询,性能提升幅度可达几十上百倍
- 无需自定义排序规则时优先选JOIN方案,执行计划更简单,大数据量下性能优于窗口函数方案
- 当你需要自定义Key的排序逻辑(比如按Job的创建时间倒序取前N个),只需要修改窗口函数的
orderBy参数即可,灵活性更高
内容的提问来源于stack exchange,提问作者kofifus
相关产品推荐
相关产品推荐

