我们有一个ASP.NET MVC Web应用程序,通过Entity Framework连接到SQL Server数据库。这个应用程序的主要任务之一是允许用户快速搜索并筛选一个包含存档值的巨大数据库表。
表结构非常简单: Timestamp (DateTime)、StationId (int)、DatapointId (int)、Value (double)。该表大约有1000万到1亿行。我使用了覆盖索引等方法来优化DB表,但是当根据 DatapointId、StationId、Time 进行筛选并跳过和获取我想在页面上显示的部分时,用户体验仍然相当滞后。
因此,我尝试了另一种方法:由于我们的服务器拥有很多 RAM,我认为我们可以在Web应用程序启动时将整个归档表加载到List中,然后直接从此List获取数据而不是进行往返数据库。这个方法效果很好,将整个存档表(当前大约有1000万条记录)加载到List中只需约9秒钟。ArchiveRow是一个简单的对象,看起来像这样:
相应地:
一些更多的细节:站点数量通常在5到50之间,很少超过50。数据点的数量小于7000。Web应用程序设置为64位,并在web.config中设置了
我期待着进一步的改进和建议。也许有一种完全不同的基于数组或类似的方法,可以在不使用linq的情况下表现得更好?
表结构非常简单: Timestamp (DateTime)、StationId (int)、DatapointId (int)、Value (double)。该表大约有1000万到1亿行。我使用了覆盖索引等方法来优化DB表,但是当根据 DatapointId、StationId、Time 进行筛选并跳过和获取我想在页面上显示的部分时,用户体验仍然相当滞后。
因此,我尝试了另一种方法:由于我们的服务器拥有很多 RAM,我认为我们可以在Web应用程序启动时将整个归档表加载到List中,然后直接从此List获取数据而不是进行往返数据库。这个方法效果很好,将整个存档表(当前大约有1000万条记录)加载到List中只需约9秒钟。ArchiveRow是一个简单的对象,看起来像这样:
public class ArchiveResponse {
public int Length { get; set; }
public int numShown { get; set; }
public int numFound { get; set; }
public int numTotal { get; set; }
public List<ArchiveRow> Rows { get; set; }
}
相应地:
public class ArchiveRow {
public int s { get; set; }
public int d { get; set; }
public DateTime t { get; set; }
public double v { get; set; }
}
当我现在试图使用Linq查询从列表中获取所需的数据时,查询速度已经比查询数据库快了,但是当按多个条件过滤时,它仍然相当慢。例如,当我按一个StationId和12个DatapointIds进行过滤时,检索25行窗口需要约5秒钟的时间。我已经从使用Where
过滤器切换到使用连接,但我认为还有改进的空间。是否有更好的方法实现这样的缓存机制,同时尽可能保持内存消耗低?是否有其他集合类型更适合这个目的?
因此,以下是过滤并从ArchiveCache列表中获取相关数据的代码:
// Total number of entries in archive cache
var numTotal = ArchiveCache.Count();
// Initial Linq query
ParallelQuery<ArchiveCacheValue> query = ArchiveCache.AsParallel();
// The request may contain StationIds that the user is interested in,
// so here's the filtering by StationIds with a join:
if (request.StationIds.Count > 0)
{
query = from a in ArchiveCache.AsParallel()
join b in request.StationIds.AsParallel()
on a.StationId equals b
select a;
}
// The request may contain DatapointIds that the user is interested in,
// so here's the filtering by DatapointIds with a join:
if (request.DatapointIds.Count > 0)
{
query = from a in query.AsParallel()
join b in request.DatapointIds.AsParallel()
on a.DataPointId equals b
select a;
}
// Number of matching entries after filtering and before windowing
int numFound = query.Count();
// Pagination: Select only the current window that needs to be shown on the page
var result = query.Skip(request.Start == 0 ? 0 : request.Start - 1).Take(request.Length);
// Number of entries on the current page that will be shown
int numShown = result.Count();
// Build a response object, serialize it to Json and return to client
// Note: The projection with the Rows is not a bottleneck, it is only done to
// shorten 'StationId' to 's' etc. At this point there are only 25 to 50 rows,
// so that is no problem and happens in way less than 1 ms
ArchiveResponse myResponse = new ArchiveResponse();
myResponse.Length = request.Length;
myResponse.numShown = numShown;
myResponse.numFound = numFound;
myResponse.numTotal = numTotal;
myResponse.Rows = result.Select(x => new archRow() { s = x.StationId, d = x.DataPointId, t = x.DateValue, v = x.Value }).ToList();
return JsonSerializer.ToJsonString(myResponse);
一些更多的细节:站点数量通常在5到50之间,很少超过50。数据点的数量小于7000。Web应用程序设置为64位,并在web.config中设置了
<gcAllowVeryLargeObjects enabled="true" />
。我期待着进一步的改进和建议。也许有一种完全不同的基于数组或类似的方法,可以在不使用linq的情况下表现得更好?
query.Count()
,每次都会进行完整迭代。挑战在于过滤参数的差异以及相同集合再次访问的可能性(用于下一页)。如果用户倾向于获取多个页面,请考虑返回批处理页面。 - Adam HouldsworthIQueryable
,尽量手动迭代一次。你可以使用像Parallel.ForEach
这样的工具来帮助实现这一点。 - Adam Houldsworth