SQL Server 2005のリリース以来,SQL Serverの情報を掲載している技術サイトの数は急増した。MicrosoftのSQL Server開発チームのメンバーたちは自身のブログを持っており,SQL Serverの操作やベスト・プラクティスに関して,ほとんど知られていないような詳細まで紹介している。

 だが多くの情報が公開されているにもかかわらず,SQL Serverに加えられた一部の変更点は非常に微細なものであるため,見逃されている可能性がある。自分がSQL Serverの挙動に関して知っていると思っていることが実際にはもはや通用しない,という事実に気づいていない人もいるかもしれないのだ。

 今回はそういう三つの誤解について,率直に紹介してみようと思う。

誤解1:デッドロックの被害者になるのは避けられない

 筆者が驚いたSQL Server 2005の変更点の一つに,改善されたデッドロック解決のアルゴリズムがある。

 どのセッションがデッドロックの“被害者”として選ばれるかは,それぞれのセッションのデッドロック優先度にかかっている。複数のセッションの優先度が異なるとき,SQL Serverは優先度の最も低いセッションを,デッドロックの被害者として選択する。二つのセッションの優先度が同じであるとき,SQL Serverはロールバックのコストが低いセッションを被害者として選択する。

 2000以前のSQL Serverには,SET DEADLOCK_PRIORITYというSETオプションには二つの値がある。LOWとNORMALだ。このオプションをLOWに設定すると,この接続はデッドロックに直面したとき,被害者に選ばれてしまう。優先度をHIGHに設定して,この接続が被害者に選ばれないようにする方法はない。

 筆者はそのころ,すでにSQL Server Internalsコースを執筆し,公開していた。このコースのなかで筆者は,SET DEADLOCK_PRIORITYについて解説し,使用可能な値は二種類のみであると述べた。

 あるとき筆者は,DEADLOCK_PRIORITYをHIGHに設定することについて書かれたリファレンスを見つけて,最初はそれを間違いだと決め付けていた。だがSQL Server 2005 Books Online(BOL)を調べてみて,筆者が存在を認識していなかった変更点があることに気づいたのだ。

 SQL Server 2005において,SET DEADLOCK_PRIORITYは-10から10までの21種類の優先度レベルの一つを使うことによって,プロセスが被害者として選ばれる優先度を決定められるようになっている。以前のように,LOWや(-5に相当)やNORMAL(デフォルトで,0に相当)という値を使うこともできるし,HIGH(5に相当)を指定することも可能だ。

誤解2:TRUNCATEを実行後にログを保存するにはフルバックアップが必要

 未だに広く信じられているが,実際にはもはや本当でない古い「事実」としてほかに,データベース・バックアップに関連するものがある。あるSQL Serverのベテラン・ユーザーが,SQL Server 2005を使っていて気づいた変更点について,公開ニュースグループに質問を投稿した。

 SQL Server 2000では,テーブルをTRUNCATE(すべての行を削除)した場合,そのあとで一連のログ・バックアップを保持するためには,データベースのフルバックアップを行わなければならなかったと言う。そうする理由として彼は,TRUNCATEを実行した後でログのバックアップを行おうとしたら,SQL Serverはエラーを表示したからだ,と主張した。

 彼の事例でなぜエラーが発生したのか,筆者にはわからない。だがSQL Server 2000のTRUNCATE操作の実行がログのバックアップを無効にする理由は何もない。非常に古いバージョンのSQL Serverでは,こうしたことが実際に起こっており,おそらく彼はそれらのバージョンを使っていたときに,この挙動に出くわしたのだろう。そして,SQL Server 2005でTRUNCATEを行うまで,それが真実だと信じていたのだ。

 SQL Server 2005,2000,そして7.0において,TRUNCATEはログに記録される操作である。TRUNCATE操作の実行に関連するトランザクションを含むログのバックアップは非常に機能的で,TRUNCATEを行うトランザクションを含まないログのバックアップと同じくらい簡単に,データベースの復元に使うことができる。

 時代遅れの思い込みのために彼は,必要以上の時間を費やして,データベースのフルバックアップを行っていた可能性が高い。この時代遅れの情報を使っているのは,この質問を投稿した人物だけではないだろう,と筆者は考えている。

誤解3:データの大量変更時にはインデックスを削除するほうが良い

 読者が気づいていないかもしれない挙動の変更点を,もう一つ紹介しよう。SQL Server 7.0より前は通常,あらゆる一括ロード操作を行う前に,すべてのインデックスを削除するのがベスト・プラクティスだと思われていた。一括ロードには,BCPコマンド,T-SQL BULK INSERTコマンド,そしてSQL Server 2000のDTSやSQL Server 2005のSQL Server Integration Services(SSIS,DTSに替わる機能)の単純なロード操作が含まれる。

 SQL Server 2000より前には,すべてのインデックスを削除して,インデックスのないテーブルに新しいデータをロードしたあと,インデックスを再構築するほうが時間がかからないと考えられていた。インデックスの再構築には高いコストがかかるが,多数の新しい行が追加される場合は,インデックスを維持しながら新しい行を追加するよりは時間がかからないというわけだ。

 SQL Server 2000では,一括挿入(インデックスの一括変更)を処理するためのテクニックが導入された。このテクニックを使えば,複数のインデックスを維持しているときでも,データ挿入の能率をはるかに高められる。このテクニックは一括挿入だけでなく,大規模な更新のときにも利用可能だ。

 インデックス一括変更の代替方法としては,行の一括変更がある。SQL Server 2000より前は,それが唯一の選択肢だった。すべての新しい行や更新された行に対して,SQL Serverはそれぞれのインデックスを個別に維持して,それぞれの非クラスタ化インデックス内の適切な場所で,ポインタの挿入や更新を行う。

 例として,AdventureWorksデータベースのSales.Sales OrderHeaderテーブルを見てみよう。このテーブルのコピーを作成して,SalesOrderIDにクラスタ化インデックスを構築し,CustomerIDとSalesPersonID,ContactIDには非クラスタ化インデックスを構築することにする。

 新しい行を100,000個挿入すると,CustomerIDとSalesPersonID,ContactIDの値は,インデックスのリーフレベルの至る所に比較的ランダムに分散される。それぞれの新しい行に対してSQL Serverは,CustomerIDインデックスのどこに新しいCustomerID値を挿入あるいは更新するのか,そしてSalesPersonIDインデックスのどこに新しいSalesPersonID値を挿入あるいは更新するのかを発見して,そのContactIDインデックスに新しいContactID値を挿入もしくは更新する必要がある。このプロセスを実行すると,より多くの行がリーフに追加されるので,インデックスの上位レベルのすべての変更を除外しても,300,000個のランダムアクセス・データの変更が発生する。

 インデックスの一括変更が可能なのは,SQL Serverがデータの並べ替えを能率的に行うからである。十分な数の行が挿入されたり,更新されたりしていれば,SQL Serverはそのデータに対して並べ替え操作を繰り返し行うことができる。そのデータはそれぞれのインデックスに対して適切な順序を使って,並べ替えることが可能だ。そのあとは,そのインデックスのリーフレベルにパススルーを一度行うだけで,新しいインデックス値のすべてを既存のインデックス・レコードにマージできる。

 リスト1リスト2のコードを実行して,Sales.SalesOrderHeaderテーブルのコピーを作成し,そのテーブルに四つのインデックスを構築してみよう。リスト2はオリジナルのテーブルをその新しいテーブル内に再度コピーして,行数を倍の62,930個に増やす。リスト3のUPDATEクエリーのプランを見てみると,グラフィカル・プランの右端には単一のクラスタ化インデックス・スキャン操作(テーブル・スキャンと同じ)があり,左側には最終ステップとして,クエリーのコストの91パーセントを占めるクラスタ化インデックスの更新があることを確認できる。この操作は,行の一括変換のテクニックを使って,すべてのインデックスに対して,すべての変更を行う。

リスト1:四つのインデックスを持つテーブルのコピーを作成する

USE AdventureWorks
GO
SELECT * INTO SalesOrderHeader
FROM Sales.SalesOrderHeader
GO
ALTER TABLE SalesOrderHeader  ADD CONSTRAINT
   [PK_SalesOrderHeader_SalesOrderID]
   PRIMARY KEY CLUSTERED
([SalesOrderID])
GO
CREATE NONCLUSTERED INDEX
   [IX_SalesOrderHeader_CustomerID]
   ON [SalesOrderHeader]
([CustomerID])
GO
CREATE NONCLUSTERED INDEX
   [IX_SalesOrderHeader_SalesPersonID]
   ON [SalesOrderHeader]
([SalesPersonID])
GO
CREATE NONCLUSTERED INDEX
   [IX_SalesOrderHeader2_ContactID]
   ON [SalesOrderHeader]
([ContactID])
GO

リスト2:テーブルの行の数を2倍にする

INSERT INTO SalesOrderHeader
SELECT  [RevisionNumber],[OrderDate],
   [DueDate],[ShipDate],[Status],
   [OnlineOrderFlag],[SalesOrderNumber],
   [PurchaseOrderNumber],[AccountNumber],
   [CustomerID],[ContactID],[SalesPersonID],
   [TerritoryID],[BillToAddressID],
   [ShipToAddressID],[ShipMethodID],
   [CreditCardID],[CreditCardApprovalCode],
   [CurrencyRateID],[SubTotal],[TaxAmt],
   [Freight],[TotalDue],[Comment],[rowguid],
   [ModifiedDate]
  FROM [Sales].[SalesOrderHeader]
GO
SELECT count(*) FROM SalesOrderHeader
GO

リスト3:UPDATEクエリーのプラン

UPDATE SalesOrderHeader
SET  CustomerID = CustomerID + 100000,
ContactID = ContactID + 100000,
SalesPersonID = SalesPersonID + 1000

 リスト2のコードをもう一度実行すると,テーブルのサイズは100,000行近くまで増える。ここでリスト3のUPDATEのグラフィカル・プランを見てみると,先ほどと異なっていることがわかる。インデックス更新操作を示すいくつかのアイコンと,SQL Serverが並べ替えを行う前に,ワークテーブル内のインデックスキー値を集めていることを示すスプール操作をいくつか確認できる。

 システム上で利用可能なリソースによって,インデックス一括変更のプランを取得するのに必要なリスト2の実行回数が異なるかもしれない。筆者の同僚の技術部門の編集者は,リスト2の一度目の実行でインデックス一括変更のプランを取得した。だが筆者が別のSQL Serverマシンでこのコードを再テストしたときは,リスト2を4回実行する必要があった。

 SQL Serverが挿入や更新の操作に,行の一括変更とインデックスの一括変更のどちらの操作を選択するかは,様々な要因によって決まる。こうした要因には,変更される行の総数やその変更の影響を受ける既存の行の割合,利用できるシステム・リソースなどが含まれる。

 筆者は,一括ロードや一括更新を行う前は,絶対にインデックスを削除すべきではない,とアドバイスするつもりはない。そうではなくて,インデックスを削除するのが常に最善の策だと思い込まないほうがいい,ということを言いたいのだ。自分のデータを使って,自分のSQL Serverで操作をテストする必要があるだろう。データ変更前にインデックスを削除すればコストが低くなるポイントを特定するテストに関して,こちらのページに情報や事例が掲載されている。

小さな驚きの数々

 今回はSQL Serverの挙動に関して,あまり周知されていない三つの変更点を紹介した。読者の皆様のご想像通り,バージョン間でのSQL Serverの変更点は,今回取り上げた例だけではない。今回の教訓は次の通りだ。全く新しいバージョンであろうとサービスパックであろうと,SQL Serverのアップグレード後に挙動を調べるときは,「うれしい」驚きがあるだろう。