適用対象:SQL Server
Azure SQL Database
Azure SQL Managed Instance
メモリ最適化テーブルでは、すべての行とインデックスをメモリ内に保持するために十分なメモリが必要です。 メモリは有限のリソースであるため、システム上のメモリ使用量を把握して管理することが重要です。 このセクションのトピックでは、一般的なメモリの使用量と管理シナリオについて説明します。
十分なメモリでサーバーをプロビジョニングできるように、各メモリ最適化テーブルのメモリニーズを適切に見積もる必要があります。 これは、新しいテーブルと、ディスク ベースのテーブルから移行されたテーブルの両方に適用されます。 ここでは、メモリ最適化テーブルのデータを保持するために必要とされるメモリの量を推定する方法について説明します。
ディスク ベースのテーブルからメモリ最適化テーブルへの移行を検討している場合は、移行に最適なテーブルのガイダンスについては、「 テーブルまたはストアド プロシージャを OLTP In-Memory に移植する必要があるかどうかを判断 する」を参照してください。 「 インメモリ OLTP への移行 」に掲載されているすべてのトピックには、ディスク ベース テーブルからメモリ最適化テーブルへの移行に関するガイダンスが掲載されています。
メモリ要件を見積もるための基本的なガイダンス
SQL Server 2016 (13.x) 以降のバージョンでは、メモリ最適化テーブルのサイズに制限はありませんが、テーブルはメモリに収まる必要があります。 SQL Server 2014 (12.x) では、SCHEMA_AND_DATA テーブルでサポートされるデータ サイズは 256 GB です。
メモリ最適化テーブルのサイズは、データのサイズに行ヘッダーの一部のオーバーヘッドを加えたものに相当します。 メモリ最適化テーブルのサイズは、元のディスク ベース テーブルのクラスター化インデックスまたはヒープのサイズとほぼ同じです。
メモリ最適化テーブルのインデックスは、ディスク ベース テーブルの非クラスター化インデックスよりも小さくなる傾向があります。 非クラスター化インデックスのサイズは [primary key size] * [row count]と同程度です。 ハッシュ インデックスのサイズは [bucket count] * 8 bytesです。
アクティブなワークロードがある場合は、行のバージョン管理およびさまざまな操作を行うために追加のメモリが必要です。 必要なメモリ量はワークロードによって異なりますが、安全を確保するには、メモリ最適化テーブルとインデックスの予想サイズの 2 倍から始めて、実際のメモリ消費量を確認することをお勧めします。 行のバージョン管理のオーバーヘッドは、常にワークロードの特性に依存します。特に長時間実行されるトランザクションではオーバーヘッドが増加します。 大規模なデータベース (100 GB を超えるなど) を使用するほとんどのワークロードでは、オーバーヘッドが制限される傾向があります (25% 以下)。
In-Memory OLTP エンジンの潜在的なメモリ オーバーヘッドの詳細については、「 メモリの断片化」を参照してください。
メモリ要件の詳細な計算
サンプルのメモリ最適化テーブル
次のメモリ最適化テーブルのスキーマを考えてみます。
CREATE TABLE t_hk
(
col1 int NOT NULL PRIMARY KEY NONCLUSTERED,
col2 int NOT NULL INDEX t1c2_index
HASH WITH (bucket_count = 5000000),
col3 int NOT NULL INDEX t1c3_index
HASH WITH (bucket_count = 5000000),
col4 int NOT NULL INDEX t1c4_index
HASH WITH (bucket_count = 5000000),
col5 int NOT NULL INDEX t1c5_index NONCLUSTERED,
col6 char (50) NOT NULL,
col7 char (50) NOT NULL,
col8 char (30) NOT NULL,
col9 char (50) NOT NULL
) WITH (memory_optimized = on) ;
GO
このスキーマを使用して、このメモリ最適化テーブルに必要な最小メモリを決定しましょう。
テーブルに対応するメモリ
メモリ最適化テーブル行には、次の 3 つの部分があります。
タイムスタンプ
行のヘッダー/タイムスタンプ = 24 バイトです。インデックス ポインター
テーブル内の各ハッシュ インデックスにある各行には、インデックス内の次の行を指す 8 バイトのアドレス ポインターが含まれています。 4 つのインデックスがあるため、各行はインデックス ポインターに 32 バイトを割り当てます (インデックスごとに 8 バイトのポインター)。Data
行のデータ部分のサイズは、各データ列に対応する型サイズの合計によって決まります。 このテーブルには、4 バイトの整数が 5 個、50 バイトの文字列型の列が 3 個、30 バイトの文字列型の列が 1 個あります。 したがって、各行のデータ部分は、4 + 4 + 4 + 4 + 4 + 50 + 50 + 50 + 30、つまり 200 バイトです。
以下は、メモリ最適化テーブル内に存在する 5,000,000 (500 万) 行のサイズの計算です。 データ行で使用される合計メモリは、次のように推定されます。
テーブルの行に対応するメモリ
上記の計算結果から、メモリ最適化テーブル内にある各行のサイズは 24 + 32 + 200、つまり 256 バイトです。 500 万行があるため、テーブルは 5,000,000 * 256 バイト、または 1,280,000,000 バイト (約 1.28 GB) を消費します。
インデックスに対応するメモリ
各ハッシュ インデックスに対応するメモリ
各ハッシュ インデックスは、8 バイトのアドレス ポインターから成るハッシュの配列です。 配列のサイズは、そのインデックスの一意のインデックス値の数によって最適に決定されます。 現在の例では、一意の Col2 値の数が、t1c2_indexの配列サイズの開始点として適しています。 大きすぎるハッシュ配列は、メモリを浪費します。 ハッシュ配列が小さすぎると、同じインデックス エントリにハッシュするインデックス値による競合が多すぎるため、パフォーマンスが低下します。
ハッシュ インデックスは、次のように等値参照を実行する場合は非常に高速です。
SELECT * FROM t_hk
WHERE Col2 = 3;
非クラスター化インデックスは、次のように範囲参照を実行する場合はより高速になります。
SELECT * FROM t_hk
WHERE Col2 >= 3;
ディスク ベース テーブルを移行する場合は、t1c2_index インデックスに対応する一意の値の数を決定するために、次を使用できます。
SELECT COUNT(DISTINCT [Col2])
FROM t_hk;
新しいテーブルを作成する場合は、デプロイ前に配列サイズを見積もるか、テストからデータを収集する必要があります。
インメモリ OLTP メモリ最適化テーブル内でのハッシュ インデックスの動作方法の詳細については、「ハッシュ インデックス」を参照してください。
ハッシュ インデックスの配列サイズの設定
ハッシュの配列サイズは (bucket_count= value) によって設定されます。ここで、 value は、0 より大きい整数値です。
value が 2 のべき乗でない場合は、実際の bucket_count は、最も近い 2 のべき乗になるように切り上げられます。 この例のテーブルでは bucket_count = 5,000,000 であり、5,000,000 は 2 のべき乗ではないため、実際のバケット数は 8,388,608 (2^23) に切り上げられます。 ハッシュ配列が必要とするメモリを計算するときは、5,000,000 ではなく、この数値を使用する必要があります。
したがって、この例の各ハッシュ配列で必要とされるメモリは次のようになります。
8,388,608 * 8 = 2^23 * 8 = 2^23 * 2^3 = 2^26 = 67,108,864 つまり約 64 MB です。
3 つのハッシュ インデックスが存在するため、ハッシュ インデックスで必要とされるメモリは 3 * 64 MB = 192 MB です。
非クラスター化インデックスに対応するメモリ
非クラスター化インデックスは Bw ツリーとして実装されており、内部ノードはインデックス値と、後続のノードを指すポインターを保持しています。 リーフ ノードは、インデックス値と、メモリ内にあるテーブルの行を指すポインターを保持しています。
ハッシュ インデックスとは異なり、非クラスター化インデックスには、固定的なバケット サイズがありません。 インデックスは、データと共に動的に拡張または圧縮されます。
非クラスター化インデックスのメモリ必要量は、次のように計算することができます。
非リーフ ノードに割り当てられたメモリ
一般的な構成では、非リーフ ノードに割り当てられるメモリは、インデックスによって取得されたメモリ全体のごく一部です。 これは非常に小さい割合であり、無視しても安全です。リーフ ノードに割り当てられるメモリ
リーフ ノードにはテーブル内に存在する一意のキーごとに 1 行のデータがあり、リーフ ノードは、その一意のキーを持つデータ行を指します。 同じキーを持つ複数の行がある場合 (つまり、一意でない非クラスター化インデックスがある場合)、インデックス リーフ ノードには、他の行が相互にリンクされている行の 1 つを指す行が 1 つだけあります。 したがって、必要とされるメモリ全体は、次のように近似できます。- 非クラスター化インデックスのメモリ = (ポインタサイズ + キー列データ型サイズの合計) * ユニークキーを持つ行数
非クラスター化インデックスは、次のクエリで例示する範囲参照の場合に使用するのが最適です。
SELECT * FROM t_hk
WHERE c2 > 5;
行のバージョン管理に対応するメモリ
ロックを回避するために、インメモリ OLTP は行を更新または削除するときに、オプティミスティック コンカレンシーを使用します。 これは、行を更新するときに、行の他のバージョンが作成されることを意味します。 さらに、削除は論理的であり、既存の行が削除済みとしてマークされますが、すぐには削除されません。 システムは、バージョンを使用できる可能性のあるすべてのトランザクションの実行が完了するまで、古い行バージョン (削除された行を含む) を使用できるようにします。
メモリ内にはいつでも多数の追加行が存在している可能性があり、それらの行は自らのメモリを解放するガベージ コレクション サイクルを待機しているため、これらの他の行を収容するために十分なメモリを用意する必要があります。
追加の行の数を推定するには、1 秒あたりの行の更新と削除に関するピークの数値を求め、その値に、最も長いトランザクションが要する秒数 (最小値は 1) を掛けます。
この積に、行サイズを掛けると、行のバージョン管理に必要とされるバイト数を得ることができます。
rowVersions = durationOfLongestTransactionInSeconds * peakNumberOfRowUpdatesOrDeletesPerSecond
古い行のメモリニーズは、古い行の数とメモリ最適化テーブル行のサイズを乗算することによって推定されます。 詳細については、 テーブルのメモリを参照してください。
memoryForRowVersions = rowVersions * rowSize
テーブル変数に対応するメモリ
テーブル変数に対応するメモリは、テーブル変数がスコープ外になる場合にのみ解放されます。 テーブル変数から削除された行 (更新の一部として削除された行を含む) には、ガベージ コレクションは適用されません。 テーブル変数がスコープを終了するまでメモリは解放されません。
ストアド プロシージャではなく大規模な SQL バッチで定義され、多くのトランザクションで使用されるテーブル変数は、大量のメモリを消費する可能性があります。 ガベージ コレクションの対象ではないため、テーブル変数内にある削除された行は多くのメモリを使用し、パフォーマンスが低下することがあります。読み取り操作では、削除されたこれらの行をスキャンで通過させる必要があるためです。
成長に対応するメモリ
前の計算では、現在存在するテーブルのメモリニーズが見積もられます。 このメモリに加えて、テーブルが成長することを推定し、その成長を収容するために十分なメモリを用意する必要があります。 たとえば、10% 増加が予想される場合、テーブルに必要な合計メモリを取得するには、前の結果を 1.1 で複数する必要があります。
メモリの断片化
メモリ割り当て呼び出しのオーバーヘッドを回避し、パフォーマンスを向上させるために、In-Memory OLTP エンジンは常に、スーパーブロックと呼ばれる 64 KB のブロックを使用して SQL Server オペレーティング システム (SQLOS) からメモリを要求します。
各スーパーブロックには、sizeclass と呼ばれる特定のサイズ範囲内でのみメモリ割り当てが含まれます。 たとえば、スーパーブロック A では 1 から 16 バイトのサイズクラスにメモリ割り当てがあり、スーパーブロック B には 17 から 32 バイトのサイズクラスにメモリ割り当てがある場合があります。
既定では、スーパーブロックも論理 CPU によってパーティション分割されます。 つまり、論理 CPU ごとに、個別のスーパーブロックのセットがあり、さらに sizeclass で分類されます。 これにより、異なる CPU で実行されている要求間のメモリ割り当ての競合が軽減されます。
In-Memory OLTP エンジンは、新しいメモリ割り当てを行うときに、最初に、要求されたサイズクラスと要求を処理する CPU の既存のスーパーブロックで空きメモリを検索しようとします。 この試行が成功した場合、特定のメモリ コンシューマーのsys.dm_xtp_system_memory_consumersのused_bytes列の値は、要求されたメモリ サイズによって増加しますが、allocated_bytes列の値は変わりません。
既存のスーパーブロックに空きメモリがない場合は、新しいスーパーブロックが割り当てられ、 used_bytes の値は要求されたメモリ サイズによって増加しますが、 allocated_bytes 列の値は 64 KB 増加します。
時間が経つにつれて、スーパーブロック内のメモリが割り当てられ、割り当てが解除されると、In-Memory OLTP エンジンによって消費されるメモリの合計量が、使用されたメモリの量よりも大幅に大きくなる可能性があります。 つまり、メモリが断片化する可能性があります。
ガベージ コレクション によって使用されるメモリが減少する可能性がありますが、1 つ以上のスーパーブロックが空になり、割り当てが解除された場合にのみ、割り当てられたメモリが削減されます。 これは、 sys.sp_xtp_force_gc システム ストアド プロシージャを使用した自動ガベージ コレクションと強制ガベージ コレクションの両方に適用されます。
In-Memory OLTP エンジンのメモリ断片化と割り当てられたメモリ使用量が予想より高くなった場合は、 トレース フラグ 9898 を有効にすることができます。 これにより、スーパーブロックのパーティション分割スキームが CPU 単位から NUMA ノードごとに変更され、スーパーブロックの合計数とメモリの断片化の可能性が減少します。
この最適化は、多くの論理 CPU を持つ大規模なマシンに適しています。 この最適化のトレードオフは、スーパーブロックの数が少ないほどメモリ割り当ての競合が増加する可能性があり、ワークロード全体のスループットが低下する可能性があります。 ワークロード パターンによっては、NUMA メモリごとのパーティション分割を使用した場合のスループットの低下が目立つ場合と、目立たない場合があります。