EF Core中Group By后获取分组键及Decimal列表并计算中位数
EF Core分组后计算中位数实现方案
需求场景
现有EF Core分组代码可按城市名称(含阿拉伯语名称)分组,获取分组键及AmountSqft的Decimal列表,需保留分组键和该列表的同时,计算列表的中位数(替代Linq内置的Average方法)。
原代码
var response = await _db.Transaction .Where(expression) .OrderBy(x => x.AmountSqft) .GroupBy(x => new { CityName = x.City.Name, CityNameAr = x.City.NameAr }) .Select(x => new SalesTrendDTO { Label = x.Key.CityName, LabelAr = x.Key.CityNameAr, ListAvgPricePerSqft = x.Select(z => z.AmountSqft).ToList() }).ToListAsync();
实现方案
由于EF Core无法将自定义中位数逻辑转换为SQL语句,通用做法是先将分组后的列表数据拉取到内存,再在内存中计算中位数,具体步骤如下:
1. 调整数据查询逻辑
先从数据库获取分组后的基础数据(分组键+数值列表),再转换为目标DTO并计算中位数:
// 从数据库拉取分组后的基础数据 var rawData = await _db.Transaction .Where(expression) .GroupBy(x => new { x.City.Name, x.City.NameAr }) .Select(x => new { CityName = x.Key.Name, CityNameAr = x.Key.NameAr, AmountSqftList = x.Select(z => z.AmountSqft).ToList() }) .ToListAsync(); // 转换为带中位数的目标DTO var result = rawData.Select(item => new SalesTrendDTO { Label = item.CityName, LabelAr = item.CityNameAr, ListAvgPricePerSqft = item.AmountSqftList, MedianPricePerSqft = CalculateMedian(item.AmountSqftList) }).ToList();
2. 实现中位数计算方法
添加一个通用的中位数计算函数,处理奇偶长度的列表:
private decimal CalculateMedian(List<decimal> values) { if (values == null || values.Count == 0) throw new ArgumentException("数值列表不能为空"); // 先对列表排序 var sortedValues = values.OrderBy(v => v).ToList(); int count = sortedValues.Count; if (count % 2 == 1) { // 奇数个元素,取中间位置的数值 return sortedValues[count / 2]; } else { // 偶数个元素,取中间两个数值的平均值 var middleLeft = sortedValues[(count / 2) - 1]; var middleRight = sortedValues[count / 2]; return (middleLeft + middleRight) / 2; } }
大数据量场景优化(可选)
如果数据量极大,内存计算存在性能瓶颈,可针对特定数据库使用原生中位数函数(如SQL Server的PERCENTILE_CONT、PostgreSQL的PERCENTILE_CONT),通过原生SQL查询实现,但会失去跨数据库兼容性。示例(SQL Server):
// 需提前在SalesTrendDTO中添加MedianPricePerSqft和ListAvgPricePerSqft字段 var sqlQuery = @" SELECT c.Name AS Label, c.NameAr AS LabelAr, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY t.AmountSqft) OVER (PARTITION BY c.Name, c.NameAr) AS MedianPricePerSqft, STRING_AGG(t.AmountSqft, ',') AS AmountSqftString FROM Transactions t JOIN Cities c ON t.CityId = c.Id WHERE {your_conditions_here} GROUP BY c.Name, c.NameAr"; var rawResult = await _db.SalesTrendDTO.FromSqlRaw(sqlQuery, /* 传入查询参数 */).ToListAsync(); // 将字符串格式的列表转换为List<decimal> var finalResult = rawResult.Select(item => { item.ListAvgPricePerSqft = item.AmountSqftString.Split(',') .Select(decimal.Parse) .ToList(); return item; }).ToList();
内容的提问来源于stack exchange,提问作者Khalil Saqib
相关产品推荐
相关产品推荐

