エンティティフレームワーク経由でSQL Server DBに接続されているASP.NET MVC Webアプリケーションがあります。このアプリケーションの主なタスクの1つは、ユーザーがアーカイブ値を保持する巨大なデータベーステーブルをすばやく検索およびフィルタリングできるようにすることです。C#の巨大なリストをフィルタリングする最もパフォーマンスの良い方法は?
テーブル構造は非常に単純です:タイムスタンプ(DateTime)、StationId(int)、DatapointId(int)、Value(double)。この表には、10億〜1億行の間のものがあります。私はDBテーブルをカバリングインデックスなどで最適化しましたが、DatapointId、StationId、Time、Skippingでフィルタリングして、ページに表示したい部分だけを取ると、ユーザーエクスペリエンスはまだかなり遅いです。
私はサーバーに多くのRAMがあるため、Webアプリケーションの起動時にアーカイブテーブル全体をList<ArchiveRow>
にロードするだけで、このリストから直接データを取得することができますデータベースへの往復をする代わりに。これは非常にうまくいきます。アーカイブテーブル全体(現在は約1,000万エントリ)をリストにロードするのに約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; }
}
し、それに応じて:
public class ArchiveRow {
public int s { get; set; }
public int d { get; set; }
public DateTime t { get; set; }
public double v { get; set; }
}
私は今、LINQクエリをリストから目的のデータを取得しようとすると、それはすでに高速ですArchiveRow
は、次のようになります単純なオブジェクトでありますDBにクエリを実行していますが、複数の条件でフィルタリングすると、まだかなり遅いです。たとえば、1つのStationIdと12のDatapointIdsでフィルタすると、25行のウィンドウを取得するのに約5秒かかります。私はすでにWhere
のフィルタリングから結合を使用するように切り替えましたが、まだ改良の余地があると思います。メモリ消費量を可能な限り低く抑えながらキャッシュメカニズムを実装する方が良いでしょうか?この目的に適した他のコレクションタイプがありますか?
// 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リストから該当するデータをフェッチコードですデータポイントの数は< 7000です。Webアプリケーションは、web.configに<gcAllowVeryLargeObjects enabled="true" />
を設定して64ビットに設定されています。
私は本当にさらなる改善と推奨をお待ちしております。たぶん、配列やそれに類するものをベースにした全く異なるアプローチがありますが、より良い方法を実行しますなし linq?
おそらく編集し、人々が可能な改善を提案することができるようにパフォーマンスが低下して、テーブルのデザイン&SQLクエリの詳細を追加し、あなたはそれを説明してきたように、1つは、それが完全にパフォーマンスであることを期待します。 –
ありがとう、アレックス、どこに向かうのかは分かっていますが、私は明示的にこのキャッシュメカニズムを改善したいので、DB部分を意図的に記述しませんでした。 – Robert
私は基になるクエリパラメータに何らかの識別子またはハッシュを与えて、フィルタリングからデータセットをニアフィールドキャッシュすることができるので、ウィンドウを使って再利用するだけです(識別子にページング情報を含めないでください)。そのたびに完全な繰り返しを行う 'query.Count()'をキャッシュすることができます。課題は、フィルタリングパラメータの分散と、同じセットが(次のページのために)再訪問する可能性です。ユーザーが複数のページを取得する傾向がある場合は、代わりにページのバッチを返すことを検討してください。 –