XML データ型のパフォーマンスの最適化
Shankar Pal、Vasili Zolotov、Leo Giakoumakis 共著
Microsoft Corporation
June 2004
対象 :
Microsoft SQL Server 2005
概要 : この資料では、次期バージョンの Microsoft SQL Server 2005 の XML データ型のクエリとデータ変更のパフォーマンスを向上させるためのいくつかの考え方を扱っています。この資料を最大限に活用するには、SQL Server 2005 の XML 機能について十分に理解しておく必要があります。背景資料については「Microsoft SQL Server 2005 での XML のベスト プラクティス」 を参照してください。
目次
はじめに
XML データ型によるデータ モデリング
クエリとデータ変更
はじめに
Microsoft SQL Server 2005 では、XML データの処理が幅広くサポートされます。 XML データは XML データ型の列にネイティブに保存されます。XML データは、XML スキーマのコレクションに従って型指定することも、型指定しないでおくこともできます。 XQuery と XML データ変更言語を使用することで、きめ細かいデータ操作がサポートされています。XQuery は、現時点では最終審査請求中の開発段階にある W3C 標準クエリ言語です。 XML 列にインデックスを設定して、クエリ パフォーマンスを向上させることができます。 半構造化されたデータや構造化されていないデータのモデリングに XML を使用するエンタープライズ アプリケーションが増えてきているので、このようなエンタープライズ アプリケーションには、SQL Server 2005 の XML サポートが役立つことでしょう。
この資料では、ストレージ、クエリ、XML データ型のデータ変更の最適化に関する提案を行っています。この提案は、コード サンプルを使って例示されています。また、関連資料「Microsoft SQL Server 2005 での XML のベスト プラクティス」 では、XML データのモデリングと用途に関するベスト プラクティスを紹介しています。 マッピングを使用した XML ビュー テクノロジの最適化については、MSDN ライブラリの「SQLXML パフォーマンスの最適化」を参照してください。
この資料の残りの部分では、データベースのデザイン原理を含めて、XML を使用したデータ モデリングのガイドラインを検討し、パフォーマンスを最適化するためのクエリとデータ変更のガイドラインを示します。
XML データ型によるデータ モデリング
XML データ型は、企業内の半構造化されたデータや構造化されていないデータにデータ モデリング機能を提供します。XML ストレージやクエリ処理のパフォーマンスは、データベースのデザインに依存し、XML データの粒度や XML 列からのプロパティの昇格などの要素が影響します。
まず、アプリケーションに XML データ モデルの機能が必要かどうかを決めます。構造化されたデータは、リレーショナルとしてモデル化されるのが最適なので、リレーショナル列を備えたテーブルに格納されます。 データでドキュメントの順序とコンテナ階層を保持する必要がある場合は、XML データ モデルが最適で、おそらく再帰的な構造を持つことになります。
XML データ型の列に、構造化されたデータを格納することが便利な場合があります。たとえば、データが柔軟な構造を備えている場合や、データの構造が "事前" にはわからない場合などです。
このようなアプリケーションの 1 つがプロパティ管理です。プロパティ管理では、オブジェクトのメタデータ情報が XML としてモデル化され、XML データ型の列に格納されます。最も頻繁にクエリされるプロパティは、同一テーブルまたは異なるテーブルの列に昇格されます。 昇格されたプロパティにインデックスを設定してクエリを実行できるので、XML 列をクエリするよりも単純なクエリ プランになります。
また、入力される XML データはテーブルに分解され、SQL 言語を使用してクエリできます。XML の生成がクエリのワークロードに大きな影響を与える場合は、XML データを余分にコピーして XML データ型の列に格納することが役に立つことがあります。この冗長コピーにより、XML の生成にかかる実行時のコストが回避されます。
XML データ型のデータのモデリングに絶対的なルールはないので、モデリングのさまざまな状況に合わせて利点と欠点を注意深く比較する必要があります。 型指定された XML 列と型指定されていない XML 列のどちらを使用するかを選択することや、XML マークアップをデータに取り入れる方式も同様に重要です。 以下では、このような点とその他いくつかの考慮事項について説明します。
XML データの構造
同一データを異なる方式 (要素中心、属性中心、これら 2 つの組み合わせなど) でマークアップできます。どの方式を選択するかは、コンテンツの構成要素 (要素値)、メタ情報の構成要素 (属性値)、およびカーディナリティ (複数出現する要素数) をどう認識するかによって左右されます。 半構造化されたデータや構造化されていないデータに、ある方式で XML マークアップを取り入れることにより、ストレージ処理やクエリ処理をより効率的に行えるようになります。
具体的なマークアップを使用する
汎用的な要素名を使用し、異なる種類の要素は属性を追加して区別すると便利なこともあります。 しかし、XML インデックス照合を効率的に実行できないので、この方式はクエリするには不向きです (XML へのインデックス設定の詳細については、「XML データへのインデックス設定」を参照してください)。
一方、具体的に意味のある要素名からは、人間が判読しやすいマークアップを生成でき、より効率的なクエリ プランの生成に役立ちます。 あまり細かすぎるマークアップは、ストレージのコストが増加します。これらの点を次の例で示します。
例 : 汎用的なマークアップと具体的なマークアップ
書籍と DVD の情報に関して XML マークアップを使用するとします。<item> という汎用な要素を使用し、この要素の @type 属性に "book" と "DVD" という 2 つの値のいずれかを指定することにより 2 種類の項目を区別する場合は、書籍と DVD を次のように表現できます。
<item type="book"><title>Writing Secure Code</title></item>
<item type="DVD"><title>The Godfather</title></item>
書籍と DVD のパス式は、それぞれ /item[@type = "book"]、/item[@type = "DVD"] と記述できます。
一方、<book> や <DVD> の方がより直接的な XML マークアップになります。
<book><title>Writing Secure Code</title></book>
<DVD><title>The Godfather</title></DVD>
この表記では、簡単に /book や /DVD というパス式になります。@type 属性に関する述語がなくなるので、クエリ プランもより簡単で効率的になります。
さらに、プライマリ XML インデックスの行数が 4 (<item> に 1 行、@type とその値に 1 行、<title> に 1 行、title の値に 1 行) から 3 (<book> または <dvd> に 1 行、<title> に 1 行、title の値に 1 行) に減ります。
属性中心のマークアップ
属性の値は属性のマークアップと共に 1 行のプライマリ XML インデックスに格納されます。これに対し、型指定されていない XML では、単純な値要素の値が要素のマークアップとは別の行に格納されます。したがって、型指定されていない XML の内部で属性値を使用して、ストレージをより少なくする必要があります。
属性を使用すると、属性の値が属性のマークアップと同じプライマリ XML インデックス内の行から取得されるので JOIN を使用せずに済み、述語の評価がより効率的になります。このことを、次の例で示します。
例 : 属性中心のマークアップ
上記の例の title を、次に示すように要素ではなく属性としてモデル化できます。
<book title="Writing Secure Code"/>
<DVD title="The Godfather"/>
書籍の場合、プライマリ XML インデックスの行数が 3 (<book>、<title>、title の値にそれぞれ 1 行) から 2 (<book> に 1 行、属性 @title に 1 行) に減ります。DVD の場合も同様です。
title のマークアップが要素中心の場合は、パス式 /dvd[title = "The Godfather"] でタイトル「The Godfather」の DVD が検索されます。属性中心のマークアップでは、同じクエリを /dvd[@title = "The Godfather"] と記述できますが、必要な JOIN が 1 つ減ります。
型指定された XML か型指定されていない XML か
型指定されていない XML (つまり、XML スキーマによって記述されていない XML データ) の要素と属性は、内部的には Unicode 文字列として格納されます。型指定されていない XML の要素と属性に対する操作では、データを適切な型へ変換する必要があります。たとえば、パス式 (/book/price)[1] > 19.99 を評価するときは、<price> を文字列値から、数値比較ができるように 10 進数に変換します。このような比較が大量になると、コストが増加します。
XML スキーマから提供される型情報は、データベース エンジンによってさまざまな方法で使用されます。挿入される XML データや更新される XML データは、XML スキーマに準拠していることが確認されます。要素値と属性値は型指定された値として XML インスタンスに格納されるので、テキスト形式で格納されるよりもデータを効率的に解析できます。型指定されたデータも、同様に XML インデックスに格納されます。クエリのコンパイルでは型情報を使用して、XQuery 式の静的型とデータ変更ステートメントが適切であることが確認されます。
また、型の推測に基づいてクエリが最適化されます (たとえば、<book> の <price> が xs:decimal 型の場合、(/book/price)[1] から xs:decimal への変換が実行されなくなります)。これにより、XML インデックス照合の効率が向上することがあります。 (/book/price)[1] < 19.99 などの範囲述語では、VALUE 型のセカンダリ XML インデックスに対して範囲スキャンを実行します (XML データへのインデックス設定の詳細については、「XML データへのインデックス設定」を参照してください)。型指定されていない XML ではデータの変換が必要なので、このような範囲スキャンは実行できません。
プロパティの昇格
XML データ モデルでは、ドキュメントの順序や階層などの構造情報が、クエリ処理中も保持されます。したがって、クエリ プランは複雑になる傾向があります。スカラ値を XML 列から同一テーブルまたは別のテーブルのリレーショナル列に昇格させることにより、プランが簡単になるクエリもあります。昇格したプロパティにはインデックスを設定できます。プロパティ値を具体化したり、プロパティ値にインデックスを設定すると、通常、XML 列で XQuery を使用するよりもパフォーマンスが向上します。
プロパティ値を取得する場合、または対応する XML blob を取得するためのフィルタとしてプロパティ値を使用する場合、プロパティを昇格するとパフォーマンスが向上します。後者の場合、プロパティ値の選択性が重要な要因になります。
単一値プロパティは、計算列と同じテーブルの列に昇格できます。 単一値プロパティと複数値プロパティは、どちらも、別のテーブルの列に昇格でき、トリガを使用して管理できます。このような 2 つのプロパティの昇格方法を以下で検討します。
計算列の使用
まず、T-SQL ユーザー定義関数 (UDF) を作成し、XML データ型のメソッドを使用してスカラ値を抽出します。次に、UDF で定義される計算列をテーブルに追加します。この 2 つの手順を、昇格されるプロパティごとに繰り返して、必要に応じてそれらの列にリレーショナル インデックスを作成します。
この計算列を使用するように、XML 列の XQuery 式を書き直す必要があります。該当する行については、XML インスタンスを取得します。コストに基づくオプティマイザによって、計算列のインデックスが必要に応じて選択されます。昇格されるプロパティは事前に計算されているので、通常、XML 列を直接クエリするよりもパフォーマンスが向上します。
計算列を SELECT リストだけで使用し、述語の評価には使用しない場合は、計算列にインデックスを設定しないことも可能です。 このような場合は、計算列の永続性だけで十分です。 計算列にインデックスを設定するときは、計算列の式が不正確または不明確な場合は、計算列を永続化する必要があります。
プロパティの昇格における計算列の使用方法を次の例で示します。
例 : プロパティの昇格における計算列の使用
通常のワークロードを考えると、ISBN 番号を指定して書籍を検索する場合、ISBN 番号を計算列に昇格することは無駄ではありません。ISBN 番号を取得するユーザー定義関数を以下のように定義します。
CREATE FUNCTION udf_get_book_ISBN (@xData xml) RETURNS varchar(20)
WITH SCHEMABINDING
BEGIN
RETURN @xData.value('/book[1]/@ISBN', 'varchar(20)')
END
計算列を ISBN のテーブルに追加します。
ALTER TABLE DOCS ADD ISBN AS dbo.udf_get_book_ISBN(xCol)
ISBN 列に非クラスタ化インデックスを作成します。
CREATE INDEX COMPUTED_IDX ON DOCS (ISBN)
以下のクエリを、
SELECT xCol
FROM docs
WHERE xCol.exist ('/book[@ISBN = "0-2016-3361-2"]') = 1
以下のように、計算列を使用するように書き直します。
SELECT xCol
FROM docs
WHERE ISBN = '0-2016-3361-2'
書き直されたクエリでは、抽出される ISBN は事前に計算されているので、より単純なクエリ プランが生成されます。
プロパティ テーブルを使用する
プロパティ テーブルを独立させる場合は、プロパティ テーブルの保守のために、トリガの挿入、削除、および更新を設定する必要があります。 このようなプロパティ テーブルは複数値プロパティに適しており、プロパティ テーブル内の各行に 1 つのプロパティ値 ("ピボットされていない" 表記) が格納されます。プロパティ テーブルの作成と管理を示す例については、「Microsoft SQL Server 2005 での XML のベスト プラクティス」で参照できます。
プロパティ テーブルで兄弟の相対順序を維持する必要がある場合は、シーケンス番号が適しています。ただし、シーケンス番号を使用すると、XML サブツリーを挿入したり削除したりする場合に、プロパティ テーブルの管理が複雑になります。
便宜上、単一値プロパティ列をテーブルに追加することもできます。追加することで列が冗長になりますが、両方のプロパティを必要とするときに JOIN を使用せずに済みます。
昇格されたプロパティの最大カーディナリティ N が事前にわかっている場合は、独立したプロパティ テーブルではなく、N 個の計算列を作成して、クエリ プロセッサでそれらの列を管理することが役立ちます。
データベースの照合順序
XML マークアップでは大文字と小文字が区別されますが、XML データ型のメソッドでの文字列比較はデータベースの照合順序に依存します。データベースの既定の照合順序では大文字と小文字が区別されませんが、これはインストール中、および COLLATE オプションを指定して ALTER DATABASE ステートメントを使用することでインストール後に変更できます。サーバー全体の照合順序は、sp_configure を使用して変更できます。どのような場合にこれを変更するのが効果的かについては、Books Online を参照してください。
XML クエリとデータ変更ステートメントで、大文字と小文字を区別して比較することが適している場合は、値をバイナリ データ型 (たとえば、varbinary(max)) として取得し、その後、検索値とバイナリ比較を実行する必要があります。 大文字と小文字を区別する比較がデータベースで実行されるので、データベースのバイナリ照合順序を使用することが適切かつ高速な方法です。 バイナリ照合順序を選択するオプションがない場合、そのデータベースで、大文字と小文字を区別する照合順序を選択します。
XML データの一括読み込み
SQL Server の一括読み込み機能を使用して、XML データを XML データ型の列に一括で読み込めます。一括読み込み機能には、BCP IN メソッド、BULK INSERT メソッド、および OPENROWSET メソッドがあります。
BCP IN は、できる限り XML データの中間コピーを生成しないように最適化されています。したがって、BCP IN は、XML 列に (列や行の) 制約が存在しない場合に、3 つのメソッドの中で最適なパフォーマンスを発揮します。
XML データへのインデックス設定
XML インデックス
XML 列にきめ細かくクエリする場合は、XML 列にプライマリ XML インデックスを作成することをお勧めします。プライマリ XML インデックスは型指定されていない XML 列と型指定された XML 列の両方に作成できます。また、XML 列全体にわたり、すべてのパスと値にインデックスを設定できます。PATH 型、PROPERTY 型、および VALUE 型のセカンダリ XML インデックスを使用して、さらにアプリケーションの高速化を図れます。
- PATH インデックスは、T-SQL の WHERE 句の XML データ型の exist() メソッド内で /book[@ISBN = "0-2016-3361-2"] などのパス式を使用する場合に常に役立ちます。パス式が長いほど大きな効果が得られます。PATH インデックスでは、適切かつ全体的なスピードアップを図れます。
- PROPERTY インデックスは、SELECT ステートメントで XML インスタンスの複数のプロパティを取得する場合に役立ちます。XML インスタンスごとにプロパティをクラスタ化すると、パフォーマンスの向上が図れます。
- VALUE インデックスは、descendant-or-self 軸 (//) やワイルドカード (/book[@* = "novel"] など) を含んでいるパス式に役立ちます。
1 つ以上のセカンダリ XML インデックスが役立つかどうかを判断するには、クエリ ワークロードを分析する必要があります。インデックスの管理コストは、XML データへのインデックス設定の全体的な利点を視野に入れて考慮する必要があります。
多くのアプリケーションでは期待するクエリ ワークロードがわかっているので、クエリ内に出現するパスだけにインデックスを設定することにより、メリットが得られます。「プロパティの昇格」で説明したように、そのようなパスをプロパティとして昇格できます。
XML 列のフルテキスト インデックス
XML 列には XML インデックスとは別にフルテキスト インデックスを作成できます。 フルテキスト インデックスでは、XML マークアップと attribute 値が無視され、トークンの境界として要素タグを使用して、要素の内容にインデックスが設定されます。
XQuery 関数の fn:contains() では、意味合いとしてリテラルでのサブストリング照合が使用されますが、CONTAINS() を使用したフルテキスト検索では語幹検索を使用したトークンの照合が使用されます。したがって、その意味合いが異なります。 この違いを説明すると、"data" という単語の検索では、XQuery では "database" という単語と一致しますが、フルテキストという意味では一致しません。一方、"drove" という単語の検索では、フルテキストという意味では "driving" という単語と一致することになりますが、XQuery では一致しません。さらに、attribute 値を検索する場合はフルテキスト検索を使用できません。XQuery 式では、さまざまなコンテンツを検索する場合、集計関数 fn:string() を使用する必要があります。
フルテキスト インデックスが XML 列に存在するときは、以下の手順を実行することをお勧めします。
- フルテキスト検索を使用して、希望する XML 値をフィルタ処理します。
- 次に、XML データ型のメソッドを使用して、選択した XML インスタンスをクエリします。この手順では、XML 列の XML インデックスが使用されます。
これにより、フルテキスト インデックスと XML インデックスの両方が使用されるようになります。 最初の段階で検索語や検索フレーズのヒット率が高いと、次の段階の処理範囲が絞られ、テーブル内の行数が比較的少なくなります。これにより、クエリの処理速度を大幅に向上できます。 この方法は、検索フレーズがキーワードの境界と一致する語幹で構成される場合に使用できます。
スナップショット分離と XML インデックス
XML データの変更では、古い XML インスタンスを更新される XML インスタンスに置き換えます。このような変更は、プライマリ XML インデックスとセカンダリ XML インデックスに反映されます。ベース テーブルの変更される行と XML インデックスはロックされるので、行ロックとページ ロックはオプティマイザの判断により、テーブル ロックにエスカレートされる場合があります。特に、ワークロードで更新が共通している場合には、ロックのエスカレーションが原因で同時実行に悪影響を及ぼします。
分離に基づくスナップショットでは、"スナップショット" と呼ばれる新しい分離レベルと、コミット済みの読み取りレベルの実装が SQL Server 2005 に導入されます。これらの詳細については、Books Online で参照できます。これらのレベルは、データベースでスナップショット分離を有効にした場合に、リーダーとライタ間でロックによる競合を発生させない、内部的なバージョン管理メカニズムに基づいています。ロックによる競合を削減すれば、スループットを向上できます。
分離に基づくスナップショットでの読み取り操作では、同時実行の更新をブロックせずに、バージョンが付けられたデータにアクセスできます。このようにブロックを減らすと、同時実行ワークロードのトランザクション スループットを向上できます。
XML 列の値とそれに対応するプライマリ XML インデックス行とセカンダリ XML インデックス行は更新時にバージョンが変わります。これにより、他の列を変更したことにより、同じ行のバージョンが更新されるような不要なバージョン変更を防ぎます。スナップショット分離はこのような最適化により、XML 処理に非常に役立ちます。
クエリとデータ変更
exist() メソッドを使用して存在を確認する
パフォーマンスを向上するために、可能であれば、value() メソッドではなく、XML データ型の exist() 関数を使用します。exist() は、SQL WHERE 句で使用するのが最も効果的で、value() メソッドよりも有効に XML インデックスが使用されます。これは、XQuery 式で sql:variable() や sql:column() を使用する場合にも当てはまります。
たとえば、次のクエリを考えてみましょう。このクエリでは、exist() メソッドを使用して "Database Theory" という書名の書籍を取得します。
SELECT *
FROM docs
WHERE xCol.exist('(/book/title/text())[.="Database Theory"]') = 1
PATH セカンダリ XML インデックスまたは VALUE セカンダリ XML インデックスを使用して、値 (この例では "Database Theory") のインデックス照合を含め、パス式 (/book[title="Database Theory"]) を評価し、XML インスタンスを返します。パスと検索値を選択できれば、列内のすべての XML blob についてパス式を評価するよりもはるかに速く実行できます。
以下に示すように、value() 関数を使用してクエリを記述します。
SELECT *
FROM docs
AND xCol.value('(/book/title)[1]', 'varchar(50)') = 'Database Theory'
この場合、すべての書名が最初に評価され、次に "Database Theory" フィルタが適用されます。"Database Theory" というフィルタ値は XML インデックス照合では使用されないので、この関数では、クエリの実行はそれほど効率的ではありません。変数やさらに value() メソッドを使用してフィルタ値を指定しても、同じ動作になります。
型指定されていない XML のテキストの集計
次のような型指定されていない XML を考えます。
SELECT xCol.value ('(/book/title[.="Database Theory"])[1]',
'nvarchar(64)')
FROM docs
または
SELECT xCol.value ('(/book/title[fn:string()="Database Theory"])[1]'),
'nvarchar(64)')
FROM docs
XQuery の意味合いに従うと、このようなクエリは <title> 要素の下のすべてのテキスト ノードを集約して述語を評価する必要があります。 この場合、検索文字列の XML インデックス照合を実行できません。
<title> 要素にテキスト ノードだけが含まれている場合は、以下に示すように、テキスト ノードの述語を評価することが、より効率的なクエリの記述方法です。
SELECT xCol.value ('(/book/title/text())[1][. = "Database Theory"]',
'nvarchar(64)')
FROM docs
この場合は、値 "Database Theory" の XML インデックス照合が行われます。
型指定された XML の場合は、データベース エンジンが関連する XML スキーマからこのことを推測できるので、XML インデックスを使用できます。
シングルトンの指定
XML スキーマで minOccurs = maxOccurs = 1 を使用して要素をシングルトンとして指定できます。この結果、クエリの最適化中に、要素のより正確なカーディナリティが推定され、JOIN 操作が効率的になります。 通常、入れ子になっているループ結合内の内部ループや外部ループについて適切な選択が行われます。
型指定されていないデータの場合、要素のカーディナリティを指定する方法はありません。 ノードのシングルトン カーディナリティは、パス式を満たすノードを厳密に 1 つ選択する、序数述語を使用するパス式で示せます。これは、T-SQL クエリの TOP 1 の使用方法に似ています。以下に例を示します。
単一ノードの選択を省略すると、クエリ オプティマイザでは既定のカーディナリティ推定が使用されますが、この推定値は高すぎることがあるので、入れ子になっているループ結合の内部ループまたは外部ループに次善の選択を行うことがあります。 XML インデックスが存在しない XML blob では、効率的なカーディナリティ推定に使用できる統計情報ないので、この影響がより明確に現れます。
例 : 型指定されていない XML に対するシングルトン カーディナリティの指定
次のクエリを考えてみましょう。
SELECT xCol.query ('/book/title')
FROM docs
クエリ オプティマイザでは、<title> 要素に既定のカーディナリティ推定が使用されます。 各書籍には書名が 1 つあるので、<title> はシングルトン要素ですが、オプティマイザの推定ではそれよりはるかに高い値が推定されます。クエリを次のように書き直します。
SELECT xCol.query ('(/book/title)[1]')
FROM docs
これにより、正しいカーディナリティがオプティマイザに伝わります。よく似たパス式 (/a/b)[1] と /a/b(/book/price)[1] の違いについては、「Microsoft SQL Server 2005 での XML のベスト プラクティス」で説明しています。
Data() アクセサ、text() アクセサ、および string() アクセサ
XQuery には、型指定されたスカラ値をノードから抽出するための fn:data() 関数、テキスト ノードを返すための text() ノード テスト、およびノードの文字列値を返すための fn:string() 関数が用意されています。 これらの使用法に混乱することがあります。SQL Server 2005 でのこれらの適切な使用方法に関するガイドラインを、XML インスタンス <age>12</age> を使用して以下で説明します。
- 型指定されていない XML: パス式 /age/text() から、値が "12" のテキスト ノードが返されます。 fn:data(/age) 関数では文字列値 "12" が返されます。fn:string(/age) でも同じ値が返されます。
- 型指定されている XML: 単純型が指定されている <age> 要素では、式 /age/text() から静的エラーが返されます。 一方、fn:data(/age) からは整数 12 が返されますが、fn:string(/age) からは文字列 "12" が返されます。
これらの関数には、パフォーマンス上の特性に違いがあります。fn:string() は、コンテキスト ノードの下のすべてのテキスト ノードを再帰的に集計します。これは、単一値のコンテキスト ノードでは過剰な処理なので、fn:data() や text() で十分であるだけでなく、これらを使用する方が効率的です。同様に、ノード値が適切な場合、text() を使用してテキスト ノードを返すのではなく、fn:data() を使用する方が効率的です。
XQuery 式と XML DML 式のパラメータ化
XQuery 式と XML DML 式では、自動パラメータ化が行われません。 したがって、2 つの XQuery 式でパラメータの値だけが異なる場合は、動的 SQL ステートメントを使用する必要があります。動的 SQL ステートメントの代わりに、sql:column() または sql:variable() を使用して XQuery 式や XML DML 式にパラメータ値を指定すると、クエリが自動的にパラメータ化されます。
以下の例はストアド プロシージャでの実行を示します。また、クエリ、関数呼び出し、またはデータ変更ステートメントのパラメータ化でも、この方法を使用できます。
たとえば、以下に示すストアド プロシージャでは、入力引数で指定される章番号を含むドキュメントが検索されます。
CREATE PROC sp_myProc
@num INT
AS
SELECT *
FROM XmlTable
WHERE 1= xCol.exist('(//sec/@secId)[. = sql:variable(@num)])'
ADO と OLEDB では、@num の入力値をパラメータにバインドします。これにより、パラメータが異なる値にバインドされたときにクエリが再コンパイルされないようにします。sql:column() を使用しても同様の結果になります。
ストアド プロシージャの呼び出しで行われるパラメータのバインディングを以下の Visual Basic.NET コードで示します。
'myConn は接続文字列です。
SqlCommand cmd = New SqlCommand("sp_myProc", myConn)
cmd.CommandType = CommandType.StoredProcedure
' パラメータのバインド
Dim myParm As SqlParameter = cmd.Parameters.Add("@num", SqlDbType.Int)
myParm.Direction = ParameterDirection.Input
myParm.value = 2
' ストアド プロシージャを呼び出します。
SqlDataReader myReader = cmd.ExecuteReader()
' ストアド プロシージャをもう 1 度呼び出します。
myParm.value = 4
SqlDataReader myReader = cmd.ExecuteReader()
詳細については、Microsoft Visual Studio .NET の製品ドキュメントを参照してください。
パス式内の述語を削除する
ノード テストや分岐 (つまり、パス内の中間ノードの述語) が含まれていない完全なパス (つまり、ルート ノードから選択したノードまでの絶対パスで、子とそのノード自身の軸だけを含まれます) は、効率的に評価できます。 インデックスが設定されている場合は、インデックスのシークで完全なパスを使用できます。 XML blob は、分岐、ワイルドカード (*)、descendant-or-self (//) 軸などを含む他のパスよりも速く解析されます。
完全なパスの末尾のノード テストと述語が、選択したノードのフィルタとして使用されます。 インデックスが使用されるので、XML bolb の解析が効率的です。 以下に例を示します。
例 : 完全なパスの評価
著者の名前が Davis である書籍を選択するパス式を考えてみましょう。
SELECT xCol.query ('/book[author/first-name = "Davis"]')
FROM docs
述語は <book> の直接の要素ではありませんが、完全なパス /book/author/first-name を使用して参照される <first-name> ノードが "Davis" という値でフィルタ処理されます。 <book> 要素は、<first-name> 要素を修飾する親の親として検索されます。
パス ベースの照合は、/book//first-name など、パスを部分的に指定する場合にはそれほど効率的ではありません。 <book> 要素と <first-name> 要素という離れた要素が指定されているので、それらの間にある祖先と子孫の関係が、それらのノードの内部的な番号を使用して確認されます。 したがって、できるだけ多くのパスを指定することで、処理の効率を向上できます。
/book[@ISBN = "1-8610-0157-6"]/author/first-name のような分岐 (つまり、パス式内のノード テストと述語) では、/book[@ISBN = "1-8610-0157-6"] と /book/author/first-name が評価され、2 組の <book> 要素の積集合が取得されます。 そのため、実行速度は分岐を含んでいないパス式よりも遅くなります。 実用的な場合以外は、パス式内でノード テストと述語を同時に使用しないようにしてください。 ただし、「汎用的なマークアップと具体的なマークアップ」で説明したように、データ モデリングを慎重に行えば、適切な場合もあります。
範囲条件
型指定された XML を使用すると、範囲条件を設定するときにメリットがあります。 XML 列に格納されているデータと XML インデックスは、XML スキーマで指定されている型に従って型指定されます。 値の比較により、実行時のデータ変換が回避され、VALUE セカンダリ XML インデックス値を使用して範囲スキャンを実行できるようになります。 この場合、次の例に示すように、範囲条件でコンテキスト ノード (.) を指定する必要もあります。
例 : 範囲条件でのコンテキスト ノード
9.99 ドルから 29.99 ドルまでの価格帯の書籍を検索するクエリを考えてみましょう。
SELECT xCol
FROM T
WHERE xCol.exist ('/book[price > 9.99 and price < 29.99]') = 1
パス式 /book/price > 9.99 と /book/price < 29.99 は個別に評価されます。 クエリ オプティマイザでは <price> 要素が同じであることは認識されないので、VALUE セカンダリ XML インデックスの範囲スキャンが行われません。 以下のように書き直すと、同じコンテキスト ノードが確実に使用され、VALUE セカンダリ XML インデックスの範囲スキャンが 9.99 から 29.99 までの間の値に対して実行されます。 これにより、パフォーマンスが向上します。
SELECT xCol
FROM T
WHERE xCol.exist ('/book/price[. > 9.99 and . < 29.99]') = 1
動的クエリ
XQuery 式は XML データ型のメソッド内でリテラルとして指定されます。 XQuery 式の評価では、使用可能な場合や、クエリ オプティマイザによって選択された場合に XML インデックスが使用されます。
XQuery 式を、リテラルとしてではなく、動的に指定できると、アプリケーション開発が容易になります。 次の方法でこれを実現できます。
クエリの構築
クエリを文字列として作成し、sp_executesql を使用して実行します。 EXEC とは異なり、コンパイル済みのクエリ プランがキャッシュされ、そのコンパイル済みのプランがオプティマイザによって再利用されます。 クエリは文字列形式なので、パラメータ化できます。また、クエリに埋め込みパラメータを含めることができます。
XPath 関数の使用
XPath 式内の各位置指定手順を name() 関数、または local-name() 関数と namespace-URI() 関数に置き換えます。 これにより、ノード名と検索語を渡せるクエリになります。 さらに、「XQuery 式と XML DML 式のパラメータ化」で説明しているように、パラメータ化できます。 このようなパラメータ化したクエリは、アプリケーション開発では便利ですが、コンパイルの時点では具体的なパスが不明なので、このクエリに対して作成されたクエリ プランでは、XML インデックスが無視されます。
クエリは、これらの 2 つの方法のいずれを使用しても適切に構築されますが、実行時のクエリ コンパイルのコストが含まれるので、リテラルとして完全なクエリを指定した場合よりも速度が遅くなります。 ユーザーによって渡される実際のクエリは、検証により、SQL インジェクション攻撃によるセキュリティ上の脅威が生じないようにする必要があります。このような検証を行わない場合は、クエリのパラメータ化にこの方法を使用すべきではありません (「XQuery 式と XML DML 式のパラメータ化」を参照してください)。 このアプローチを次の例で示します。
ノード名を使用してノード テストを指定している 2 番目の方法では、SQL インジェクション攻撃の問題が回避されます。 ただし、クエリ プランは複雑になり、最初のクエリよりもはるかにパフォーマンスが悪くなります。 このことを、この後の 2 番目の例に示します。
例 : sp_executesql を使用するクエリ
以下のクエリを動的に作成し、パラメータを使用して @genre の検索値を渡すことを想定します。
SELECT *
FROM T
WHERE xCol.exist('/book[@genre = "novel"]') = 1
以下に示すように、動的クエリを作成して実行できます。 クエリ文字列が @SQLString 内に作成されます。また、このクエリ文字列には、exist メソッドで使用される埋め込み変数 @bksubj が含まれています。 変数 @subj により、パラメータの実行時の値が指定されます。 @SQLString を使用して渡される動的クエリを検証して (ここでは示していません)、SQL インジェクション攻撃を回避する必要があります。
DECLARE @SQLString NVARCHAR(500)
DECLARE @subj NVARCHAR(64)
DECLARE @ParmDefinition NVARCHAR(500)
--- いったん SQL 文字列を構築します
SET @SQLString =
N'SELECT *
FROM docs
WHERE xCol.exist(''/book[@subject=sql:variable("@bksubj")]'')=1'
SET @ParmDefinition = N'@bksubj NVARCHAR(64)'
--- 最初のパラメータ値で文字列を実行します
SET @subj = 'security'
EXECUTE sp_executesql @SQLString, @ParmDefinition,
@bksubj = @subj
例 : local-name() を使用するクエリ
以下のようなクエリがあるとします。
SELECT *
FROM T
WHERE xCol.exist('/book[@genre = "novel"]') = 1
以下のように、タグ名をリテラルとして使用するように書き直すことができます。
SELECT *
FROM T
WHERE xCol.exist('/*[local-name() = "book" and
@*[local-name() = "genre" and . = "novel"]]') = 1
書き直されたクエリには、ワイルドカード (*) とノード名を使用するノード テストが含まれており、最適化はそれほど簡単ではありません。 したがって、最初のクエリやクエリの構築方法よりも、はるかにパフォーマンスが悪くなります。
親軸
親のノードに移動すると、型情報が失われ、anyType ノードが返されます。 そのノードをさらに操作する場合、明示的なキャストが必要なり、クエリの処理が遅くなることがあります (たとえば、キャストが原因で XML インデックスが使用できない場合があります)。 親ノードへ移動するのではなく、子ノードへ移動することをお勧めします。
XML データからの行セットの生成
アプリケーションによっては、1 つ以上のプロパティを行セットの列に昇格することによって、行セットを XML データから生成する必要があります。 たとえば、あるアプリケーションでは、著者についてクエリを実行し、結果を、姓と名前の 2 つの列を持つ表として表示する場合があります。 このような行セットの生成は、サーバーとクライアントのどちらでも行うことができ、それぞれ異なるパフォーマンス上の特性があります。
- サーバーでは、次のいずれかのメカニズムが使用されます。
- XML データ型の nodes() メソッドと value() メソッドの組み合わせ
- OpenXML
- CLR のストリーム テーブル値関数
- また、XML 形式の結果はクライアントに返されます。この XML 結果は、クライアント側でのプログラミング (たとえば、DataSet) を使用してデータを行セットに変換します。
クライアント側での行セットの生成は、サーバーの負荷を軽減するので、サーバーからクライアントに送信されるデータのほとんどが行セットにマップされる場合に効果的です。 それ以外の場合は、クライアント側で生じるプログラミング上の利点よりも、データの出力にかかるコスト方が高くなる場合があります。
サーバー側での行セットの生成は、サーバーに着信する XML データから行セットを生成する場合に役立ちます。 サーバーで格納された XML データのごく一部が行セットの列に昇格される場合は、一般的に、この方法が適しています。 サーバー側でのアプローチのメリットとデメリットの詳細については、「Microsoft SQL Server 2005 での XML のベスト プラクティス」 で参照できます。