You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 12:34:55