パフォーマンスのためのモデリング

多くの場合、モデル化の方法は、アプリケーションのパフォーマンスに大きな影響を与える可能性があります。適切に正規化された "正しい" モデルが通常は適切な開始点ですが、実際のアプリケーションでは、いくらかの実利的な妥協が、優れたパフォーマンスを実現するために大変役立つこともあります。 アプリケーションがいったん運用環境で実行されると、モデルを変更することは非常に困難なため、初期モデルを作成する際にパフォーマンスのことを考慮することをお勧めします。

非正規化とキャッシュ

"非正規化" とは、スキーマに冗長データを追加する方法のことで、通常は、クエリを実行するときに結合をなくすことが目的です。 たとえば、ブログと投稿を含むモデルで、各投稿に評価がある場合、ブログの平均評価を頻繁に表示する必要がある場合があります。 このための単純な方法は、ブログによって投稿をグループ化し、クエリの一部として平均を計算することですが、このためにはコストのかかる結合を 2 つのテーブル間で行う必要があります。 非正規化では、すべての投稿で計算された平均が、ブログの新しい列に追加されるため、結合や計算を行わずにすぐにアクセスできます。

上記は一種の "キャッシュ" として見ることができ、投稿からの集計情報がブログにキャッシュされます。あらゆるキャッシュと同じように、ここでの問題は、キャッシュ中のデータを使用して、キャッシュされた値をいかにして最新の状態に保つかということです。 多くの場合、キャッシュされたデータが少し遅延しても問題ではありません。たとえば上記の例では、ブログの平均評価が特定の時点で完全に最新の状態でなくても通常は理にかなっています。 その場合は、毎回再計算させることができます。それ以外の場合は、キャッシュされた値を最新の状態に保つために、もっと入念なシステムを設定する必要があります。

次の部分では、EF Core での非正規化とキャッシュのための一部の手法と、ドキュメント内の関連するセクションを示しています。

格納された計算列

キャッシュされるデータが同じテーブル内の他の列から生成されたものである場合は、格納された計算列が最適なソリューションになることがあります。 たとえば、Customer には FirstNameLastName の列が含まれますが、顧客の "フル ネーム" で検索することが必要な場合もあります。 格納された計算列は、データベースによって自動的に維持管理され、行が変更されるたびに再計算されます。また、クエリを高速化するためにインデックスを定義することもできます。

入力が変更されたときにキャッシュ列を更新する

キャッシュされる列がテーブルの行の外部からの入力を参照する必要がある場合は、計算列を使用できません。 ただし、入力が変更されるたびに列を再計算することはできます。たとえば、投稿が変更、追加、または削除されるたびにブログの平均評価を再計算できます。 再計算が必要な場合の正確な条件を特定してください。そうしないと、キャッシュされる値が同期されなくなります。

これを行う方法の 1 つは、通常の EF Core API を使用して自分で更新を実行する方法です。 SaveChanges イベントまたはインターセプターを使用すると、Post が更新されているかを自動的に確認し、それに従って再計算を実行することができます。 これは追加のコマンドを送信する必要があるため、通常は追加のデータベース ラウンドトリップが必要になります。

パフォーマンスが重要なアプリケーションの場合、データベース トリガーを定義して、データベースで再計算を自動的に実行できます。 これにより、追加のデータベース ラウンドトリップがなくなり、メインの更新と同じトランザクション内で自動的に実行され、設定が簡単になります。 EF では、トリガーを作成または管理するための特定の API は提供されていませんが、空の移行を作成し、生の SQL を介してトリガー定義を追加してもまったく問題ありません。

具体化されたまたはインデックス付きビュー

具体化された (インデックス付き) ビューは通常のビューに似ていますが、ビューに対してクエリが実行されるたびに計算されるのではなく、データがディスクに格納される ("具体化される") 点が異なります。 このようなビューは、コストがかかる可能性がある計算の結果をキャッシュするため、概念的には格納された計算列と似ていますが、単一の列ではなく、クエリの結果セット全体をキャッシュします。 具体化されたビューに対しては、通常のテーブルと同様にクエリを実行できます。また、これらはディスクにキャッシュされるため、ビューを定義するクエリの高コストな計算を常に実行することなく、このようなクエリが非常に高速かつ低コストで実行されます。

具体化されたビューの具体的なサポートは、データベースによって異なります。 一部のデータベース (PostgreSQL など) では、具体化されたビューの値が基になるテーブルと同期されるように、ビューを手動で更新する必要があります。 これは通常、タイマー (多少のデータ ラグが許容される場合) または特定の条件でのトリガーまたはストアド プロシージャ呼び出しを介して行われます。 一方、SQL Server インデックス付きビューは、基になるテーブルが変更されると自動的に更新されます。これにより、更新は遅くなりますが、ビューには常に最新のデータが表示されるようになります。 さらに、SQL Server インデックス ビューには、サポートされる内容に関してさまざまな制限があります。詳細については、ドキュメントを参照してください。

EF では現在、具体化されたビューやインデックス付きビューなどを作成または管理するための特定の API は提供されていませんが、空の移行を作成し、生の SQL を介してビュー定義を追加してもまったく問題ありません。

継承マッピング

このセクションに進む前に、継承の専用ページをお読みください。

EF Core では現在、継承モデルをリレーショナル データベースにマッピングする 3 つの手法がサポートされています。

  • Table-Per-Hierarchy (TPH) では、クラスの .NET 階層全体が 1 つのデータベース テーブルにマップされます。
  • Table-Per-Type (TPT) では、.NET 階層内の各型がデータベース内の別のテーブルにマップされます。
  • Table-per-concrete-type (TPC) では、 .NET 階層の各具象型がデータベース内の異なるテーブルにマップされます。ここでは、各テーブルに、対応する型のすべてのプロパティの列が含まれます。

継承マッピング手法の選択は、アプリケーションのパフォーマンスに大きな影響を与える可能性があるため、選択を行う前に慎重に測定することをお勧めします。

直感的に、TPT は "よりクリーンな" 技術のように見えるかもしれません。.NET 型ごとに個別のテーブルを使用するデータベース スキーマは、.NET 型の階層に似ています。 さらに、TPH では階層全体を 1 つのテーブルで表現する必要があるため、行には、実際に行に保持される型に関係なく "すべての" 列が含まれ、関連しない列は常に空で使用されません。 "きれいではない" マッピング手法のように見えるだけでなく、多くの人は、これらの空の列によってデータベース内のかなりの領域が使用され、パフォーマンスも低下する可能性があると信じています。

ヒント

データベース システムで "スパース列" がサポートされている場合は (たとえば、SQL Server)、ほとんど設定されない TPH 列に対してそれを使うことを検討してください。

ただし、測定によれば、TPT はほとんどの場合、パフォーマンスの観点から見て劣ったマッピング手法であることがわかっています。TPH のすべてのデータが 1 つのテーブルから取得されるのに対し、TPT クエリでは複数のテーブルを結合する必要があります。結合は、リレーショナル データベースのパフォーマンスの問題の主な原因の 1 つです。 また、データベースは一般に空の列をうまく処理する傾向があり、SQL Server スパース列などの機能を使用すると、このオーバーヘッドをさらに削減できます。

TPC のパフォーマンス特性は TPH と似ていますが、すべての種類のエンティティを選択する場合は、複数のテーブルが含まれるため、少し遅くなります。 ただし、TPC は、クエリで 1 つのテーブルのみが使用され、フィルター処理を必要としない、単一のリーフ型のエンティティに対してクエリを実行する場合に優れています。

具体的な例については、このベンチマークを参照してください。ここでは 7 種類の階層を持つ単純なモデルが設定され、種類ごとに 5,000 行 (合計 35,000 行) がシード処理され、ベンチマークでは単にデータベースからすべての行が読み込まれます。

メソッド 平均 エラー StdDev Gen 0 Gen 1 Allocated
TPH 149.0 ms 3.38 ms 9.80 ms 4000.0000 1000.0000 40 MB
TPT 312.9 ms 6.17 ms 10.81 ms 9000.0000 3000.0000 75 MB
TPC 158.2 ms 3.24 ms 8.88 ms 5000.0000 2000.0000 46 MB

ご覧のように、TPH と TPC は、このシナリオでは TPT よりかなり効率的です。 実際の結果は、実行される特定のクエリと階層内のテーブルの数によって常に異なります。そのため、別のクエリではパフォーマンスの差が異なる可能性があります。このベンチマーク コードは、他のクエリをテストするテンプレートとして使用することをお勧めします。