EntityFrameworkを介してSQLServerDBに接続されているASP.NETMVCWebアプリケーションがあります。このアプリケーションの主なタスクの1つは、ユーザーがアーカイブ値を保持する巨大なデータベーステーブルをすばやく検索してフィルタリングできるようにすることです。
テーブル構造は非常に単純です:Timestamp(DateTime)、StationId(int)、DatapointId(int)、Value(double)。このテーブルには、1,000万から1億の行が含まれています。カバーインデックスなどを使用してDBテーブルを最適化しましたが、DatapointId、StationId、Time、およびページに表示したい部分のみをスキップして取得することでフィルタリングすると、ユーザーエクスペリエンスは依然としてかなり遅くなりました。
そこで、別のアプローチを試しました。サーバーには大量のRAMList<ArchiveRow>
があるため、Webアプリの起動時にアーカイブテーブル全体をにロードし、ラウンドを実行する代わりに、このリストから直接データを取得できると考えました。-データベースに移動します。これは非常にうまく機能し、アーカイブテーブル全体(現在約1,000万エントリ)をリストにロードするのに約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クエリを使用してリストから目的のデータを取得しようとすると、DBのクエリはすでに高速になりますが、複数の条件でフィルタリングすると、それでもかなり低速になります。たとえば、1つのStationIdと12のDatapointIdでフィルタリングすると、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アプリケーションは<gcAllowVeryLargeObjects enabled="true" />
、web.configで設定された64ビットに設定されます。
さらなる改善と推奨事項を本当に楽しみにしています。おそらく、配列などに基づく完全に異なるアプローチがあり、linqなしではるかに優れたパフォーマンスを発揮しますか?
この特定のクエリタイプに合わせてストレージを調整できます。まず、メモリ内アーカイブから辞書を作成します。
ArchiveCacheByDatapoint = ArchiveCache.GroupBy(c => c.DataPointId)
.ToDictionary(c => c.Key, c => c.ToList());
ArchiveCacheByStation = ArchiveCache.GroupBy(c => c.StationId)
.ToDictionary(c => c.Key, c => c.ToList());
次に、クエリでこれらの辞書を使用します。
bool hasStations = request.StationIds.Length > 0;
bool hasDatapoints = request.DatapointIds.Length > 0;
int numFound = 0;
List<ArchiveCacheValue> result;
if (hasDatapoints && hasStations) {
// special case - filter by both
result = new List<ArchiveCacheValue>();
// store station filter in hash set
var stationsFilter = new HashSet<int>(request.StationIds);
// first filter by datapoints, because you have more different datapoints than stations
foreach (var datapointId in request.DatapointIds.OrderBy(c => c)) {
foreach (var item in ArchiveCacheByDatapoint[datapointId]) {
if (stationsFilter.Contains(item.StationId)) {
// both datapoint and station matches filter - found item
numFound++;
if (numFound >= request.Start && result.Count < request.Length) {
// add to result list if matches paging criteria
result.Add(item);
}
}
}
}
}
else if (hasDatapoints) {
var query = Enumerable.Empty<ArchiveCacheValue>();
foreach (var datapoint in request.DatapointIds.OrderBy(c => c))
{
var list = ArchiveCacheByDatapoint[datapoint];
numFound += list.Count;
query = query.Concat(list);
}
// execute query just once
result = query.Skip(request.Start).Take(request.Length).ToList();
}
else if (hasStations) {
var query = Enumerable.Empty<ArchiveCacheValue>();
foreach (var station in request.StationIds.OrderBy(c => c))
{
var list = ArchiveCacheByStation[station];
numFound += list.Count;
query = query.Concat(list);
}
// execute query just once
result = query.Skip(request.Start).Take(request.Length).ToList();
}
else {
// no need to do Count()
numFound = ArchiveCache.Count;
// no need to Skip\Take here really, ArchiveCache is list\array
// so you can use indexes which will be faster
result = ArchiveCache.Skip(request.Start).Take(request.Length).ToList();
}
// Number of entries on the current page that will be shown
int numShown = result.Count;
私はそれを測定し、私のマシンでは、1億アイテムについて、試したすべてのタイプのクエリ(セクションのみ、データポイントのみ、セクションとデータポイントの両方)に対して1ミリ秒(場合によっては最大10ミリ秒)で実行されます。
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加