如何在ASP.NET Core服务器高效存储百万级查询结果供前端后续处理
我有一个ASP.NET Core Razor Pages应用,用JavaScript图表展示数据,部分场景下图表最多包含130万条数据点(对应数据库行)。根据图表选择的日期范围,我需要向服务器发起请求,计算该时间范围内的最小值、最大值、平均值等统计值。
现在的问题是,请求到服务器后如何最优获取数据?每次选新日期范围就查130万行显然不是最佳策略。我想到的方案有两个:一是从前端传递数据,二是在服务器端用MemoryCache缓存数据,请求时直接取用避免数据库查询,但不确定在MemoryCache中存130万条数据是不是最佳实践,甚至能不能行得通。
简言之:当前端发起请求时,怎么高效存储这130万行数据供后续处理?
以下是我用Dapper写的MySQL查询代码:
public async Task<ICollection<ReadingModel>> GetAllAsync(int sensorId, int locationId, DateTime? afterDateMT = null, DateTime? beforeDateMT = null) { try { using IDbConnection connection = new MySqlConnection(_config.GetConnectionString("Default")); var query = new StringBuilder(); var parameters = new DynamicParameters(); query.AppendLine(""" SELECT * FROM Reading R WHERE R.LocationId = @LocationId AND R.SensorId = @SensorId """); parameters.Add("LocationId", locationId); parameters.Add("SensorId", sensorId); if (afterDateMT != null) { query.AppendLine(""" AND R.RecordedAtMT > @StartDateMT """); parameters.Add("StartDateMT", afterDateMT.Value); } if (beforeDateMT != null) { query.AppendLine(""" AND R.RecordedAtMT < @EndDateMT """); parameters.Add("EndDateMT", beforeDateMT.Value); } query.AppendLine(""" ORDER BY R.RecordedAtMT ASC """); var result = await connection.QueryAsync<ReadingModel>(query.ToString(), parameters); return result.ToList(); } catch (Exception ex) { _logger.LogError(ex, "An error has occured while fetching ReadingModels with SensorId: {sensorId} and LocationId: {locationId} and StartDateMT: {afterDateMT}", sensorId, locationId, afterDateMT); throw; } }
1. 让数据库直接计算统计值(最优首选)
完全没必要把130万行数据拉到服务器再计算统计值,数据库做这类聚合计算比应用层高效得多。修改查询逻辑,直接让MySQL返回min、max、avg等结果,而非SELECT *:
public async Task<ReadingStatsModel> GetStatsAsync(int sensorId, int locationId, DateTime afterDateMT, DateTime beforeDateMT) { try { using IDbConnection connection = new MySqlConnection(_config.GetConnectionString("Default")); var query = """ SELECT MIN(R.Value) AS MinValue, MAX(R.Value) AS MaxValue, AVG(R.Value) AS AvgValue, COUNT(R.Id) AS TotalCount FROM Reading R WHERE R.LocationId = @LocationId AND R.SensorId = @SensorId AND R.RecordedAtMT > @StartDateMT AND R.RecordedAtMT < @EndDateMT """; var parameters = new DynamicParameters(); parameters.Add("LocationId", locationId); parameters.Add("SensorId", sensorId); parameters.Add("StartDateMT", afterDateMT); parameters.Add("EndDateMT", beforeDateMT); return await connection.QueryFirstOrDefaultAsync<ReadingStatsModel>(query, parameters); } catch (Exception ex) { _logger.LogError(ex, "获取统计数据失败,SensorId: {sensorId}, LocationId: {locationId}, 时间范围: {afterDateMT} 至 {beforeDateMT}", sensorId, locationId, afterDateMT, beforeDateMT); throw; } }
配套的ReadingStatsModel只需包含对应的统计字段即可,每次请求仅返回少量数值,性能最优。
2. 若必须缓存数据(针对频繁查询相同大范围的场景)
如果确实需要缓存130万条数据,MemoryCache可行,但需注意以下几点:
- 内存占用评估:先估算单条
ReadingModel的大小,比如每条占100字节,130万条约130MB,多数服务器内存可承受;但如果有多组传感器/位置数据,内存占用会成倍增长,需做好限制。 - 缓存策略:用
MemoryCacheEntryOptions设置滑动过期(多久未访问则清除)和绝对过期时间,同时设置缓存大小限制,避免内存溢出。 - 缓存键设计:以
sensorId + locationId作为缓存键,确保不同传感器数据不混叠。 - 增量更新:若数据会新增,不要每次全量缓存,可定时增量更新缓存中的新数据,或在数据写入时同步更新缓存。
示例缓存代码:
public async Task<ICollection<ReadingModel>> GetAllCachedAsync(int sensorId, int locationId) { var cacheKey = $"ReadingData_{sensorId}_{locationId}"; if (_memoryCache.TryGetValue(cacheKey, out ICollection<ReadingModel> cachedData)) { return cachedData; } // 从数据库拉取全量数据 var data = await GetAllAsync(sensorId, locationId); // 设置缓存选项:滑动过期1小时,绝对过期24小时,避免数据过旧 var cacheOptions = new MemoryCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromHours(1)) .SetAbsoluteExpiration(TimeSpan.FromDays(1)) .SetSize(data.Count); // 标记缓存大小,配合全局缓存限制 _memoryCache.Set(cacheKey, data, cacheOptions); return data; }
后续前端请求统计值时,直接从缓存数据中计算即可,无需再查询数据库。但仍需强调:能让数据库处理的聚合逻辑,不要放到应用层。
3. 前端传递数据的局限性
若把130万条数据传到前端,会导致首次加载极慢、消耗大量带宽,且前端计算统计值会占用浏览器资源导致页面卡顿,不推荐这种方案。
额外优化:数据库索引
无论采用哪种方案,都要给Reading表添加合适的复合索引,比如(LocationId, SensorId, RecordedAtMT),可大幅加快日期范围查询的速度,聚合查询也能受益。
内容的提问来源于stack exchange,提问作者kj49

