우리는 Entity Framework를 통해 SQL Server DB에 연결되는 ASP.NET MVC 웹 응용 프로그램을 보유하고 있습니다. 이 응용 프로그램의 주요 작업 중 하나는 사용자가 아카이브 값을 보유하고있는 거대한 데이터베이스 테이블을 빠르게 검색하고 필터링 할 수 있도록 허용하는 것입니다.C#에서 거대한 목록을 필터링하는 가장 좋은 방법은 무엇입니까?
테이블 구조는 매우 간단합니다 : Timestamp (DateTime), StationId (int), DatapointId (int), Value (double). 이 테이블은 10 억에서 1 억 개의 행을 포함합니다. DB 테이블을 커버 인덱스 등으로 최적화했지만, DatapointId, StationId, Time, Skipping으로 필터링 할 때 사용자 경험이 상당히 뒤떨어져 페이지에 표시하고 싶은 부분 만 가져 왔습니다.
우리는 서버에 많은 RAM이 있기 때문에 웹 응용 프로그램이 시작될 때 전체 보관 테이블을 List<ArchiveRow>
에로드 한 다음이 목록에서 데이터를 가져올 수 있다고 생각했습니다. 데이터베이스로 왕복하는 대신에 이것은 매우 효과적입니다. 전체 아카이브 테이블 (현재 약 1 천만 항목)을 목록에로드하는 데 약 9 초가 걸립니다.
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; }
}
따라 : 나는 지금 LINQ 쿼리와 함께 목록에서 원하는 데이터를 얻을 때
public class ArchiveRow {
public int s { get; set; }
public int d { get; set; }
public DateTime t { get; set; }
public double v { get; set; }
}
, 그것은 이미 빠르게입니다 ArchiveRow
은 다음과 같습니다 간단한 객체이다 DB를 쿼리하지만 여러 기준으로 필터링 할 때 여전히 매우 느립니다. 예를 들어 하나의 StationId와 12 DatapointIds로 필터링하면 25 행의 창을 검색하는 데 약 5 초가 걸립니다. 나는 이미 Where
으로 필터링을 조인 (join)을 사용하는 것으로 바꾸었지만, 여전히 개선의 여지가 있다고 생각합니다. 메모리 소비를 가능한 한 낮게 유지하면서 이러한 캐싱 메커니즘을 구현하는 더 나은 방법이 있습니까? 이 목적에 더 적합한 다른 컬렉션 유형이 있습니까?
// 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);
좀 더 자세한 사항 : 방송국의 수가 50-5 사이에 일반적으로 무언가가, 거의 이상 (50)이다
그래서 여기가 필터와 ArchiveCache 목록에서 관련 데이터를 가져 오는 코드입니다 datapoints의 수는 < 7000입니다. 웹 응용 프로그램은 web.config에 <gcAllowVeryLargeObjects enabled="true" />
으로 64 비트로 설정됩니다.
정말 개선 및 권장 사항을 기다리고 있습니다. 어쩌면 배열이나 비슷한 것을 기반으로 완전히 다른 접근 방식이 있습니다. 더 좋은 방법을 수행합니다 없이 linq?
아마도 성능이 좋지 않았던 테이블 디자인과 SQL 쿼리의 세부 정보를 편집하고 추가하여 사람들이 가능한 개선을 제안 할 수 있다고 설명 했으므로 완벽한 성능을 기대할 수 있습니다. –
감사합니다. Alex, 나는 어디로 향하고 있는지 알고 있습니다. 그러나 명시 적으로이 캐싱 메커니즘을 개선하고 싶으므로 DB 부분을 의도적으로 설명하지 않았습니다. – Robert
기본 쿼리 매개 변수에 일종의 식별자 또는 해시를 제공하여 필터링에서 데이터 집합을 니어 필드 캐시에 저장할 수 있으므로 창을 통해 다시 사용할 수 있습니다 (식별자에 페이징 정보가 포함되지 않음). 매번 전체 반복을 수행하는'query.Count()'를 캐시 할 수 있습니다. 문제는 필터링 매개 변수의 차이와 동일한 세트가 다음 페이지에 다시 방문 할 가능성입니다. 사용자가 여러 페이지를 얻는 경향이있는 경우 대신 일괄 페이지를 반환하는 것이 좋습니다. –