SQL Server 2005 の複数のアクティブな結果セット (MARS)
Christian Kleinerman
Microsoft Corporation
April 2005
Updated June 2005
日本語版最終更新日 2005 年 11 月 14 日
対象:
Microsoft SQL Server 2005
複数のアクティブな結果セット (MARS)
概要 : すべての Microsoft SQL Server データ アクセス アプリケーション プログラミング インターフェイス (API) には、セッションとそれらのセッション内の要求を抽象化するオブジェクトが用意されています。SQL Server 2000 以前のバージョンでは、任意の時点で特定のセッションに存在する保留中の要求は多くても 1 つとなるように、プログラミング モデルが制限されていました。SQL Server 2005 では、複数のアクティブな結果セット (MARS) を実装し、この制約を取り除いています。この資料では、MARS のデザイン、アーキテクチャ、およびセマンティクスの変更点と、これらの機能強化を最大限に活用するためにアプリケーションに取り入れるべき考慮事項について説明します。
目次
はじめに
SQL Server 2000 データ アクセスの概要
"接続ビジー"
"既に MARS を保持しています"
対処方法
トランザクションと実行環境の概要
複数のアクティブな結果セット - MARS
インタリーブ実行
MARS のパフォーマンスとコストに関する考慮事項
トランザクションのセマンティクス
セーブポイント
実行環境
MARS のデッドロック
監視と診断
まとめ
はじめに
すべての Microsoft SQL Server データ アクセス API には、セッションとそれらのセッション内の要求を抽象化するオブジェクトが用意されています。SQL Server 2000 以前のバージョンでは、任意の時点で特定のセッションに存在する保留中の要求は多くても 1 つとなるように、プログラミング モデルが制限されていました。この制限を解決するために、いくつかの代替策が実装されました。その中で最も一般的なのは、サーバー側のカーソルの使用です。SQL Server 2005 では、複数のアクティブな結果セット (MARS) を実装し、この制約を取り除いています。この資料では、MARS のデザイン、アーキテクチャ、およびセマンティクスの変更点と、これらの機能強化を最大限に活用するためにアプリケーションに取り入れるべき考慮事項について説明します。
SQL Server 2000 データ アクセスの概要
SQL Server に対してアプリケーションを構築するために現在サポートされている主要なデータ アクセス API は、ODBC、OLEDB、ADO、および SqlClient .NET Provider1 です。これらすべての API によって、サーバーに対して確立された接続を表す抽象オブジェクトと、その接続で実行される要求を表す別の抽象オブジェクトが提供されます。たとえば、ODBC ではハンドルを使用するのに対して、SqlClient には SQL_HANDLE_DBC 型の SqlConnection オブジェクトと SQL_HANDLE_STMT 型の SqlCommand オブジェクトが用意されています。
実行要求は、多くの場合、1) 一般的にバッチとして呼ばれる一連の T-SQL ステートメント、または 2) 必要なパラメータ値を指定するストアド プロシージャ名または関数名のいずれかの形式で SQL Server に送信されます。1 つの SELECT ステートメントや DML ステートメントをサーバーに送信することは、1 つのステートメント バッチを送信しているにすぎないことに注意してください。これは、1 つ目の形式の実行要求の特殊な例です。
どちらの場合でも、SQL Server によって、バッチまたはストアド プロシージャに含まれているステートメントが繰り返され、それらのステートメントが順次実行されます。ステートメントによって結果が生成されるかどうかは場合によって異なり、ステートメントから呼び出し側に追加情報が返されるかどうかも場合によって異なります。
結果は主に SELECT ステートメントと FETCH ステートメントで生成されます。SQL Server では、結果を呼び出し側にストリーミングして返すことで、SELECT ステートメントを実行します。つまり、行はクエリ実行エンジンによって生成されたとき、ネットワークに書き込まれます。さらに詳しく説明すると、生成された行は、あらかじめ確保されたネットワーク バッファにコピーされます。このバッファが呼び出し側に送信されます。ネットワークへの書き込み操作は、クライアント ドライバがネットワークからの読み取りを実行している限り正常に実行され、使用済みのバッファを解放します。クライアントで結果が読み取られないと、ある時点でネットワークへの書き込み操作がブロックされ、サーバーのネットワーク バッファがいっぱいになり、実行が中断されます。この操作は、クライアント ドライバによって読み取りの遅れが取り戻されるまで、状態と実行スレッドで保持されます。一般的に、結果を生成して取得するこのモードは "既定の結果セット" と呼ばれ、俗に "firehose カーソル" とも呼ばれます。
他の方法で追加情報が呼び出し側に返されることがあります。この方法は、結果の場合ほど明確ではない可能性があります。エラー、警告、および情報メッセージがその一例です。これらは、PRINT ステートメントや RAISERROR ステートメントから明示的に返されるか、ステートメントの実行中に生成される警告やエラーから暗黙に返されます。同様に、NOCOUNT 設定オプションが OFF に設定されている場合、SQL Server では実行されるステートメントごとに "done row count" トークンを送信します。この追加情報により、ネットワーク書き込みバッファがいっぱいになり、実行が中断される可能性もあります。
このような背景を把握することで、接続ごとに複数の保留中の要求をサポートする場合の、SQL Server 2000 以前のバージョンに見られるプログラミング モデルの一部の制限事項を理解できます。
"接続ビジー"
この資料の例として、疎結合の在庫処理システムの単純なシナリオを想定します。このシステムでは、さまざまな他のコンポーネントから要求を受信するためのキューとして、次の "Operations" というテーブルを使用します。
表 1. Operations
|
列名 |
データ型 |
コメント |
|---|---|---|
|
processed |
bit |
|
|
operation_id |
int |
主キー |
|
operation_code |
char(1) |
'D' - 在庫の減少 'I' - 在庫の増加 'R' - 在庫の予約 |
|
product_id |
uniqueidentifier |
|
|
quantity |
bigint |
|
このコンポーネントでは、さまざまな製品ラインと業者の在庫を管理し、product_id によって、使用するサーバーとデータベースおよび要求された操作の実行方法を判断するものとします (つまり、キュー内のすべての要求を処理する小さなセット ベースの操作は記述できないものとします)。
このコンポーネントはループ内で実行され、テーブルに挿入される要求を処理し、指定された操作が正常に完了したときに要求に processed を設定します。
擬似コードは、次のようになります。
while (1)
{
Get all messages currently available in Operations table;
For each message retrieved
{
ProcessMessage();
Mark the message as processed;
}
}
ODBC を使用した場合、最初のアプローチは次のようになりました (一部の詳細とエラー処理は省略しています)。
SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt1);
SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt2);
while (true)
{
SQLExecDirect(hstmt1, (SQLTCHAR*)"select operation_id,
operation_code, product_id, quantity from dbo.operations
where processed=0", SQL_NTS);
while (SQL_ERROR!=SQLFetch(hstmt1))
{
ProcessOperation(hstmt1);
SQLPrepare(hstmt2,
(SQLTCHAR*)"update dbo.operations set processed=1
where operation_id=?", SQL_NTS);
SQLBindParameter(hstmt2, 1, SQL_PARAM_INPUT, SQL_C_SLONG,
SQL_INTEGER, 0, 0, &opid, 0, 0);
SQLExecute(hstmt2);
}
}
しかし、hstmt2 を実行しようとすると、次のような結果が返されました。
[Microsoft][ODBC SQL Server Driver]Connection is busy with results for another hstmt.
SqlClient を使用して Microsoft Visual C# で同じロジックを記述すると、次のようになります。
SqlCommand cmd = conn.CreateCommand();
SqlCommand cmd2 = conn.CreateCommand();
cmd.CommandText= "select operation_id, operation_code, product_id, quantity
from dbo.operations where processed=0";
cmd2.CommandText="update dbo.operations set processed=1
where operation_id=@operation_id";
SqlParameter opid=cmd2.Parameters.Add("@operation_id", SqlDbType.Int);
reader=cmd.ExecuteReader();
while (reader.Read())
{
ProcessOperation();
opid.Value=reader.GetInt32(0); // operation_id
cmd2.ExecuteNonQuery();
}
同様に、このロジックを実行しようとすると次の結果が表示されます。
InvalidOperationException, There is already an open DataReader associated with this Connection which must be closed first.
上記のエラーは、MARS の欠如を最もわかりやすく説明しています。つまり、特定の SQL Server 接続で保留状態にできる要求は多くても常に 1 つということです。
OLEDB ではやや異なる動作が見られるので、ここでは意図的に除外しました。
"既に MARS を保持しています"
以前のリリースの SQLOLEDB クライアント ドライバによって MARS のシミュレーションが試行されたので、MARS に関しては OLEDB を特別扱いする必要があります。ただし、この試みには多くの危険が潜んでいます。上記の例を OLEDB で表すと、次のようになります (ここでもエラー処理は省略しています)。
pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**) &pICommandText);
pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown**) &pICommandText2);
pICommandText->SetCommandText(DBGUID_DBSQL,
OLESTR("select operation_id, operation_code, product_id, quantity
from dbo.operations where processed=0"));
pICommandText2->SetCommandText(DBGUID_DBSQL,OLESTR("update dbo.operations
set processed=1 where operation_id=?"));
//Execute the command
pICommandText->Execute(NULL, IID_IRowset, NULL, &cRowsAffected, (IUnknown**) &pIRowset);
...
ProcessOperation();
...
//Execute the command 2
pICommandText2->Execute(NULL, IID_IRowset, NULL, &cRowsAffected, NULL);
大変興味深いことに、このコードは成功し、目的の処理が実行されるようです。MARS の欠如によってエンジンが根本的に制限されるように思われますが、このコードはどのようにして処理を成功させているのでしょうか。さらに調査を進めると、SQLOLEDB ドライバによって暗黙に新しい接続が作成され、その接続でコマンド 2 が実行されていることが明らかになります。つまり、既に MARS の機能があったということでしょうか。いいえ、そうではありません。
データベース エンジンでは、これら 2 つの接続を適切に連携させるために、何も特別な処理は行われていません。これらは単純に 2 つの接続なので、保持する実行環境が異なります。より重要なのは、これら 2 つの接続が互いに競合する可能性があることです。ITransactionLocal::StartTransaction が明示的に呼び出されたことや、セッションが DTC トランザクションに参加したことにより、明示的なトランザクションにセッションが存在するときは、常に SQLOLEDB では新しい接続を作成できません。この場合、コマンド 2 の実行は失敗します。
ただし、いずれかのコマンドが TSQL によりトランザクションを開始した場合、SQLOLEDB はこの状態を認識せず、新しい接続の作成を許可します。残念なことに、2 つの異なるコマンドは同じセッションの一部のように見えますが、結果的には別のトランザクションで実行されます。
セッションの分離レベルを上げて REPEATABLE READ にすると、上記のアプリケーション コードは望ましくない状態になります。コマンド 1 では、operations テーブル内の未処理の行をすべて取得するクエリを実行しています。分離レベルを上げると、トランザクションが終了するまでロックが保持されます。明示的なトランザクションが使用されていない場合は、ステートメントが自動コミット モードで実行され、ステートメントの最後までロックが保持されます。コマンド 2 によって特定の行が変更されるときに、その行でロックが保持されている場合は、クライアント コードが関係するデッドロックが発生し、アプリケーションが応答を停止します。
動作を少し予想しにくくするために、生成された最後の行がクライアント アプリケーションによって読み込まれたときではなく、サーバーのネットワーク バッファにコピーされたときにコマンド 1 のステートメントが完了するものとします。この想定は、上記のコードが小さな行セットでは成功しても、コマンド 2 の実行までにサーバーがコマンド 1 の実行を完了できない程データ量が大きくなると、上記のコードが失敗するということを暗黙に示しています。開発環境では予想どおりに動作しても、実稼動環境に展開されると突然応答を停止するアプリケーションを目にするのは、驚くべきことではありません。
結果的に、エミュレートされた MARS のような SQLOLEDB の動作に依存するのは、あまり最適なアプリケーション デザインとは言えません。このようなアプリケーション デザインを使用する場合は、このデザインに含まれる可能性がある、その他の暗黙の接続やセマンティクスへの影響を認識しておいてください。
対処方法
SQL Server 2000 以前のバージョンには MARS が存在しませんが、どうすればアプリケーションを動作させることができるでしょうか。アプリケーションの要求に応じて、複数の接続を明示的に使用する必要がある場合があります。その他の多くの場合は、サーバー側のカーソルを使用できます。
アプリケーションでは、サーバー側カーソルを使って、1 度に 1 行または行の小さなブロックに分けてクエリ結果を利用できます。さまざまな型やオプションの詳しく説明しないと、一般にカーソルは 1 つのクエリの結果を表すと言えます。カーソルでは、クエリ結果を必要に応じて返すことができるように、メモリ内とディスク上に状態を保持します。
一般的な使用方法のパターンとして、カーソルは指定されたクエリの先頭で宣言されます。各行または行ブロックを結果セットから取得するために、フェッチ操作が実行されます。行がすべて使用されるか結果セットが不要になったら、カーソルが破棄され、関連付けられたサーバー側のリソースが解放されます。この説明の中で注意する最も重要なことは、各フェッチ操作間でカーソルの代わりにその機能を実行するコードが存在しないことです。サーバーでは、状態は保持されますが、処理は保留されません。
ODBC と OLEDB では、クエリ要求をマップしてサーバー側のカーソルを使用できるように、プロパティを公開しています。
最初のコマンドでサーバー側のカーソルを使用するように上記の ODBC の例を変更すると、アプリケーション シナリオが成功し、予想どおりに機能するようになります。次のように、1 行を変更します。
SQLSetStmtAttr(hstmt1, SQL_ATTR_CURSOR_TYPE, (SQLPOINTER)SQL_CURSOR_DYNAMIC, 0);
OLEDB でも同様の変更を行い、暗黙の接続の作成や関連する危険性を回避することができます。
ここまで、サーバー側のカーソルが MARS の欠如に対処するのにどのように役立つかを見てきました。後ほど説明しますが、これは MARS の利用によってカーソルが不要になったり、MARS を使用するように、カーソルを使用しているすべてのアプリケーションを変更する必要があることを意味しているわけではありません。
そうすると、"見たところ制限が厳しい既定の結果セットではなく、なぜサーバー側のカーソルを常に使用しないのか" という疑問が自然と浮かんできます。この理由として、1) すべての種類のカーソルが、すべての有効な SQL でサポートされているわけではないこと、2) カーソルは、1 度に 1 つの SELECT ステートメントでしか使用できないこと、3) パフォーマンスが挙げられます。
既定の結果セットは、結果が利用できるようになったときに "生成される" ので、サーバー側のカーソルよりパフォーマンスに優れています。一方、カーソルでは、フェッチ操作ごとにサーバーへのラウンド トリップが必要になります。
つまり、MARS の最も大きな特徴は "接続ビジー" が取り除かれることであり、このことはプログラミング モデルの大きな強化点であると言えます。
トランザクションと実行環境の概要
SQL Server 2000 以前のリリースのセッションは、次のような状態になる可能性があります。
-
アクティブなトランザクションがない。この状態は一般に自動コミット モードとして知られており、セッションで実行されるすべてのステートメントが別々のトランザクションで実行されることを示します。
-
ローカル トランザクションがアクティブである。セッションで実行されるすべてのステートメントが、TSQL の BEGIN TRANSACTION コマンドを明示的に実行するか、IMPLICIT_TRANSACTION を ON に設定することで開始されたトランザクションで実行されます。
-
参加済み。セッションが、別のセッションまたは異なるトランザクション マネージャによって所有されているトランザクションに参加しています。別のセッションによって所有されているトランザクションへの参加は、バインド セッション (sp_getbindtoken と sp_bindsession) を使用して実現され、異なるトランザクション マネージャによって所有されているトランザクションへの参加は、DTC トランザクションへの参加によって実現されます。
MARS が使用できなかったので、特定の時点で複数のステートメントを同じトランザクションで実行することはできませんでした。バインド セッションや DTC の場合でも、基になるインフラストラクチャでは、1 度に 1 つのセッションのトランザクション コンテキストでしか、処理を行うことができません。
ODBC や OLEDB/ADO では、トランザクションがセッションで開始されたら、その後のすべての要求は、セッション全体のトランザクション コンテキストのように見える 1 つのトランザクションで実行されます。
SqlClient では、モデルが直感的に理解しにくいように思われます。新しく作成されたトランザクションを表す抽象型 (SqlTransaction) を返す接続 (SqlConnection) オブジェクトからトランザクションを開始するための API が用意されています。一見無謀に思えますが、トランザクションを作成したら、要求を明示的にそのトランザクション コンテキストに関連付けない限り、その要求を実行することはできません。SqlClient API は、トランザクションのスコープが接続に対して必ずしもグローバルに設定されるわけではありません。特定のセッションで複数のトランザクションが作成されることがあり、要求を任意のアクティブなトランザクションに自由にマップできるようなプログラミング モデル向けに用意されています。SQL Server 2005 では、複数のアクティブなトランザクションを接続ごとにサポートすることはありませんが、プログラミング モデルは既にそのような将来の拡張に対応しています。
MARS では、同じトランザクションで実行されている要求間で発生する競合に対して適切なセマンティクスを定義する必要がある特定のセッションで、複数の要求を保留状態にできます。
同様に、SQL Server 2000 では、要求によって行われた実行環境への任意の変更が、セッション全体の変更であるように見えます。ここでいう実行環境とは具体的に何を意味するでしょうか。実行環境は、SET オプションの値 (ARITHABORT など)、現在のデータベース コンテキスト、実行状態変数 (@@error など)、カーソル、および一時テーブルで構成されます。
要求内で USE ステートメントを実行して現在のデータベースを変更すると、その後のすべての要求が、新しいコンテキストで実行されます。同様に、バッチ内で SET オプションの値を変更すると、その後のすべての実行が新しく設定された値で実行されます。
MARS により、特定のセッションでは多くても 1 つの要求しか保留状態にできないという前提が取り除かれます。また、旧バージョンとの互換性を維持しながら、実行環境の変更に対してよりきめ細かいセマンティクスが定義されます。
複数のアクティブな結果セット - MARS
現段階では、MARS について漠然としか理解していないかもしれません。簡単に言えば、MARS は、特定の SQL Server 接続で保留中の要求を複数保持するための機能です。ほとんどの場合、他の操作を同じセッション内で実行中に、未解決の既定の結果セット (firehose カーソル) を複数保持できる機能、と言い換えても問題ありません。
MARS が対応していないことについて、次のように境界を定めることも重要です。
-
並列実行。MARS では、同じ接続で複数の要求を送信できますが、このことは要求がサーバー内部で並列実行されることを意味するわけではありません。MARS では、適切に定義された時点でインタリーブしながら、接続内の未処理の要求間で実行スレッドを多重化します。
-
カーソルの代替。前半で説明したように、カーソルが MARS の欠如に対する適切な回避策を示すシナリオがいくつかあります。これらのシナリオで MARS を使用することが有効な場合があります。ただし、現在使用しているすべてのカーソルを MARS に移行する必要があることを意味しているわけではありません。
既定では、SQL Server 2005 サーバーに対して MARS 対応のクライアント ドライバを使用すると、前述のコードはすべて "そのまま機能" します。また、アプリケーションが応答を停止する上記のデッドロック シナリオも、MARS 対応の接続では成功するようになります。
MARS 対応のクライアント ドライバは次のとおりです。
-
SQL ネイティブ クライアントに含まれている SQLODBC ドライバ
-
SQL ネイティブ クライアントに含まれている SQLOLEDB ドライバ
-
Microsoft .NET Framework Version 2.0 に含まれている SqlClient .Net Data Provider
既定では、上記のドライバによって MARS 対応の接続が確立されます。何らかの理由で、ダウン レベルのドライバの動作を公開する接続を確立する必要がある場合は、各 API によって、MARS 以外の接続を要求するオプションが提供されます。
SqlClient には、MultipleActiveResultSets 接続文字列オプションが用意されています。false に設定した場合、MARS はそのセッションに対して無効になります。true に設定するか省略した場合は、MARS が有効になります。
同様に、ODBC には SQL_COPT_SS_MARS_ENABLED 接続オプションが用意されており、OLEDB には SSPROP_INIT_MARSCONNECTION オプションが用意されています。MARS は既定で有効になっているので、これらのオプションも MARS を無効にする場合だけ必要になります。
注意 MARS は、SQL ネイティブ クライアント バージョンの ODBC と OLEDB プロバイダでのみ使用できます。これらのプロバイダの以前のバージョンは、MARS をサポートするように拡張されていません。言うまでもなく、SQL Server 2000 以前のサーバーに接続しているときは、新しいドライバでも MARS をサポートできません。
インタリーブ実行
MARS は、最も詳細なレベルで、1 つの接続内の複数の要求のインタリーブ実行を可能にします。つまり、バッチの実行を可能にし、その実行内で他の要求を実行できるようにします。ただし、MARS は並列実行という用語ではなく、インタリーブという用語で定義されている点に注意してください。
MARS インフラストラクチャでは、複数のバッチをインタリーブ方式で実行できます。ただし、実行は適切に定義された時点でしか切り替えることができません。実際には、大部分のステートメントは、1 つのバッチ内で 1 つの単位として実行される必要があります。完了前に実行をインタリーブできるのは、次のステートメントだけです。
-
SELECT
-
FETCH
-
READTEXT
-
RECEIVE
-
BULK INSERT (または bcp インターフェイス)
-
非同期カーソルによるデータ設定
このことは、具体的に何を意味するのでしょうか。これは、ストアド プロシージャまたはバッチの一部として実行される、この一覧に記載されていないその他のステートメントは、実行を完了するまで他の MARS 要求の実行に切り替えることができないことを意味しています。
例として、数十万ものレコードに影響する UPDATE ステートメントなど、実行時間の長い DML ステートメントを送信するバッチがあるとします。このステートメントの実行中に 2 番目のバッチが送信された場合、このバッチは UPDATE ステートメントが完了するまで実行されません。
一方、SELECT ステートメントが最初に送信された場合は、SELECT ステートメントの途中で UPDATE ステートメントを実行できます。ただし、DML 操作が完了するまで、SELECT ステートメントに対して新しい行は作成されません。
この例も MARS によって要求が "インタリーブ" されることを示しており、"並列処理" されることを示しているわけではありません。インタリーブは、要求のステートメントが、バッチ、EXEC ステートメント、ストアド プロシージャのどれに含まれていても影響を受けません。
注意 RECEIVE ステートメントは、行の生成が開始されたら、インタリーブできます。WAITFOR 句内で実行される RECEIVE ステートメントの場合、ステートメントが待機状態の間はステートメントをインタリーブできません。
注意 一括操作をインタリーブできるのは、SET XACT_ABORT を ON に設定して実行されている場合と、トリガが挿入操作の対象となるテーブルで定義されていないか、トリガを実行しないようにするオプションが指定されている場合だけです。また、RECEIVE をインタリーブできるのは、XACT_ABORT が ON に設定されている場合だけです。
注意 任意の .Net 言語で記述されたストアド プロシージャでは、マネージ コードの実行中はインタリーブを実行しません。inproc データ プロバイダを使用している場合、実行されるバッチには、ステートメントのインタリーブと 1 単位としての実行に関して、通常のルールが適用されます。
MARS のパフォーマンスとコストに関する考慮事項
前半で説明したように、MARS は、SQL Server データ アクセス API の既定の処理モードです。MARS の実行モデルでは、サーバー側のカーソルとは異なり、大きなステートメント バッチがサポートされています。また、ストアド プロシージャの呼び出しと動的 SQL 操作も共にサポートされる場合があります。結果が生成される "firehose" モードの場合、既定の結果セット (MARS) のパフォーマンスは、サーバー側のカーソルのパフォーマンスよりも優れています。
ただし、例外も多少あります。既定の結果セットでは、結果が "可能な限り迅速に" 生成されます。ただし、このことは、生成された結果がクライアント ドライバまたはアプリケーションで使用されている間のみ当てはまります。アプリケーションで結果が使用されていない場合は、サーバー側のバッファがいっぱいになり、結果が使用されるまで処理が中断されます。実行が中断されている間は、多くのリソースがその中断によって拘束されます。つまり、データとスキーマのロックが保持され、スタックや他の関連付けられたメモリを含む、サーバーのワーカー スレッドが拘束されます。この条件は、MARS 固有のものではないという点に注意してください。つまり、1 つの要求によって生成された既定の結果セットがしばらく使用されないと、SQL Server 2000 以前のバージョンで発生したのと同じオーバーヘッドが生じます。MARS では、firehose カーソルのオーバーヘッドは削減されません。
このリソースの拘束は、サーバー側のカーソルの場合は発生しません。多少関係しますが、要求するカーソルの種類によっては、既定の結果セットには存在しない新しいセマンティクスを使用できることがあります。つまり、結果のスクロール可能性と更新可能性です。
要求の処理方法に関する説明が提供されていれば、SQL Server から結果を取得するための使用方法に関する指針を推測しやすくなります (スクロール可能性と更新可能性は不要であると仮定します)。つまり、アプリケーションによって結果が積極的に使用される場合は、MARS に基づく既定の結果セットから、最適なパフォーマンスとオーバーヘッドという特性がもたらされます。アプリケーションによって結果があまり積極的に使用されない場合は、サーバー側のカーソル、特に FAST_FORWARD カーソルを使用することをお勧めします。
ほとんどの場合、MARS の既定の結果セットを使用することが適切です。では、結果があまり積極的に使用されないのはどのような場合でしょうか。結果を返すバッチまたはストアド プロシージャを実行するアプリケーションを想像してください。行の使用は、ユーザー入力、UI 操作、他のタスクとの同期など、データベースの外部で行われる操作の完了に依存します。一般的に、長期間保留状態の要求を保持していると、アプリケーションと SQL Server の拡張性に影響します。
トランザクションのセマンティクス
MARS の導入により、トランザクションのセマンティクスや 1 つのトランザクション内での操作の同時実行に関して、データベース エンジン内部の既存の想定の多くが変化しました。
OLEDB は、トランザクションがセッション内でアクティブなときは、常に接続の暗黙の作成を許可せず、ODBC は、"接続ビジー" エラーで追加要求に失敗していました。MARS 対応の環境ではこれらの組み合わせが成功するようになりました。セッションにアクティブなトランザクションがある場合は、すべての新しい要求が指定されたトランザクションで実行されます。一方、セッションにアクティブなトランザクションがない場合は、各バッチが自動コミット モードで実行されます。これは、実行される各ステートメントが独自のトランザクションで実行されることを意味しています。
SqlClient マネージ プロバイダのモデルはより明確です。特定の SqlCommand は、特定の要求を実行するトランザクションを指定するために、特定の SqlTransaction オブジェクトに関連付ける必要があります。
一般的には、トランザクションにより、複数のユーザー間の分離が決定されます。ただし、MARS では、複数の要求を同じトランザクションで実行できます。このトランザクションによって、要求が相互に互換性を持つようになり、「概要」で説明したデッドロックが発生しなくなります。では、同じトランザクションの 2 つの要求間で操作が競合している場合、どのような現象が発生するでしょうか。
次に説明するように、いくつかの事例が考えられます。
-
1 つの要求で、一部の結果が読み取られています (SELECT、FETCH、READTEXT など)。そのとき、別の要求で、読み取り中のデータが変更されます (DML 操作など)。この場合、変更操作は成功しますが、読み取り操作は変更とは独立して実行されるので、読み取られるすべてのデータは読み取り操作を開始した時点のものとなります。この事例は、読み取り操作が変更バッチの前に開始された場合のみ有効であることに注意してください。DML ステートメントの実行が最初に開始された場合、読み取り操作はそれに続いて実行されるため、行われたすべての変更が反映されます。
-
2 つの要求で、同じデータの変更が試みられます。ステートメントの原子性規則により、常に DML ステートメントの実行が完了しないと、他のステートメントを実行できるようになりません。そのため、データを変更しようとしている 2 つのバッチはインタリーブされません。要求は直列に実行され、実行順序が結果に反映されます。クライアント アプリケーションがマルチスレッド化されている場合は、常に同じ動作が行われるとは限らないことに注意してください。
-
要求によってデータが読み取られ (SELECT、FETCH、READTEXT など) ているときに、その基になる一部のオブジェクトのスキーマが変更されます (DDL 操作など)。この場合、同じトランザクションで保留中の要求が競合していると、DDL 操作は失敗します。また、この動作は、RECEIVE ステートメントによる結果の生成中に、サービス ブローカー キューのスキーマを変更する場合にも当てはまります。
-
一括挿入の対象となっているテーブルで、操作が重複します。BULK INSERT (または bcp、IRowsetFastLoad) を 1 つの単位としてではなく実行できます。つまり他のステートメントとのインタリーブが許可されます。ただし、DDL、DML、または読み取り操作は、BULK INSERT の対象となるオブジェクトでは同時に実行できません。このような場合、同じトランザクションで保留中の要求が競合すると、エラーが生成されます。
上記の事例は、同じトランザクションで実行される要求にだけ当てはまることに留意してください。別々のトランザクションで実行される要求の場合は、通常のロック、ブロック、または分離のセマンティクスが適用されます。
MARS で実行されるトランザクションのセマンティクスは、バインド セッションや DTC でも使用されるようになったトランザクション フレームワークによって実装されます。これは、以前は保留中の要求がない場合にのみ、セッション間のトランザクション コンテキストを変更できましたが、現在は 1 つの単位でなくても実行できる同じセットのステートメント間でコンテキストを切り替えられるようになったことを意味します。同様に、トランザクション コンテキストは、DML、DDL、および 1 つの単位として実行する必要があるその他のステートメントの実行中は切り替えることができません。
注意 特定のトランザクションに保留中の要求がある場合は、トランザクションをコミットできません。
セーブポイント
一般的に、トランザクションのセーブポイントは、トランザクション内の部分的なロールバックを可能にするために使用します。通常アプリケーションでは、トランザクションを開始し、セーブポイントを設定して、何らかの処理を行います。処理が成功した場合は続行され、失敗した場合はセーブポイントにロールバックされます。次の例は、同じトランザクションで実行される、セーブポイントを持つ 2 つの要求の相互作用を示しています。
表 2. トランザクションのセーブポイント
|
時刻 |
バッチ 1 |
バッチ 2 |
|---|---|---|
|
T1 |
トランザクションを開始します。 |
|
|
T2 |
operation_id=5 の dbo.operations を削除します。 |
|
|
T3 |
|
トランザクション sp1 を保存します。 |
|
T4 |
|
dbo.operations の既定値を挿入します。 |
|
T5 |
operation_id=10 の dbo.operations を削除します。 |
|
|
T6 |
|
dbo.operations の既定値を挿入します。 |
|
T7 |
|
@@error>0 の場合、 トランザクション sp1 をロールバックします。 |
|
... |
|
|
|
Tn |
トランザクションをコミットします。 |
|
上記の例では、最初の要求によってトランザクションが開始され、処理 (行の削除) が行われます。その後、バッチ 2 の実行が (同じトランザクションで) 開始され、指定された一連のステートメントがトランザクション内で 1 つの単位としてすべて成功するかすべて失敗することを保証するために、セーブポイントが設定されます。
ただし、バッチ 2 によって実行される 2 つのステートメント内で、バッチ 1 の削除操作がインタリーブされます。バッチ 2 でエラーが発生したと仮定すると、要求では、セーブポイント sp1 へのロールバックを試行します。ただし、この場合、バッチ 1 によって T5 で実行される削除操作も "暗黙に" ロールバックされます。
この予測不可能でデバッグが困難な状況を回避するために、1 つのトランザクションで複数のアクティブな要求が実行されている場合、MARS では、セーブポイントの設定、セーブポイントへのロールバック、およびトランザクションのコミットを許可しません。上記の 2 つの要求操作は、直列に実行されれば成功しましたが、上記のように、特定の同時実行要求がインタリーブされている場合は、バッチ 2 のセーブポイントの設定操作がエラーで失敗します。
v実行環境
上記で説明したように、実行環境は 1 つのセッション全体にまたがるグローバル環境のように見えます。MARS では、複数の要求によって環境が同時に変更された場合、どのような現象が発生するでしょうか? 次に例を示します。
表 3. 複数の要求が同時に実行される例
|
時刻 |
バッチ 1 |
バッチ 2 |
バッチ 3 |
|---|---|---|---|
|
T1 |
operations を使用します。 |
|
|
|
T2 |
|
msdb を使用します。 |
|
|
T3 |
dbo.operations から operation_id を選択します。 |
sys.objects から名前を選択します。 |
|
|
... |
|
|
|
|
Tn |
|
|
sys.objects から名前を選択します。 |
上記の例は、同じ接続で実行されている 3 つのバッチを示しています。バッチ 1 と 2 では、データベース コンテキストを変更してから SELECT ステートメントを実行します。バッチ 3 では、しばらくしてから、データベース コンテキストを指定せずに SELECT ステートメントを実行します。実行環境の状態が接続に対してグローバルだとすると、上記の組み合わせの結果は、アプリケーション開発にとって非常にわかりにくく、予測不可能です。
MARS には、要求レベルの実行環境とセッション レベルの既定の実行環境があります。要求の実行が開始されると、セッション レベルの環境が複製されて要求レベルの環境になります。バッチ全体が完了すると、その時点の環境がセッション レベルの既定の実行環境にコピーされます。
このセマンティクスでは、一連の直列化された要求 (SQL Server 2000 で唯一許可されている動作) によって、1 つのセッションのグローバルな実行環境が存在するように見えます。しかし、MARS 要求を同時に実行した場合、ある要求によって行われた変更が、同時に実行される他の要求に影響することはありません。
上記の例では、セッション環境がバッチ 1 とバッチ 2 にそれぞれコピーされ、各バッチの SELECT ステートメントが目的のデータベース コンテキストで実行されます。完了すると、各バッチによって、セッション コンテキストがコピー (および上書き) されます。この場合、セッションの結果として生成されるデータベースは、バッチ 1 とバッチ 2 が完了するタイミングによって異なることに注意してください。バッチ 1 とバッチ 2 の完了後にバッチ 3 によって実行が開始されると仮定した場合、返される結果は、最初の 2 つの要求のタイミングによって、'operations' データベースまたは 'msdb' データベースのいずれかになります。
MARS に基づいて複数のバッチに対してプログラミングを行う際は、セマンティクスをコピーするコンテキストに留意してください。コンテキストのコピーには、SET オプションと残りの実行環境が含まれます。
注意 バッチをキャンセルした場合、実行環境は、キャンセル要求が認識された時点の状態で、既定のセッションにコピーされます。
MARS のデッドロック
MARS によっていくつかの新しいシナリオが可能になりますが、強力な機能を備えた場合によくあるように、不注意で自ら問題を発生させる状況も新たに出てきます。次の例について考えてみましょう。
従来、DML ステートメントではトリガが許可されていました。SQL Server 2005 では、トリガを DDL ステートメントで定義できるようにモデルが拡張されています。ここでは、結果を必ずトリガ内から呼び出し側に返すアプリケーションについて考えます (もちろん、このような違和感のあるコードを実行することはありません)。SELECT ステートメントは、トリガ本文定義の中に含まれています。アプリケーションの観点から見た擬似コードは、次のようになります。
Request 1: update table operations; // this will return results from the trigger.
For each row returned from the trigger
{
Request 2: Read from some other table based on current row;
}
上記の例では、新しい種類のアプリケーション デッドロックが生じました。要求 2 の実行は、要求 1 から 1 行返されるごとに 1 回試行されます。ただし、要求 1 は DML ステートメントなので、その他のステートメントの実行を許可する前に完了する必要があります。同じことが DDL ステートメントにも当てはまります。この場合、要求 2 は、要求 1 が完了するまで実行されません。ただし、要求 1 の完了は、それぞれの要求 2 の実行に依存します。
MARS では、ネットワークのブロック操作をデッドロックの検出チェーンに追加することで、この問題を解決しています。上記のシナリオの場合、SQL Server のデッドロック モニタによって状況が検出され、セッションが別の要求でビジー状態になっていることを示すエラーで、要求 2 が失敗します。
一般的な規則として、1 つの単位として実行する必要があるステートメントに留意し、他の操作によってステートメントの進行が妨げられないようにしてください。さらに重要なことは、このステートメントが、最初のステートメントの完了に依存する操作によって妨げられないようにすることです。
トリガから結果を返すことは、このような状況が発生しやすい処理の 1 つです。この理由や他のいくつかの理由により、トリガから結果を返さないことを強くお勧めします。代入句が指定されていない SELECT ステートメントや FETCH ステートメント、PRINT ステートメント、または NOCOUNT を OFF に設定して実行されているその他のステートメントも、このような状況が発生しやすい処理です。
監視と診断
ここまで見てきたように、MARS によって、SQL Server エンジン内部の中核となる前提条件の一部が変化しました。SQL Server インスタンスを監視および診断する際、一部の新しい前提条件について留意することは重要です。
SQL Server プロセス ID (SPID) は、SQL Server のセッションを表しています。以前のリリースには MARS が存在しなかったので、SPID と要求を関連付けることは一般的でした。また、特定の SPID の SQL テキストを取得することについて考えることも一般的でした。さらに、SPID の sysprocess で実行統計を確認することも一般的でした。MARS によって有効になったシナリオを考えると、これらの条件だけでは不十分な場合があります。
sysprocess によってセッション情報は引き続き表示されますが、MARS の監視に役立ついくつかの拡張機能も用意されています。
新しい動的管理ビュー (DMV) の sys.dm_exec_sessions により、セッションの既定の実行環境など、セッション情報の新しいビューが提供されます。このビューでは、従来 SPID と呼ばれていたものが session_id 列に反映されます。
また、sys.dm_exec_connections を使用して、サーバーに対して確立されたすべての物理接続と論理接続を表示できます。論理接続は、MARS に基づいて実行される要求ごとに確立されたセッション内の仮想パイプです。論理接続に対しては、parent_connection_id column 列が作成されます。また、一般的な session_id 列は、1 つのセッション内の複数の論理接続の関係を示します。
新しい DMV の sys.dm_exec_requests により、各セッションで使用できる要求の詳細な一覧が提供されます。
また、新しく組み込み関数の current_request_id() が導入され、現在実行されている要求の ID をプログラムから参照できるようになりました。この関数は、既存の @@spid 関数に似ています。
まとめ
SQL Server 2005 の複数のアクティブな結果セット (MARS) に対するサポートにより、SQL Server アプリケーションの開発に使用できるオプションが増加します。これにより、カーソルを使用するプログラミング モデルと、リレーショナル エンジンの既定の処理モードのパフォーマンスや能力との結び付きがより緊密になります。
MARS により、複数の接続を使用して MARS の欠如による制限を克服している一部のアプリケーションに、より負荷の少ない代替手段が提供されます。ただし、複数の接続を使用するとサーバーで並列実行が行われる (それらの接続が同じトランザクションに参加していない場合) ため、このことが常に当てはまるわけではありません。
多くの場合、MARS によって、サーバー側のカーソルに対する代替手段が提供され、パフォーマンスが向上しますが、MARS はカーソルの代わりではありません。この資料で説明したように、MARS が優れた代替手段となる場合もありますが、カーソルの方が優れている場合も数多くあります。
簡単に言うと、MARS はサーバーで複数の要求をインタリーブできるようにする、プログラミング モデルの拡張機能です。MARS によって、サーバーで並列実行が行われるわけではありませんが、正しく使用すればパフォーマンスが向上する場合があります。
1 この資料では、DB-Lib、または ODBC や OLEDB の上位層にあたる .Net マネージ プロバイダについては取り上げません。