印刷用ページ      送信     
クリックして評価とフィードバックをお寄せください
MSDN
MSDN ライブラリ
SQL Server "Yukon"Beta 1 Transact-SQL の機能強化

Itzik Ben-Gan

Solid Quality Learning

October 2003

日本語版最終更新日 2003 年 12 月 9 日

対象 :

   Microsoft® SQL Server™ "Yukon" Beta 1

概要 : 本書では、Microsoft SQL Server "Yukon" Beta 1 で Transact-SQL に対して新しく強化された機能をいくつか紹介します。 これらの新しい機能により、表現力、クエリのパフォーマンス、エラー管理機能を向上できます。 本書では、概念的に新しい、リレーションの機能強化に主に重点を置いており、実例を使用して新しい機能を例示します。 本書には、新しい Transact-SQL 機能すべての説明が含まれているわけではありません。

目次

紹介と対象範囲 紹介と対象範囲
クエリの表現力と DRI サポートの向上 クエリの表現力と DRI サポートの向上
パフォーマンスとエラー処理の機能強化 パフォーマンスとエラー処理の機能強化
Transact-SQL に影響を与える SQL Server "Yukon" Beta 1 の他の機能 Transact-SQL に影響を与える SQL Server "Yukon" Beta 1 の他の機能
まとめ まとめ

紹介と対象範囲

本書では、Microsoft SQL Server "Yukon" Beta 1 で Transact-SQL に対して新しく強化された機能をいくつか紹介します。 これらの新しい機能により、表現力、クエリのパフォーマンス、エラー管理機能を向上できます。 本書では、概念的に新しい、リレーションの機能強化に主に重点を置いており、実例を使用して新しい機能を例示します。 本書には、新しい Transact-SQL 機能すべての説明が含まれているわけではありません。

前提となる知識 : 対象読者は、Microsoft SQL Server 2000 のアドホック クエリに、および Microsoft SQL Server 2000 のアプリケーションのコンポーネントとして、Transact-SQL を使用する技術を持っている必要があります。

クエリの表現力と DRI サポートの向上

ここでは、次の新しいリレーショナル機能と強化された機能を紹介します。

  • 共通テーブル式 (CTE) に基づいた新しい再帰クエリ

  • 新しいリレーショナル演算子の PIVOT および APPLY

  • 宣言参照整合性制約 (DRI) の機能強化

再帰クエリと共通テーブル式

本書では、再帰 CTE 式の優れた点を探求し、従来のアプローチを非常に簡素化する方法での一般的な問題点に対するソリューションとして再帰的な CTE 式を適用します。

共通テーブル式

共通テーブル式 (CTE) は、定義しているステートメントによって参照できる名前付きの一時的な結果セットです。 CTE の単純な形式では、CTE を派生テーブルの向上したバージョンと見なすことができます。このバージョンは、非永続的なビューと非常に似ています。 派生テーブルとビューを参照する方法と同様、クエリの FROM 句で CTE を参照します。 CTE は一度だけ定義します。その後、クエリ内で CTE を数回参照できます。 CTE の定義では、同一バッチ内に定義された変数を参照できます。 また、ビューを使用する方法と同様、INSERT、UPDATE、DELETE、および CREATE VIEW の各ステートメントにも CTE を使用できます。 ただし、CTE の実際の能力は、CTE が CTE 自体を参照する再帰的な機能にあります。 本書では、前半に単純な形式の CTE、後半に再帰的な形式の CTE を説明します。 また、本書では、CTE を使った SELECT クエリを網羅しています。

クエリ結果を参照するときに、クエリ結果がテーブルであるかのように派生テーブルを使用しますが、データベースに永続的なビューを作成することは望みません。 ただし、派生テーブルには、CTE による緩やかな制限があります。 この制限として、クエリ内で一度定義した派生テーブルを数回使用することはできません。 代わりに、同じクエリを含むいくつかの派生テーブルを定義する必要があります。 代替策として、CTE を一度定義して、データベース内で CTE を保存せずに、クエリ内でその CTE を数回使用することができます。

CTE の実例を提供する前に、CTE の基本構文を派生テーブルおよびビューと比較します。 以下は、ビュー、派生テーブル、および CTE 内のクエリの一般的な形式です。

ビュー

CREATE VIEW <view_name>(<column_aliases>)
AS
<view_query>
GO

SELECT *
FROM <view_name>

派生テーブル

SELECT * 
FROM (<derived_table_query>) AS <derived_table_alias>(<column_aliases>) 

CTE

WITH <cte_alias>(<column_aliases>)
AS
(
  <cte_query>
)
SELECT *
FROM <cte_alias>

WITH キーワードの後に、CTE の別名および別名の一覧 (省略可能) を指定し、その本体を記述し、その本体を外部クエリから参照します。

CTE の WITH 句がバッチ内の最初のステートメントでない場合、先行するステートメントと WITH 句を含むステートメントをセミコロン (;) で区切る必要があることに注意してください。セミコロンは、WITH 句を含むステートメントの前に配置します。 セミコロンは、WITH 句の他の用途 (たとえば、テーブル ヒント) との曖昧さをなくすために使用します。 すべての場合にセミコロンを指定する必要があるわけではありませんが、セミコロンを一貫して使用することをお勧めします。

実例として、Northwind データベースの Employees および Orders テーブルを考えます。 各従業員は、ReportsTo 列で指定されたマネージャに直属します。 Employees テーブルの各従業員は、Orders テーブル内の注文に関連させることができます。 従業員ごとに注文数と最終注文日付を返し、同じ行にマネージャに対して同様の詳細を返すとします。 以下の例で、ビュー、派生テーブル、および CTE を使用してソリューションを実装できる方法を示します。

ビュー

CREATE VIEW VEmpOrders(EmployeeID, NumOrders, MaxDate)
AS

SELECT EmployeeID, COUNT(*), MAX(OrderDate)
FROM Orders
GROUP BY EmployeeID
GO

SELECT E.EmployeeID, OE.NumOrders, OE.MaxDate,
  E.ReportsTo, OM.NumOrders, OM.MaxDate
FROM Employees AS E
  JOIN VEmpOrders AS OE
    ON E.EmployeeID = OE.EmployeeID
  LEFT OUTER JOIN VEmpOrders AS OM
    ON E.ReportsTo = OM.EmployeeID

派生テーブル

SELECT E.EmployeeID, OE.NumOrders, OE.MaxDate,
  E.ReportsTo, OM.NumOrders, OM.MaxDate
FROM Employees AS E
  JOIN (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
        FROM Orders
        GROUP BY EmployeeID) AS OE(EmployeeID, NumOrders, MaxDate)
    ON E.EmployeeID = OE.EmployeeID
  LEFT OUTER JOIN
       (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
        FROM Orders
        GROUP BY EmployeeID) AS OM(EmployeeID, NumOrders, MaxDate)
    ON E.ReportsTo = OM.EmployeeID

CTE

WITH EmpOrdersCTE(EmployeeID, NumOrders, MaxDate)
AS
(
  SELECT EmployeeID, COUNT(*), MAX(OrderDate)
  FROM Orders
  GROUP BY EmployeeID
)
SELECT E.EmployeeID, OE.NumOrders, OE.MaxDate,
  E.ReportsTo, OM.NumOrders, OM.MaxDate
FROM Employees AS E
  JOIN EmpOrdersCTE AS OE
    ON E.EmployeeID = OE.EmployeeID
  LEFT OUTER JOIN EmpOrdersCTE AS OM
    ON E.ReportsTo = OM.EmployeeID

CTE は外部クエリの前に定義する必要があります。ただし、CTE の定義は参照する場合としない場合があります。 他の介入しているステートメントの後のバッチ内で CTE を後で参照することはできません。

1 つの WITH 句にいくつかの CTE を定義して、それぞれが前に定義した CTE を参照するように指定できます。 CTE を区切るには、コンマを使用します。 たとえば、従業員の注文数の最大値、最小値、および差異を計算するとします。

WITH
EmpOrdersCTE(EmployeeID, Cnt)
AS
(
  SELECT EmployeeID, COUNT(*)
  FROM Orders
  GROUP BY EmployeeID
),
MinMaxCTE(MN, MX, Diff)
AS
(
  SELECT MIN(Cnt), MAX(Cnt), MAX(Cnt)-MIN(Cnt)
  FROM EmpOrdersCTE
)
SELECT * FROM MinMaxCTE

結果セットは以下のようになります。

MN          MX          Diff       
----------- ----------- -----------
42          156         114  

EmpOrdersCTE で、各従業員の注文数を計算し、MinMaxCTE で、EmpOrdersCTE を参照してその数の最大値、最小値、および差異を計算します。

注意   CTE 内では、CTE の直前に定義された CTE しか参照できないという制限はありません。前に定義されたすべての CTE を参照できます。 前方参照は許可されていないことに注意してください。 つまり、ある CTE はそれより前に定義された CTE とその CTE 自体 (本書後半の「再帰クエリ」を参照) を参照できますが、その CTE の後に定義された CTE を参照することはできません。 1 つの WITH ステートメントに CTE C1、C2、C3 を定義した場合、C2 は C1 と C2 を参照できますが、C3 を参照することはできません。

別の例として、以下のコードでは、最小値と最大値間の注文数の 4 つの範囲それぞれに含まれる従業員の数を計算するヒストグラムが生成されます。 計算が少し複雑に感じる場合、この計算を理解することに時間を費やす必要はありません。 この例の目的は、以下のような実用的なシナリオを使用して、1 つの WITH ステートメントでの複数の CTE の宣言を例示することにあります。

WITH
EmpOrdersCTE(EmployeeID, Cnt)
AS
(
  SELECT EmployeeID, COUNT(*)
  FROM Orders
  GROUP BY EmployeeID
),
MinMaxCTE(MN, MX, Diff)
AS
(
  SELECT MIN(Cnt), MAX(Cnt), MAX(Cnt)-MIN(Cnt)
  FROM EmpOrdersCTE
),
NumsCTE(Num)
AS
(
  SELECT 1 AS Num
  UNION ALL SELECT 2
  UNION ALL SELECT 3
  UNION ALL SELECT 4
),
StepsCTE(Step, Fromval, Toval)
AS
(
  SELECT
    Num,
    MN + ROUND((Num-1)*((Diff+1)/4.), 0),
    MN + ROUND((Num)*((Diff+1)/4.), 0) - 1
  FROM MinMaxCTE CROSS JOIN NumsCTE
),
HistogramCTE(Step, Fromval, Toval, Samples)
AS
(
  SELECT S.Step, S.Fromval, S.Toval, COUNT(*)
  FROM StepsCTE AS S
    JOIN EmpOrdersCTE AS OE
      ON OE.Cnt BETWEEN S.Fromval AND S.Toval
  GROUP BY S.Step, S.Fromval, S.Toval
)
SELECT * FROM HistogramCTE

結果セットは以下のようになります。

Step  Fromval  Toval  Samples
----- -------- ------ ---------
1     42       70     3
2     71       99     2
3     100      127    3
4     128      156    1

2 番目の CTE (MinMaxCTE) が最初の CTE (EmpOrdersCTE) を参照しており、3 番目の CTE (NumsCTE) はどの CTE も参照していないことに注目してください。 また、4 番目の CTE (StepsCTE) は 2 番目と 3 番目の CTE を参照しており、5 番目の CTE (HistogramCTE) は最初と 4 番目の CTE を参照しています。

再帰クエリ

非再帰 CTE を使用すると、表現力が増します。 ただし、非再帰 CTE を使用するコードの各部分には、通常、より短いコードを記述できます。その短いコードで、派生テーブルなどの他の Transact-SQL 構造体を使用して同じ結果を得ることができます。 この場合は、再帰 CTE とは異なります。 ここでは、再帰クエリのセマンティクスについて説明し、組織図での従業員の階層、および資料の請求書 (BOM) のシナリオの実用的な実装を提供します。

セマンティクス

CTE が CTE 自体を参照する場合、CTE は再帰していると考えられます。 再帰 CTE は、少なくとも 2 個のクエリ (または再帰クエリ用法のメンバ) で構成されます。 1 つのクエリは、非再帰クエリであり、アンカー メンバ (AM) とも呼ばれています。 もう 1 つのクエリは、再帰クエリで、再帰メンバ (RM) とも呼ばれます。 クエリは、UNION ALL 演算子によって分離されます。 以下の例で、再帰 CTE の簡素化された汎用形式を示します。

WITH RecursiveCTE(<column_list>)
AS
(
  -- Anchor Member:
  -- SELECT query that does not refer to RecursiveCTE
  SELECT ... 
  FROM <some_table(s)>
  ...

  UNION ALL

  -- Recursive Member
  -- SELECT query that refers to RecursiveCTE
  SELECT ...
  FROM <some_table(s)>
    JOIN RecursiveCTE
  ...
)
-- Outer Query
SELECT ...
FROM RecursiveCTE
...

再帰 CTE を実装するアルゴリズムは、以下のように論理的に考えることができます。

  1. アンカー メンバがアクティブになります。 SET R0 (R は結果 (Results) を表します) が生成されます。

  2. 再帰メンバがアクティブになり、RecursiveCTE を参照しているときに、入力として SET Ri (i = 手順番号) を取得します。 SET Ri + 1 が生成されます。

  3. 手順 2 のロジックは、空の SET が返されるまで、繰り返し実行されます (繰り返されるたびに、手順番号が増加されます)。

  4. 外部クエリが実行され、RecursiveCTE を参照しているときに、以前の手順すべての累積 (UNION ALL) 結果が取得されます。

CTE には複数のメンバを保持できます。しかし、再帰メンバと他のメンバ (再帰または非再帰) 間に使用できる演算子は、UNION ALL 演算子のみです。 UNION などの他の演算子は、非再帰メンバ間にしか使用できません。 暗黙の変換をサポートする通常の UNION や UNION ALL 演算子とは異なり、再帰 CTE では、同じデータ型、長さ、有効桁数など、すべてのメンバの列が正確に一致する必要があります。

再帰 CTE および再帰ルーチンの従来の開発 (SQL Server に固有である必要なし) の間に共通点を見出したい場合、類似点は見つけることができます。 再帰ルーチンは、通常 3 つの重要な要素で構成されます。3 つの要素とは、ルーチンの初回の呼び出し、再帰的な終了チェック、および同じルーチンへの再帰呼び出しです。 アンカー メンバがルーチンの初回呼び出しに対応し、再帰メンバがルーチンの再帰呼び出しに対応します。また、終了チェックは、再帰ルーチン (たとえば、IF ステートメントを使用) では通常明示的ですが、再帰 CTE では暗黙的です。再帰 CTE では、前の呼び出しから行が返されなかったときに再帰が停止します。

本書の次のセクションでは、1 つおよび複数の親を持つ環境での再帰 CTE の実例と用途を紹介します。

1 つの親を持つ環境 : 従業員組織図

1 つの親を持つ階層のシナリオには、従業員組織図を使用します。

注意   ここでの例では、NorthwindEmployees テーブルとは異なる構造を持つ Employees という名前のテーブルを作成および使用します。 コードは、独自のテスト用データベース、または tempdb で実行してください。Northwind は使用しないでください。

以下のコードを実行して、Employees テーブルを生成し、そのテーブルにサンプル データを作成します。

USE tempdb -- or your own test database

CREATE TABLE Employees
(
  empid   int         NOT NULL,
  mgrid   int         NULL,
  empname varchar(25) NOT NULL,
  salary  money       NOT NULL,
  CONSTRAINT PK_Employees PRIMARY KEY(empid),
  CONSTRAINT FK_Employees_mgrid_empid
    FOREIGN KEY(mgrid)
    REFERENCES Employees(empid)
)

CREATE INDEX idx_nci_mgrid ON Employees(mgrid)

SET NOCOUNT ON
INSERT INTO Employees VALUES(1 , NULL, 'Nancy'   , $10000.00)
INSERT INTO Employees VALUES(2 , 1   , 'Andrew'  , $5000.00)
INSERT INTO Employees VALUES(3 , 1   , 'Janet'   , $5000.00)
INSERT INTO Employees VALUES(4 , 1   , 'Margaret', $5000.00) 
INSERT INTO Employees VALUES(5 , 2   , 'Steven'  , $2500.00)
INSERT INTO Employees VALUES(6 , 2   , 'Michael' , $2500.00)
INSERT INTO Employees VALUES(7 , 3   , 'Robert'  , $2500.00)
INSERT INTO Employees VALUES(8 , 3   , 'Laura'   , $2500.00)
INSERT INTO Employees VALUES(9 , 3   , 'Ann'     , $2500.00)
INSERT INTO Employees VALUES(10, 4   , 'Ina'     , $2500.00)
INSERT INTO Employees VALUES(11, 7   , 'David'   , $2000.00)
INSERT INTO Employees VALUES(12, 7   , 'Ron'     , $2000.00)
INSERT INTO Employees VALUES(13, 7   , 'Dan'     , $2000.00)
INSERT INTO Employees VALUES(14, 11  , 'James'   , $1500.00)

各従業員は、ID が mgrid 列に格納されているマネージャに直属します。 empid 列を参照する mgrid 列に、外部キーが定義されています。つまり、マネージャ ID はテーブル内の有効な従業員 ID に合致するか、NULL である必要があります。 上司の Nancy は、mgrid 列が NULL になります。 マネージャと従業員の関係を図 1 に示します。

sql_yukontsqlenhance_image01.gif

1. 従業員組織図

以下に、Employees テーブルに対する一般的な要求をいくつかを示します。

  • Robert (empid=7) および彼の部下全員 (すべてのレベル) の詳細を教えてください。

  • Janet (empid=3) より下位の 2 レベルに存在するすべての従業員の詳細を教えてください。

  • James (empid=14) までの管理のつながりを教えてください。

  • 各マネージャに何人の従業員が直接または間接的に属していますか?

  • 階層の従属関係が見やすいように従業員すべてを示してください。

再帰 CTE は、このような要求を処理する手段を提供します。また、再帰 CTE は本質的に再帰的なので、データベースに階層に関する追加情報を保持する必要はありません。

最初の要求は、最も一般的な要求であると考えられます。つまり、従業員 (たとえば、empid=7 の Robert) およびその人物より下位のすべてのレベルに存在する部下を返します。 この要求へのソリューションは、以下の CTE により提供されます。

WITH EmpCTE(empid, empname, mgrid, lvl)
AS
( 
  -- Anchor Member (AM)
  SELECT empid, empname, mgrid, 0
  FROM Employees
  WHERE empid = 7

  UNION ALL

  -- Recursive Member (RM)
  SELECT E.empid, E.empname, E.mgrid, M.lvl+1
  FROM Employees AS E
    JOIN EmpCTE AS M
      ON E.mgrid = M.empid
)
SELECT * FROM EmpCTE

結果セットは以下のようになります。

empid       empname                   mgrid       lvl        
----------- ------------------------- ----------- -----------
7           Robert                    3           0          
11          David                     7           1          
12          Ron                       7           1          
13          Dan                       7           1          
14          James                     11          2  

この CTE は、前に説明した再帰 CTE ロジックに従って、次のように処理されます。

  1. アンカー メンバは、Employees から Robert の行が返されるときにアクティブになります。 lvl 結果列に定数 0 が返されることに注目してください。

  2. 再帰メンバは、繰り返しアクティブになり、Employees および EmpCTE の結合操作による以前の結果の直属の部下を返します。 以下のように、Employees は部下を表し、EmpCTE (以前の呼び出しの結果を含んでいます) はマネージャを表します。

    • まず、Robert の部下、David、Ron、および James が返されます。

    • 次に、David、Ron、および Dan の部下が返されます。つまり、James だけが返されます。

    • 最後に、James の部下が返されます。この場合、誰も返されず、再帰が終了されます。

  3. 外部クエリにより、EmpCTE から全行が返されます。

再帰呼び出しごとに、lvl 値が繰り返しインクリメントされることがわかります。

このレベル カウンタを使用すると、再帰での繰り返し数を制限できます。 たとえば、以下の CTE を使用して、Janet より下位の 2 レベルに存在するすべての従業員を返します。

WITH EmpCTEJanet(empid, empname, mgrid, lvl)
AS
( 
  SELECT empid, empname, mgrid, 0
  FROM Employees
  WHERE empid = 3

  UNION ALL

  SELECT E.empid, E.empname, E.mgrid, M.lvl+1
  FROM Employees as E
    JOIN EmpCTEJanet as M
      ON E.mgrid = M.empid
  WHERE lvl < 2
)
SELECT empid, empname
FROM EmpCTEJanet
WHERE lvl = 2

結果セットは以下のようになります。

empid       empname                  
----------- -------------------------
11          David                    
12          Ron                      
13          Dan      

以前のコード例と比較してこのコード例に追加された部分は、太字で示してあります。 再帰メンバのフィルタ WHERE lvl < 2 は、再帰終了チェックとして使用されます。lvl = 2 の時点で返される行がないので、再帰が停止します。 外部クエリのフィルタ WHERE lvl = 2 は、レベル 2 までのすべてのレベルを削除するために使用されます。 外部クエリのフィルタ (lvl = 2) は、それ自体で必要な行のみを返すのに論理的には十分であることに注意してください。 再帰メンバのフィルタ (lvl < 2) は、パフォーマンス上の理由で追加されています。これにより、Janet より下位 2 レベルが返された直後に、再帰が停止されます。

上記のとおり、CTE は同一バッチ内で定義されているローカル変数を参照できます。 たとえば、クエリをより汎用的にするには、従業員 ID とレベルに定数ではなく変数を使用できます。

DECLARE @empid AS INT, @lvl AS INT
SET @empid = 3 -- Janet
SET @lvl   = 2 -- two levels

WITH EmpCTE(empid, empname, mgrid, lvl)
AS
( 
  SELECT empid, empname, mgrid, 0
  FROM Employees
  WHERE empid = @empid

  UNION ALL

  SELECT E.empid, E.empname, E.mgrid, M.lvl+1
  FROM Employees as E
    JOIN EmpCTE as M
      ON E.mgrid = M.empid
  WHERE lvl < @lvl
)
SELECT empid, empname
FROM EmpCTE
WHERE lvl = @lvl

再帰を特定回数繰り返し呼び出した後で、ヒントを使用してクエリを強制終了することができます。 この処理を行うには、外部クエリの末尾に OPTION(MAXRECURSION value) を追加します。以下に例を示します。

WITH EmpCTE(empid, empname, mgrid, lvl)
AS
( 
  SELECT empid, empname, mgrid, 0
  FROM Employees
  WHERE empid = 1

  UNION ALL

  SELECT E.empid, E.empname, E.mgrid, M.lvl+1
  FROM Employees as E
    JOIN EmpCTE as M
      ON E.mgrid = M.empid
)
SELECT * FROM EmpCTE
OPTION (MAXRECURSION 2)

結果セットは以下のようになります。

empid       empname                   mgrid       lvl        
----------- ------------------------- ----------- -----------
1           Nancy                     NULL        0          
2           Andrew                    1           1          
3           Janet                     1           1          
4           Margaret                  1           1          
10          Ina                       4           2          
7           Robert                    3           2          
8           Laura                     3           2          
9           Ann                       3           2      
    
.Net SqlClient Data Provider:  Msg 530, Level 16, State 1, Line 1
(メッセージ 530、レベル 16、状態 1、行 1)
Statement terminated.(ステートメントは終了されました。) 
Maximum recursion 2 has been exhausted before statement completion
(ステートメントを完了する前に、最大再帰 2 に到達しました。)

これまでに生成した結果が返され、エラー 530 が生成されます。 Janet より下位 2 レベルに存在する従業員を返す要求を実装する場合、以下のように、再帰メンバのフィルタではなく、MAXRECURSION ヒントを使用できます。

WITH EmpCTE(empid, empname, mgrid, lvl)
AS
( 
  SELECT empid, empname, mgrid, 0
  FROM Employees
  WHERE empid = 3

  UNION ALL

  SELECT E.empid, E.empname, E.mgrid, M.lvl+1
  FROM Employees as E
    JOIN EmpCTE as M
      ON E.mgrid = M.empid
)
SELECT empid, empname
FROM EmpCTE
WHERE lvl = 2
OPTION (MAXRECURSION 2)

結果セットは以下のようになります。

empid       empname                  
----------- -------------------------
11          David                    
12          Ron                      
13          Dan                      
.Net SqlClient Data Provider:  Msg 530, Level 16, State 1, Line 1
(メッセージ 530、レベル 16、状態 1、行 1)
Statement terminated.(ステートメントは終了されました。) 
Maximum recursion 2 has been exhausted before statement completion
(ステートメントを完了する前に、最大再帰 2 に到達しました。)

クライアントが結果の他にエラーも取得することに注意してください。 有効な状況でエラーを返すコードを使用するのは、プログラミングの演習として適切ではありません。 上記のフィルタを使用することをお勧めします。MAXRECURSION ヒントは、必要に応じて、無限ループに陥らないための予防手段として使用することをお勧めします。

このヒントを指定しない場合、SQL Server の既定値は 100 になります。 この値は、巡回再帰呼び出しの疑いがある場合に、予防策として使用できます。 再帰呼び出しの回数を制限しない場合、ヒントの MAXRECURSION を 0 に設定してください。

これまでに説明した再帰の例には、マネージャであるアンカー メンバ、および部下を取得する再帰メンバが含まれています。 James の管理パス (James と彼のすべてのレベルでのマネージャ全員) を返す場合など、一部の要求ではその逆が必要になります。 以下のコードで、この要求への回答を提供します。

WITH EmpCTE(empid, empname, mgrid, lvl)
AS
( 
  SELECT empid, empname, mgrid, 0
  FROM Employees
  WHERE empid = 14

  UNION ALL

  SELECT M.empid, M.empname, M.mgrid, E.lvl+1
  FROM Employees as M
    JOIN EmpCTE as E
      ON M.empid = E.mgrid
)
SELECT * FROM EmpCTE

結果セットは以下のようになります。

empid       empname                   mgrid       lvl        
----------- ------------------------- ----------- -----------
14          James                     11          0          
11          David                     7           1          
7           Robert                    3           2          
3           Janet                     1           3          
1           Nancy                     NULL        4

アンカー メンバは James の行を返します。 ここでは 1 つの親を持つ階層を使用しており、要求が 1 人の従業員で開始されるので、再帰メンバには、以前返された従業員のマネージャまたは 1 人のマネージャが返されます。

また、各マネージャに直接または間接的に属している部下の数など、集計を計算する再帰クエリを使用することもできます。

WITH MgrCTE(mgrid, lvl)
AS
(
  SELECT mgrid, 0
  FROM Employees
  WHERE mgrid IS NOT NULL

  UNION ALL

  SELECT M.mgrid, lvl + 1
  FROM Employees AS M
    JOIN MgrCTE AS E
      ON E.mgrid = M.empid
  WHERE M.mgrid IS NOT NULL
)
SELECT mgrid, COUNT(*) AS cnt
FROM MgrCTE
GROUP BY mgrid

結果セットは以下のようになります。

mgrid       cnt        
----------- -----------
1           13         
2           2          
3           7          
4           1          
7           4          
11          1

アンカー メンバには、各従業員のマネージャ ID が保持されている行が返されます。 マネージャ ID 列が NULL の場合、特定のマネージャが存在しないことを表すので、NULL は除外されます。 再帰メンバが以前返されたマネージャのマネージャ ID を返します。ここでも、NULL は除外されます。 最終的に、CTE には、各マネージャの直接または間接的な部下の数が保持されます。 外部クエリは、マネージャ ID ごとに結果がグループ化され、その数が返された状態になります。

1 つの親を持つ階層に対する要求の最後の例として、階層の従属関係によって並べ替えまたはインデントを行った Nancy の部下 (上司) を返すとします。 これを行うには、以下のコードを使用します。従業員 ID によって兄弟を並べ替えます。

WITH EmpCTE(empid, empname, mgrid, lvl, sortcol)
AS
( 
  SELECT empid, empname, mgrid, 0,
    CAST(empid AS VARBINARY(900))
  FROM Employees
  WHERE empid = 1

  UNION ALL

  SELECT E.empid, E.empname, E.mgrid, M.lvl+1,
    CAST(sortcol + CAST(E.empid AS BINARY(4)) AS VARBINARY(900))
  FROM Employees AS E
    JOIN EmpCTE AS M
      ON E.mgrid = M.empid
)
SELECT
  REPLICATE(' | ', lvl)
    + '(' + (CAST(empid AS VARCHAR(10))) + ') '
    + empname AS empname
FROM EmpCTE
ORDER BY sortcol

(1) Nancy
 | (2) Andrew
 |  | (5) Steven
 |  | (6) Michael
 | (3) Janet
 |  | (7) Robert
 |  |  | (11) David
 |  |  |  | (14) James
 |  |  | (12) Ron
 |  |  | (13) Dan
 |  | (8) Laura
 |  | (9) Ann
 | (4) Margaret
 |  | (10) Ina

empid 値によって兄弟を並べ替えるには、従業員ごとに sortcol というバイナリ文字列を作成します。 この文字列は、各従業員までの管理チェーンで連結された、バイナリ値に変換された従業員 ID で構成されます。 アンカー メンバは開始点です。 アンカー メンバがルートの従業員の empid を持つバイナリ値を作成します。 反復されるたびに、再帰メンバがバイナリ値に変換された現在の従業員 ID をマネージャの sortcol に追加します。 その後、外部クエリが sortcol で結果を並べ替えます。 アンカー メンバと再帰メンバの両方の対応する列が、同じデータ型、長さ、および有効桁数を保持することに注意してください。 これにより、整数値が 2 進表現で 4 バイト必要な場合でも、sortcol 値を生成する式が varbinary(900) に変換されます。900 バイトは 225 レベルを表します。そのため、この式はより適切な制限であるように見えます。 より多くのレベルをサポートしたい場合、この長さを増やすことが可能ですが、両方のメンバでその操作を行う必要があります。そうしないと、エラーが発生します。

階層的なインデントを行うには、従業員数のレベルと同じ回数、文字列 (この場合は ' | ') を複製します。 そのため、従業員 ID 自体がかっこ内に追加され、最終的には従業員の名前も追加されます。

複数の親を持つ環境 : 部品一覧表

前のセクションでは、CTE を使用して、ツリー内の各ノードに親が 1 つだけ存在する階層を扱いました。 リレーションシップのより複雑なシナリオとして、各ノードに複数の親が存在するグラフがあります。 ここでは、部品一覧表 (BOM) での CTE の使用について説明します。 BOM は、非循環有向グラフで、各ノードに複数の親を保持できます。つまり、ノードを直接または間接的にノード自体の親にすることができず、2 つのノード間のリレーションシップは二次元ではありません (たとえば、A は C を含みますが、C は A を含みません)。 図 2 では、BOM シナリオでの項目間のリレーションシップを示します。

GR4666.gif

2. 複数の親を持つ環境

たとえば、項目 A は D、B、および C を含み、項目 C は B および E を含み、項目 B は項目 A と C に含まれます。 以下のコードを実行して、Items および BOM テーブルを作成し、それらのテーブルにサンプル データを作成します。

CREATE TABLE Items
(
  itemid   VARCHAR(5)  NOT NULL PRIMARY KEY,
  itemname VARCHAR(25) NOT NULL,
  /* other columns, e.g., unit_price, measurement_unit */
)

CREATE TABLE BOM
(
  itemid     VARCHAR(5) NOT NULL REFERENCES Items,
  containsid VARCHAR(5) NOT NULL REFERENCES Items,
  qty        INT        NOT NULL
  /* other columns, e.g., quantity */
  PRIMARY KEY(itemid, containsid),
  CHECK (itemid <> containsid)
)

SET NOCOUNT ON
INSERT INTO Items(itemid, itemname) VALUES('A', 'Item A')
INSERT INTO Items(itemid, itemname) VALUES('B', 'Item B')
INSERT INTO Items(itemid, itemname) VALUES('C', 'Item C')
INSERT INTO Items(itemid, itemname) VALUES('D', 'Item D')
INSERT INTO Items(itemid, itemname) VALUES('E', 'Item E')
INSERT INTO Items(itemid, itemname) VALUES('F', 'Item F')
INSERT INTO Items(itemid, itemname) VALUES('G', 'Item G')
INSERT INTO Items(itemid, itemname) VALUES('H', 'Item H')
INSERT INTO Items(itemid, itemname) VALUES('I', 'Item I')
INSERT INTO Items(itemid, itemname) VALUES('J', 'Item J')
INSERT INTO Items(itemid, itemname) VALUES('K', 'Item K')

INSERT INTO BOM(itemid, containsid, qty) VALUES('E', 'J', 1)
INSERT INTO BOM(itemid, containsid, qty) VALUES('C', 'E', 3)
INSERT INTO BOM(itemid, containsid, qty) VALUES('A', 'C', 2)
INSERT INTO BOM(itemid, containsid, qty) VALUES('H', 'C', 4)
INSERT INTO BOM(itemid, containsid, qty) VALUES('C', 'B', 2)
INSERT INTO BOM(itemid, containsid, qty) VALUES('B', 'F', 1)
INSERT INTO BOM(itemid, containsid, qty) VALUES('B', 'G', 3)
INSERT INTO BOM(itemid, containsid, qty) VALUES('A', 'B', 2)
INSERT INTO BOM(itemid, containsid, qty) VALUES('A', 'D', 2)
INSERT INTO BOM(itemid, containsid, qty) VALUES('H', 'I', 1)

Items テーブルには、各項目の行が保持されます。 BOM テーブルには、グラフのノード間のリレーションシップが含まれます。 各リレーションシップは、親の項目 ID (itemid)、子の項目 ID (containsid)、および itemid 内の containsid の数量 (qty) で構成されます。

BOM シナリオの共通の要求は、項目を "解体"することです。 つまり、グラフ全体を、直接または間接的に、特定の項目からスキャン開始し、グラフに含まれるすべての項目を返します。 この処理は、従業員組織図のようなツリーに含まれるサブツリーを返すことに似ているので、理解しやすいかもしれません。 ただし、有向グラフでは、含まれている 1 つの項目に、別の経路に含まれるいくつか異なる項目から到達できるので、要求は概念上少し複雑になります。 たとえば、項目 A を取り出すとします。 項目 A から項目 B にたどり着く 2 つの異なる経路が存在することに気付きます。 2 つの経路とは、A→B および A→C→B です。 つまり、項目 B には 2 回到達します。これは、B に含まれるすべての項目 (F および G) に 2 回到達することを意味します。 幸いにも、CTE を使用すると、このような要求は、ツリーに含まれるサブツリーを取得する要求を実装するのと同じくらい単純に実装できます。以下に例を示します。

WITH BOMCTE
AS
(
  SELECT *
  FROM BOM
  WHERE itemid = 'A'

  UNION ALL

  SELECT BOM.*
  FROM BOM
    JOIN BOMCTE
      ON BOM.itemid = BOMCTE.containsid
)
SELECT * FROM BOMCTE

結果セットは以下のようになります。

itemid containsid qty        
------ ---------- -----------
A      B          2          
A      C          2          
A      D          2          
C      B          2          
C      E          3          
E      J          1          
B      F          1          
B      G          3          
B      F          1          
B      G          3

アンカー メンバは、BOM に含まれている A の直接的な項目をすべて返します。 再帰メンバは、CTE の前の反復で返されたときに含まれていた項目ごとに、BOM と BOMCTE を結合することで含まれる項目を返します。 (出力の順序は必ずしもこのとおりではありませんが) 論理的には、まず、(A, B)、(A, C)、(A, D) が返されます。次に、(B, F)、(B, G)、(C, B)、(C, E) が返されます。最後に、(B, F)、(B, G)、(E, J) が返されます。 BOM からの大部分の要求では、最終的な結果に複数回項目を表示する必要がないことに注意してください。 解体で必要な項目のみを表示する場合、DISTINCT 句を使用して重複する項目を除去できます。以下に例を示します。

WITH BOMCTE
AS
(
  SELECT *
  FROM BOM
  WHERE itemid = 'A'

  UNION ALL

  SELECT BOM.*
  FROM BOM
    JOIN BOMCTE
      ON BOM.itemid = BOMCTE.containsid
)
SELECT DISTINCT containsid FROM BOMCTE

結果セットは以下のようになります。

containsid
----------
B         
C         
D         
E         
F         
G         
J    

部品を解体する処理を理解しやすくするために、すべての項目がその各項目に含まれている項目に展開されるツリーで中間結果をビジュアルに示します。 図 3 は、項目の量に従って部分 A と H を展開することによって形成したツリーを示しています。

sql_yukontsqlenhance_image03.gif

3. 部品の展開

元の要求をさらに進めていくと、通常は、項目自体を取得するのではなく、各項目の累積数量を取得することに関心を持つようになります。 たとえば、A が C を 2 個含みます。 C が E を 3 個含みます。 E が J を 1 個含みます。 A に必要な J の合計数は、A から J までのパスに存在する製品の数です。 2*3*1 = 6. 図 4 は、項目を集計する前に A を構成する各項目の累積数量を示しています。

sql_yukontsqlenhance_image04.gif

4. 部品の展開 計算された数量

次の CTE は、製品の累積数量を計算します。

WITH BOMCTE(itemid, containsid, qty, cumulativeqty)
AS
(
  SELECT *, qty
  FROM BOM
  WHERE itemid = 'A'

  UNION ALL

  SELECT BOM.*, BOM.qty * BOMCTE.cumulativeqty
  FROM BOM
    JOIN BOMCTE
      ON BOM.itemid = BOMCTE.containsid
)
SELECT * FROM BOMCTE

結果セットは以下のようになります。

itemid containsid qty         cumulativeqty
------ ---------- ----------- -------------
A      B          2           2            
A      C          2           2            
A      D          2           2            
C      B          2           4            
C      E          3           6            
E      J          1           6            
B      F          1           4            
B      G          3           12           
B      F          1           2            
B      G          3           6     

この CTE は、cumulativeqty 列を前の CTE に追加します。 アンカー メンバは、含まれている項目の数量を cumulativeqty として返します。 再帰メンバが、次のレベルに含まれている項目ごとに、項目の数量とその項目に含まれている項目の累積数量を乗算します。 複数の経路から到達される項目は、複数回結果に表示されることに注意してください。つまり、経路ごとの累積数量が表示されます。 このような出力は、それ自体ではあまり意味がありませんが、各項目が一度だけ表示される最終的な結果への中間ステップを理解するのに役立ちます。 A に含まれる各項目の合計数量を取得するには、以下のように、外部クエリ グループに containsid ごとの結果を保持します。

WITH BOMCTE(itemid, containsid, qty, cumulativeqty)
AS
(
  SELECT *, qty
  FROM BOM
  WHERE itemid = 'A'

  UNION ALL

  SELECT BOM.*, BOM.qty * BOMCTE.cumulativeqty
  FROM BOM
    JOIN BOMCTE
      ON BOM.itemid = BOMCTE.containsid
)
SELECT containsid AS itemid, SUM(cumulativeqty) AS totalqty
FROM BOMCTE
GROUP BY containsid

結果セットは以下のようになります。

itemid totalqty   
------ -----------
B      6          
C      2          
D      2          
E      6          
F      6          
G      18         
J      6     

PIVOT および UNPIVOT

PIVOT および UNPIVOT は、クエリの FROM 句で指定する新しいリレーショナル演算子です。 これらのリレーショナル演算子を使用すると、入力テーブル値式で何らかの操作を実行し、出力テーブルに結果を表示できます。 PIVOT 演算子は、行を列に変換し、変換を終えるまで集計を実行します。 指定したピボット列に基づいて入力テーブル式を拡大して、ピボット列内の一意の値それぞれに対する列を持つ出力テーブルを生成します。 UNPIVOT 演算子は、PIVOT 演算子とは逆の操作を実行します。つまり、列を行に変換します。 ピボット テーブル列に基づいて入力テーブル式を絞り込みます。

PIVOT

PIVOT 演算子は、オープン スキーマのシナリオの処理、およびクロス集計レポートの生成に役立ちます。

オープン スキーマのシナリオでは、各エンティティの種類に対して不明または異なる一連の属性を持つエンティティを管理します。 アプリケーションのユーザーは、属性を動的に定義します。 多くの列を事前定義して、テーブルに NULL 値を多く格納するのではなく、属性を異なる行に分割し、エンティティの各インスタンスに関連する属性のみを格納します。

PIVOT を使用すると、オープン スキーマのシナリオや他のシナリオ向けのクロス集計レポートを生成できます。その際、行を列に変換し、変換が終わるまで集計を計算して役立つ形式でデータを表示できます。

オープン スキーマのシナリオの例として、オークションへの品目の売り出しを追跡するデータベースがあります。 品目の種類、品目の作成時期、および品目の初期価格など、一部の属性は、オークションのすべての品目に関連します。 すべての品目に関連する属性のみが AuctionItems テーブルに格納されます。

CREATE TABLE AuctionItems
(
  itemid       INT          NOT NULL PRIMARY KEY NONCLUSTERED,
  itemtype     NVARCHAR(30) NOT NULL,
  whenmade     INT          NOT NULL,
  initialprice MONEY        NOT NULL,
  /* other columns */
)

CREATE UNIQUE CLUSTERED INDEX idx_uc_itemtype_itemid
  ON AuctionItems(itemtype, itemid)

INSERT INTO AuctionItems VALUES(1, N'Wine',     1822,      3000)
INSERT INTO AuctionItems VALUES(2, N'Wine',     1807,       500)
INSERT INTO AuctionItems VALUES(3, N'Chair',    1753,    800000)
INSERT INTO AuctionItems VALUES(4, N'Ring',     -501,   1000000)
INSERT INTO AuctionItems VALUES(5, N'Painting', 1873,   8000000)
INSERT INTO AuctionItems VALUES(6, N'Painting', 1889,   8000000)

他の属性は品目の種類に固有なので、別の種類の新しい品目を引き続き追加します。 このような属性は、品目の各属性が別の行に格納される、別の ItemAttributes テーブルに格納できます。 各行には、次のように、品目の ID、属性名、および属性値が含まれます。

CREATE TABLE ItemAttributes
(
  itemid    INT          NOT NULL REFERENCES AuctionItems,
  attribute NVARCHAR(30) NOT NULL,
  value     SQL_VARIANT  NOT NULL, 
  PRIMARY KEY (itemid, attribute)
)

INSERT INTO ItemAttributes
  VALUES(1, N'manufacturer', CAST(N'ABC'              AS NVARCHAR(30)))
INSERT INTO ItemAttributes
  VALUES(1, N'type',         CAST(N'Pinot Noir'       AS NVARCHAR(15)))
INSERT INTO ItemAttributes
  VALUES(1, N'color',        CAST(N'Red'              AS NVARCHAR(15)))
INSERT INTO ItemAttributes
  VALUES(2, N'manufacturer', CAST(N'XYZ'              AS NVARCHAR(30)))
INSERT INTO ItemAttributes
  VALUES(2, N'type',         CAST(N'Porto'            AS NVARCHAR(15)))
INSERT INTO ItemAttributes
  VALUES(2, N'color',        CAST(N'Red'              AS NVARCHAR(15)))
INSERT INTO ItemAttributes
  VALUES(3, N'material',     CAST(N'Wood'             AS NVARCHAR(15)))
INSERT INTO ItemAttributes
  VALUES(3, N'padding',      CAST(N'Silk'             AS NVARCHAR(15)))
INSERT INTO ItemAttributes
  VALUES(4, N'material',     CAST(N'Gold'             AS NVARCHAR(15)))
INSERT INTO ItemAttributes
  VALUES(4, N'inscription',  CAST(N'One ring ...'     AS NVARCHAR(50)))
INSERT INTO ItemAttributes
  VALUES(4, N'size',         CAST(10                  AS INT))
INSERT INTO ItemAttributes
  VALUES(5, N'artist',       CAST(N'Claude Monet'     AS NVARCHAR(30)))
INSERT INTO ItemAttributes
  VALUES(5, N'name',         CAST(N'Field of Poppies' AS NVARCHAR(30)))
INSERT INTO ItemAttributes
  VALUES(5, N'type',         CAST(N'Oil'              AS NVARCHAR(30)))
INSERT INTO ItemAttributes
  VALUES(5, N'height',       CAST(19.625              AS NUMERIC(9,3)))
INSERT INTO ItemAttributes
  VALUES(5, N'width',        CAST(25.625              AS NUMERIC(9,3)))
INSERT INTO ItemAttributes
  VALUES(6, N'artist',       CAST(N'Vincent Van Gogh' AS NVARCHAR(30)))
INSERT INTO ItemAttributes
  VALUES(6, N'name',         CAST(N'The Starry Night' AS NVARCHAR(30)))
INSERT INTO ItemAttributes
  VALUES(6, N'type',         CAST(N'Oil'              AS NVARCHAR(30)))
INSERT INTO ItemAttributes
  VALUES(6, N'height',       CAST(28.75               AS NUMERIC(9,3)))
INSERT INTO ItemAttributes
  VALUES(6, N'width',        CAST(36.25               AS NUMERIC(9,3)))

value 列には、sql_variant データ型が使用されることに注意してください。これは、さまざまな属性値がさまざまなデータ型を持つ可能性があるためです。 たとえば、size 属性には整数の属性値が格納され、name 属性には文字列の属性値が格納されます。

絵画 (品目 5 および 6) の各品目の行、および各属性の列を持つ ItemAttributes テーブルからデータを表示するとします。 PIVOT 演算子を使用しない場合、以下のようにクエリを記述する必要があります。

SELECT
  itemid,
  MAX(CASE WHEN attribute = 'artist'  THEN value END) AS [artist],
  MAX(CASE WHEN attribute = 'name'    THEN value END) AS [name],
  MAX(CASE WHEN attribute = 'type'    THEN value END) AS [type],
  MAX(CASE WHEN attribute = 'height'  THEN value END) AS [height],
  MAX(CASE WHEN attribute = 'width'   THEN value END) AS [width]
FROM ItemAttributes AS ATR
WHERE itemid IN(5,6)
GROUP BY itemid

結果セットは以下のようになります。

itemid artist           name             type       height width
------ ---------------- ---------------- ---------- ------ ------
5      Claude Monet     Field of Poppies Oil        19.625 25.625
6      Vincent Van Gogh The Starry Night Oil        28.750 36.250

PIVOT 演算子を使用すると、以下のように、より短くて読みやすいコードで、同じ結果を得ることができます。

SELECT *
FROM ItemAttributes AS ATR
  PIVOT
  (
    MAX(value)
    FOR attribute IN([artist], [name], [type], [height], [width])
  ) AS PVT
WHERE itemid IN(5,6)

最新の機能を理解する場合と同様に、PIVOT 演算子を理解するには、試しに使ってみることです。 PIVOT 構文のいくつかの要素は、見ただけで理解できます。それらの要素と新しい演算子を使用しないクエリのリレーションシップを理解することが必要なだけです。それ以外の要素は明確です。

以下にいくつかの用語を示します。これらの用語は、PIVOT 演算子のセマンティクスを理解するのに役立ちます。

table_expression (テーブル式)

PIVOT 演算子が動作する仮想テーブル (FROM 句および PIVOT 演算子間のクエリの一部) です。上記の場合は、ItemAttributes AS ATR に相当します。

pivot_column (ピボット列)

結果列に変換する値を持つ、table_expression から取得される列です。上記の場合は、attribute に相当します。

column_list (列リスト)

結果列として表示する、pivot_column から取得される値の一覧 (IN 句の後のカッコ内) です。 これらは、有効な識別子として表される必要があります。上記の場合は、[artist]、[name]、[type]、[height]、[width] に相当します。

aggregate_function (集計関数)

結果のデータまたは列の値を生成するために使用する集計関数です。上記の場合は、MAX() に相当します。

value_column (値列)

aggregate_function の引数として使用する、table_expression から取得される列です。上記の場合は、value に相当します。

group_by_list (GROUP BY リスト)

非表示の部分、つまり、結果をグループ化するために使用される、pivot_column および value_column を除外する table_expression から取得されるすべての列です。上記の場合は、itemid に相当します。

select_list (選択リスト)

SELECT 句に基づいた列の一覧で、group_by_list および column_list からの列を含めることができます。 別名を使用して、結果列の名前を変更できます。上記の場合は、* に相当し、group_by_list および column_list に含まれるすべての列を返します。

PIVOT 演算子は、GROUP BY 句を含むクエリがあり、それらの列を指定したかのように、group_by_list の一意の値ごとに 1 行を返します。 group_by_list がクエリ内のどの場所にも明示的に指定されていないことがわかります。これは、暗黙的といえます。 table_expression から、pivot_column および value_column 以外のすべての列が含まれます。 これを理解することは、PIVOT 演算子を使用して記述したクエリが動作する理由、および一部のケースでエラーになる理由を理解するのに重要であると考えられます。

結果列には、group_by_list および <column_list> からの値が含まれると考えられます。 アスタリスク (*) を指定すると、クエリにより両方の一覧が返されます。 結果列のデータ部分または結果列の値が、引数として value_column を使用する aggregate_function によって計算されます。

以下のコードは、PIVOT 演算子を使用するクエリ内のさまざまな要素を例示しています。

SELECT * -- itemid, [artist], [name], [type], [height], [width]
FROM ItemAttributes AS ATR
  PIVOT
  (
    MAX(value)
    FOR attribute IN([artist], [name], [type], [height], [width])
  ) AS PVT
WHERE itemid IN(5,6)

以下のコードは、PIVOT 演算子を使用しないクエリにさまざまな要素を関連付けています。

SELECT
  itemid,
  MAX(CASE WHEN attribute = 'artist'  THEN value END) AS [artist],
  MAX(CASE WHEN attribute = 'name'    THEN value END) AS [name],
  MAX(CASE WHEN attribute = 'type'    THEN value END) AS [type],
  MAX(CASE WHEN attribute = 'height'  THEN value END) AS [height],
  MAX(CASE WHEN attribute = 'width'   THEN value END) AS [width]
FROM ItemAttributes AS ATR
WHERE itemid IN(5,6)
GROUP BY itemid

<column_list> には、値を明示的に指定する必要があることに注意してください。 PIVOT 演算子には、静的クエリの pivot_column から動的に値を派生するオプションは用意されていません。 この処理を行うには、動的 SQL を使用して、クエリ文字列をユーザー自身で作成します。

前の PIVOT クエリをさらに進めて、各オークション品目の絵画に関連するすべての属性を返し、AuctionItems に存在する属性と、ItemAttributes に存在する属性を含めるとします。 以下のクエリを実行してみると、エラーが返されます。

SELECT *
FROM AuctionItems AS ITM
  JOIN ItemAttributes AS ATR
    ON ITM.itemid = ATR.itemid
  PIVOT
  (
    MAX(value)
    FOR attribute IN([artist], [name], [type], [height], [width])
  ) AS PVT
WHERE itemtype = 'Painting'

以下のエラー メッセージが表示されます。

.Net SqlClient Data Provider:  Msg 8156, Level 16, State 1, Line 1
(メッセージ 8156、レベル 16、状態 1、行 1)
The column 'itemid' was specified multiple times for 'PVT'.
(列名 'itemid' は 'PVT' に対して複数回指定されています。)
.Net SqlClient Data Provider:  Msg 107, Level 15, State 1, Line 1
(メッセージ 107、レベル 15、状態 1、行 1)
The column prefix 'ITM' does not match with a table name or alias name used in the query.
(列のプレフィックス 'ITM' は、テーブル名と一致しないか、クエリ内の別名と一致しません。)
 

PIVOT は、table_expression で動作し、FROM 句と PIVOT 句間のクエリのセクションによって返される仮想テーブルであることを覚えておいてください。 このクエリでは、仮想テーブルに itemid 列の 2 つのインスタンスが含まれています。1 つは AuctionItems で、もう 1 つは ItemAttributes で作成されたインスタンスです。 以下のようにクエリを変更できますが、この場合もエラーが発生します。

SELECT ITM.itemid, itemtype, whenmade, initialprice, 
  [artist], [name], [type], [height], [width]
FROM AuctionItems AS ITM
  JOIN ItemAttributes AS ATR
    ON ITM.itemid = ATR.itemid
  PIVOT
  (
    MAX(value)
    FOR attribute IN([artist], [name], [type], [height], [width])
  ) AS PVT
WHERE itemtype = 'Painting'

以下のエラー メッセージが表示されます。

.Net SqlClient Data Provider: Msg 8156, Level 16, State 1, Line 1 (メッセージ 8156、レベル 16、状態 1、行 1) The column 'itemid' was specified multiple times for 'PVT'. (列名 'itemid' は 'PVT' に対して複数回指定されています。) .Net SqlClient Data Provider: Msg 107, Level 15, State 1, Line 1 (メッセージ 107、レベル 15、状態 1、行 1) The column prefix 'ITM' does not match with a table name or alias name used in the query. (列のプレフィックス 'ITM' は、テーブル名と一致しないか、クエリ内の別名と一致しません。) 

上記のとおり、PIVOT 演算子は、select_list の列ではなく、table_expression によって返される仮想テーブル上で動作します。 select_list は、PIVOT 演算子がその操作を実行した後で評価され、group_by_list および column_list のみを参照できます。 それが理由で、別名 ITM は select_list で認識されなくなります。 これを理解すると、動作する列のみを含む table_expression に PIVOT を指定する必要があることに気付きます。 これには、グループ列 (itemid と itemtype、whenmade、およびinitialprice の 1 つだけ存在)、ピボット列 (attribute)、および値列 (value) が含まれます。 CTE または派生テーブルを使用して、これを実現できます。 以下に、CTE を使用する例を示します。

WITH PNT
AS
(
  SELECT ITM.*, ATR.attribute, ATR.value
  FROM AuctionItems AS ITM
    JOIN ItemAttributes AS ATR
      ON ITM.itemid = ATR.itemid
  WHERE ITM.itemtype = 'Painting'
)
SELECT * FROM PNT
  PIVOT
  (
    MAX(value)
    FOR attribute IN([artist], [name], [type], [height], [width])
  ) AS PVT

結果セットは以下のようになります。

itemid itemtype whenmade initialprice artist           name             type height width
------ -------- -------- ------------ ---------------- ---------------- ---- ------ -----
5      Painting 1873     8000000.0000 Claude Monet     Field of Poppies Oil  19.62  25.62
6      Painting 1889     8000000.0000 Vincent Van Gogh The Starry Night Oil  28.75  36.25

以下に、派生テーブルを使用する例を示します。

SELECT *
FROM (SELECT ITM.*, ATR.attribute, ATR.value
      FROM AuctionItems AS ITM
            JOIN ItemAttributes AS ATR
            ON ITM.itemid = ATR.itemid
      WHERE ITM.itemtype = 'Painting') AS PNT
  PIVOT
  (
    MAX(value)
    FOR attribute IN([artist], [name], [type], [height], [width])
  ) AS PVT

また、クロス集計レポートを生成してデータを要約するときに、PIVOT を使用することもできます。 たとえば、Northwind データベースの Orders テーブルを使用して、列に従業員 ID をピボットし、各従業員が担当する顧客ごとの注文数を返すとします。 PIVOT 演算子を関連データのみで指定する必要があるのと関係して、派生テーブルを使用して次のクエリを記述します。

SELECT CustomerID,
  [1] AS Emp1, [2] AS Emp2, [3] AS Emp3, [4] AS Emp4, [5] AS Emp5,
  [6] AS Emp6, [7] AS Emp7, [8] AS Emp8, [9] AS Emp9
FROM (SELECT OrderID, CustomerID, EmployeeID
      FROM Orders) ORD
  PIVOT
  (
   COUNT(OrderID)
   FOR EmployeeID IN([1], [2], [3], [4], [5], [6], [7], [8], [9])
  ) AS PVT

結果セットは以下のようになります。

CustomerID Emp1 Emp2 Emp3 Emp4 Emp5 Emp6 Emp7 Emp8 Emp9       
---------- ---- ---- ---- ---- ---- ---- ---- ---- ----
ALFKI      2    0    1    2    0    1    0    0    0   
ANATR      0    0    2    1    0    0    1    0    0   
ANTON      1    0    3    1    0    0    2    0    0   
AROUT      3    0    2    4    0    1    0    1    2   
BERGS      4    1    6    1    2    0    0    2    2   
BLAUS      0    0    1    1    0    1    0    1    3   

COUNT(OrderID) 関数は、一覧内の従業員ごとの行数をカウントします。 PIVOT では COUNT(*) を使用できないことに注意してください。 列の別名を使用して、結果列により理解しやすい名前を指定します。 従業員が少数で、従業員 ID が事前にわかっている場合、PIVOT を使用して別の列に含まれる各従業員の注文数を表示するのが合理的です。

また、式から派生した値のピボットを行うこともできます。 たとえば、年を列にピボットして、注文年ごとに、各従業員の合計運送料の値を返すとします。 注文年は、以下のように、OrderDate 列から派生します。

SELECT EmployeeID, [1996] AS Y1996, [1997] AS Y1997, [1998] AS Y1998
FROM (SELECT EmployeeID, YEAR(OrderDate) AS OrderYear, Freight
      FROM Orders) AS ORD
  PIVOT
  (
   SUM(Freight)
   FOR OrderYear IN([1996], [1997], [1998])
  ) AS PVT

結果セットは以下のようになります。

EmployeeID  Y1996                     Y1997                     Y1998
----------- ------------------------- ------------------------- ---------
8           1258.4000                 2935.2800                 3294.2000
4           1968.5900                 6648.6000                 2728.9500
3            880.0300                 6918.3400                 3086.3700
7            664.3200                 3240.6600                 2760.4600
6            766.1000                 2114.1700                  900.2000
2            973.1800                 3796.4100                 3926.8200
9            532.8400                 1046.0900                 1747.3300
5           1365.3700                 1184.7500                 1368.5900
1           1871.0400                 4584.4700                 2381.1300

データ ウェアハウスのシナリオでは、クロス集計レポートを使用するのが一般的です。 次の OrdersFact テーブルを考えてみましょう。このテーブルには、Northwind から注文データと注文の詳細データが挿入されます。

CREATE TABLE OrdersFact
(
  OrderID    INT      NOT NULL,
  ProductID  INT      NOT NULL,
  CustomerID NCHAR(5) NOT NULL,
  EmployeeID INT      NOT NULL,
  OrderYear  INT      NOT NULL,
  OrderMonth INT      NOT NULL,
  OrderDay   INT      NOT NULL,
  Quantity   INT      NOT NULL,
  PRIMARY KEY(OrderID, ProductID)
)
INSERT INTO OrdersFact
  SELECT O.OrderID, OD.ProductID, O.CustomerID, O.EmployeeID,
    YEAR(O.OrderDate) AS OrderYear, MONTH(O.OrderDate) AS OrderMonth,
    DAY(O.OrderDate) AS OrderDay, OD.Quantity
  FROM Northwind.dbo.Orders AS O
    JOIN Northwind.dbo.[Order Details] AS OD
      ON O.OrderID = OD.OrderID

行の年と列の月を返して、各年と月の合計数量を取得するには、次のクエリを使用します。

SELECT *
FROM (SELECT OrderYear, OrderMonth, Quantity
      FROM OrdersFact) AS ORD
 PIVOT
 (
  SUM(Quantity)
  FOR OrderMonth IN([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12])
 ) AS PVT

結果セットは以下のようになります。

OrderYear 1     2     3     4     5     6     7     8     9     10    11    12
--------- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
1997      2401  2132  1770  1912  2164  1635  2054  1861  2343  2679  1856  2682
1996      NULL  NULL  NULL  NULL  NULL  NULL  1462  1322  1124  1738  1735  2200 
1998      3466  3115  4065  4680  921   NULL  NULL  NULL  NULL  NULL  NULL  NULL

PIVOT は、年と月の交差部分が存在しない場合は NULL 値を返します。 年は、指定した月のいずれかとの交差部分があるかどうかに関係なく、入力テーブル式 (派生テーブル ORD) に表示されれば、結果に表示されます。 つまり、既存の月をすべて指定しない場合に、すべての列で値が NULL の行を取得する場合があります。 ただし、結果の NULL 値は、交差部分が存在しない場合には表示する必要がありません。 列が NULL 値を許可しない場合を除いては、数量列にベースの NULL 値が含まれる結果となる場合があります。 NULL を上書きして、代わりに別の値 (0 など) を表示する場合、SELECT リストに ISNULL() 関数を使用することで、その処理を行うことができます。

SELECT OrderYear,
  ISNULL([1],  0) AS M01,
  ISNULL([2],  0) AS M02,
  ISNULL([3],  0) AS M03,
  ISNULL([4],  0) AS M04,
  ISNULL([5],  0) AS M05,
  ISNULL([6],  0) AS M06,
  ISNULL([7],  0) AS M07,
  ISNULL([8],  0) AS M08,
  ISNULL([9],  0) AS M09,
  ISNULL([10], 0) AS M10,
  ISNULL([11], 0) AS M11,
  ISNULL([12], 0) AS M12
FROM (SELECT OrderYear, OrderMonth, Quantity
      FROM OrdersFact) AS ORD
 PIVOT
 (
  SUM(Quantity)
  FOR OrderMonth IN([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12])
 ) AS PVT

結果セットは以下のようになります。

OrderYear M01   M02   M03   M04   M05   M06   M07   M08   M09   M10   M11   M12   
--------- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
1997      2401  2132  1770  1912  2164  1635  2054  1861  2343  2679  1856  2682 
1996      0     0     0     0     0     0     1462  1322  1124  1738  1735  2200 
1998      3466  3115  4065  4680  921   0     0     0     0     0     0     0

派生テーブル内で ISNULL(Quantity, 0) を使用する場合、存在しない交差部分に PIVOT が生成した NULL 値ではなく、Quantity 列のベースの NULL 値 (これが存在する場合) のみを処理します。

各年度の第 1 四半期の年と月の値の組み合わせごとに、各従業員の合計数量を返すとします。 行に含まれる年と月の値、および列に含まれる従業員 ID を取得するには、次のクエリを使用します。

SELECT *
FROM (SELECT EmployeeID, OrderYear, OrderMonth, Quantity
      FROM OrdersFact
      WHERE OrderMonth IN(1,2,3)) AS ORD
  PIVOT
  (
    SUM(Quantity)
    FOR EmployeeID IN([1],[2],[3],[4],[5],[6],[7],[8],[9])
  ) AS PVT

結果セットは以下のようになります。

OrderYear OrderMonth  1    2    3    4    5    6    7    8    9   
--------- ----------- ---- ---- ---- ---- ---- ---- ---- ---- ----
1997      1           304  230  364  812  NULL 64   248  305  74  
1998      1           397  252  745  586  344  97   265  543  237 
1997      2           168  36   574  675  NULL 117  145  417  NULL
1998      2           566  290  662  554  276  129  328  13   297 
1997      3           275  151  198  267  188  75   191  288  137 
1998      3           467  558  578  481  157  288  307  912  317

EmployeeID および Quantity がそれぞれピボット列と値列として使用されるので、この場合の暗黙的な group-by リストは、OrderYear および OrderMonth です。

ただし、年と月の値の組み合わせを列として表示する場合、ピボット列は 1 つしか存在できないので、PIVOT 演算子にそれらの組み合わせを渡す前に、年と月の値を連結する必要があります。

SELECT *
FROM (SELECT EmployeeID, OrderYear*100+OrderMonth AS YM, Quantity
      FROM OrdersFact
      WHERE OrderMonth IN(1,2,3)) AS ORD
  PIVOT
  (
    SUM(Quantity)
    FOR YM IN([199701],[199702],[199703],[199801],[199802],[199803])
  ) AS PVT

結果セットは以下のようになります。

EmployeeID  199701     199702     199703     199801     199802     199803
----------- ---------- ---------- ---------- ---------- ---------- -----
1           304        168        275        397        566        467
2           230         36        151        252        290        558
3           364        574        198        745        662        578
4           812        675        267        586        554        481
5           NULL       NULL       188        344        276        157
6            64        117         75         97        129        288
7           248        145        191        265        328        307
8           305        417        288        543         13        912
9            74        NULL       137        237        297   
UNPIVOT

UNPIVOT 演算子により、ピボット化されていないデータを正規化できます。 UNPIVOT 演算子の構文と要素は、PIVOT 演算子の構文と要素に似ています。

一例として、上記のセクションの AuctionItems テーブルを考えてみましょう。

itemid      itemtype                 whenmade    initialprice
----------- ------------------------ ----------- --------------
1           Wine                      1822          3000.0000
2           Wine                      1807           500.0000
3           Chair                    1753         800000.0000
4           Ring                      -501       1000000.0000
5           Painting                  1873       8000000.0000
6           Painting                  1889       8000000.0000

ここで、以下の ItemAttributes テーブルに保持されているのと同様の方法で、各属性を異なる行に表示するとします。

itemid      attribute       value
----------- --------------- -------
1           itemtype        Wine
1           whenmade        1822
1           initialprice    3000.00
2           itemtype        Wine
2           whenmade        1807
2           initialprice    500.00
3           itemtype        Chair
3           whenmade        1753
3           initialprice    800000.00
4           itemtype        Ring
4           whenmade        -501
4           initialprice    1000000.00
5           itemtype        Painting
5           whenmade        1873
5           initialprice    8000000.00
6           itemtype        Painting
6           whenmade        1889
6           initialprice    8000000.00

UNPIVOT クエリでは、列 itemtype、whenmade、および initialprice を行に変換したいと考えます。 各行には、項目 ID、属性、および値が含まれる必要があります。 指定する必要のある新しい列名は、attribute と value です。 これらの列は、PIVOT 演算子の pivot_columnvalue_column に対応します。 attribute 列は、変換する実際の列名 (itemtype、whenmade、および initialprice) を値として取得する必要があります。 value 列は、3 つの変換元の列から 1 つの変換先の列に値を取得する必要があります。 わかりやすくするために、まず、無効な UNPIVOT クエリを示し、次に、一部制限が適用される有効なクエリを示します。

SELECT itemid, attribute, value
FROM AuctionItems
  UNPIVOT
  (
    value FOR attribute IN([itemtype], [whenmade], [initialprice])
  ) AS UPV

PIVOT 演算子の引数と同様に、FOR 句の前に value_column の名前 (この場合は、value) を指定します。 FOR 句に続いて pivot_column の名前 (この場合は、attribute)を指定し、次に、pivot_column の値として取得する変換元列名のリストを保持する IN 句を指定します。 列のリストは、PIVOT 演算子では <column_list> と呼ばれます。 このクエリは、以下のエラーを発生します。

.Net SqlClient Data Provider:  Msg 8167, Level 16, State 1, Line 1(メッセージ 8167、レベル 16、状態 1、行 1)
Type of column 'whenmade' conflicts with the type of other columns specified in the UNPIVOT list.(列 'whenmade' の型が UNPIVOT 一覧で指定された他の列の型と競合しています。)
 

変換先の value 列には、いくつか異なる変換元列 (これらの列は <column_list> で示されます) からの値が含まれます。 すべての列値の対象が単一の列なので、UNPIVOT では <column_list> のすべての列が同じデータ型、長さ、および有効桁数を持つ必要があります。 この制限を満たすために、3 つの列を同じデータ型に変換するテーブル式を UNPIVOT 演算子に指定できます。 異なる変換元列を同じデータ型に変換し、かつ、元の値を保持できるので、sql_variant データ型がこの場合に使用する適切な候補になります。 この制限を適用することによって上記のクエリを以下のように改定し、目的の結果を得ることができます。

SELECT itemid, attribute, value
FROM (SELECT itemid,
        CAST(itemtype     AS SQL_VARIANT) AS itemtype,
        CAST(whenmade     AS SQL_VARIANT) AS whenmade,
        CAST(initialprice AS SQL_VARIANT) AS initialprice
      FROM AuctionItems) AS ITM
  UNPIVOT
  (
    value FOR attribute IN([itemtype], [whenmade], [initialprice])
  ) AS UPV

結果の attribute 列のデータ型は sysname になります。 これは、SQL Server がオブジェクト名を格納するのに使用するデータ型です。

UNPIVOT 演算子は、結果から value 列の NULL 値を取り除くので、PIVOT 演算子とまったく逆の演算が行われるとは考えないでください。

AuctionItems の列を行に変換することによって、ひとまとまりの結果を提供するために、UNPIVOT 演算の結果と ItemAttributes テーブルからの行の和集合 (ユニオン) を作成できるようになります。

SELECT itemid, attribute, value
FROM (SELECT itemid,
        CAST(itemtype     AS SQL_VARIANT) AS itemtype,
        CAST(whenmade     AS SQL_VARIANT) AS whenmade,
        CAST(initialprice AS SQL_VARIANT) AS initialprice
      FROM AuctionItems) AS ITM
  UNPIVOT
  (
    value FOR attribute IN([itemtype], [whenmade], [initialprice])
  ) AS UPV

UNION ALL

SELECT *
FROM ItemAttributes
ORDER BY itemid, attribute

結果セットは以下のようになります。

itemid      attribute       value
----------- --------------- -------------
1           color           Red
1           initialprice    3000.00
1           itemtype        Wine
1           manufacturer    ABC
1           type            Pinot Noir
1           whenmade        1822
2           color           Red
2           initialprice    500.00
2           itemtype        Wine
2           manufacturer    XYZ
2           type            Porto
2           whenmade        1807
3           initialprice    800000.00
3           itemtype        Chair
3           material        Wood
3           padding         Silk
3           whenmade        1753
4           initialprice    1000000.00
4           inscription     One ring
4           itemtype        Ring
4           material        Gold
4           size            10
4           whenmade        -501
5           height          19.625
5           initialprice    8000000.00
5           itemtype        Painting
5           name            Field of Poppies
5           artist          Claude Monet
5           type            Oil
5           whenmade        1873
5           width           25.625
6           height          28.750
6           initialprice    8000000.00
6           itemtype        Painting
6           name            The Starry Night
6           artist          Vincent Van Gogh
6           type            Oil
6           whenmade        1889
6           width           36.250

APPLY

APPLY リレーショナル演算子を使用して、外部テーブル式の行ごとに一度、指定されたテーブル値関数を呼び出すことができます。 JOIN リレーショナル演算子を使用するのと同様の方法で、クエリの FROM 句で APPLY を指定します。 APPLY には、CROSS APPLY と OUTER APPLY の 2 つの形式があります。 SQL Server "Yukon" Beta 1 では、APPLY 演算子により、相関サブクエリのテーブル値関数機能を参照できます。

CROSS APPLY

CROSS APPLY は、外部テーブル式の行ごとにテーブル値関数を呼び出します。 テーブル値関数の引数として外部テーブルの列を参照できます。 CROSS APPLY では、テーブル値関数の個別の呼び出しから返されたすべての結果から成る統一された結果セットが返されます。 テーブル値関数により、特定の外部行から空のセットが返される場合、その外部行は結果に返されません。 たとえば、以下のテーブル値関数は、2 つの整数値を受け取り、列として最大値と最小値を持つ、1 行のテーブルを 1 つ返します。

CREATE FUNCTION dbo.fn_scalar_min_max(@p1 AS INT, @p2 AS INT) RETURNS TABLE
AS
RETURN
  SELECT
    CASE
     WHEN @p1 < @p2 THEN @p1
     WHEN @p2 < @p1 THEN @p2
      ELSE COALESCE(@p1, @p2)
   END AS mn,
   CASE     WHEN @p1 > @p2 THEN @p1
    WHEN @p2 > @p1 THEN @p2
      ELSE COALESCE(@p1, @p2)
    END AS mx
GO

SELECT * FROM fn_scalar_min_max(10, 20)結果セットは以下のようになります。
mn          mx         
----------- -----------
10          20        

以下のような T1 テーブルがあるとします。

CREATE TABLE T1
(
  col1 INT NULL,
  col2 INT NULL
)

INSERT INTO T1 VALUES(10, 20)
INSERT INTO T1 VALUES(20, 10)
INSERT INTO T1 VALUES(NULL, 30)
INSERT INTO T1 VALUES(40, NULL)
INSERT INTO T1 VALUES(50, 50)

T1 の行ごとに fn_scalar_min_max を呼び出すとします。 その場合、以下のような CROSS APPLY クエリを記述できます。

SELECT *
FROM T1 CROSS APPLY fn_scalar_min_max(col1, col2) AS M

結果セットは以下のようになります。

col1        col2        mn          mx         
----------- ----------- ----------- -----------
10          20          10          20         
20          10          10          20         
NULL        30          30          30         
40          NULL        40          40         
50          50          50          50  

テーブル値関数により、特定の外部行に対して複数の行が返される場合、その外部行が複数回返されます。 再帰クエリのセクション (従業員組織図の例) で使用した Employees テーブルを考えます。 同じデータベースに以下の Departments テーブルも作成します。

CREATE TABLE Departments
(
  deptid    INT NOT NULL PRIMARY KEY,
  deptname  VARCHAR(25) NOT NULL,
  deptmgrid INT NULL REFERENCES Employees
)

SET NOCOUNT ON
INSERT INTO Departments VALUES(1, 'HR',           2)
INSERT INTO Departments VALUES(2, 'Marketing',    7)
INSERT INTO Departments VALUES(3, 'Finance',      8)
INSERT INTO Departments VALUES(4, 'R&D',          9)
INSERT INTO Departments VALUES(5, 'Training',     4)
INSERT INTO Departments VALUES(6, 'Gardening', NULL)

多くの部門は、Employees テーブルの従業員に対応するマネージャ ID を所持します。ただし、Gardening 部門のように、マネージャを持たない部門もあります。 この場合、Employees テーブルのマネージャが、必然的に部門を管理することになります。 以下のテーブル値関数は、従業員 ID を引数として受け取り、その従業員と、その従業員のあらゆるレベルの部下をすべて返します。

CREATE FUNCTION dbo.fn_getsubtree(@empid AS INT) RETURNS @TREE TABLE
(
  empid   INT NOT NULL,
  empname VARCHAR(25) NOT NULL,
  mgrid   INT NULL,
  lvl     INT NOT NULL
)
AS
BEGIN
  WITH Employees_Subtree(empid, empname, mgrid, lvl)
  AS
  ( 
    -- Anchor Member (AM)
    SELECT empid, empname, mgrid, 0
    FROM employees
    WHERE empid = @empid

    UNION all

    -- Recursive Member (RM)
    SELECT e.empid, e.empname, e.mgrid, es.lvl+1
    FROM employees AS e
      JOIN employees_subtree AS es
        ON e.mgrid = es.empid
  )
  INSERT INTO @TREE
    SELECT * FROM Employees_Subtree

  RETURN
END
GO

各部門のマネージャのあらゆるレベルの部下をすべて返すには、以下のクエリを使用します。

SELECT *
FROM Departments AS D
  CROSS APPLY fn_getsubtree(D.deptmgrid) AS ST

結果セットは以下のようになります。

deptid      deptname   deptmgrid   empid       empname    mgrid       lvl
----------- ---------- ----------- ----------- ---------- ----------- ---
1           HR         2           2           Andrew     1           0
1           HR         2           5           Steven     2           1
1           HR         2           6           Michael    2           1
2           Marketing  7           7           Robert     3           0
2           Marketing  7           11          David      7           1
2           Marketing  7           12          Ron        7           1
2           Marketing  7           13          Dan        7           1
2           Marketing  7           14          James      11          2
3           Finance    8           8           Laura      3           0
4           R&D        9           9           Ann        3           0
5           Training   4           4           Margaret   1           0
5           Training   4           10          Ina        4   

ここでは、注意する点が 2 つあります。 まず、部門のマネージャの fn_getsubtree から複数の行が返されるので、Departments からの各行が重複しています。 次に、Gardening 部門では fn_getsubtree が空のセットを返すので、この部門が結果セットに表示されません。

もう 1 つの CROSS APPLY 演算子の実際的な使い方は、グループごとに n 行を返すことを希望する一般的な要求に答えることです。 一例として、特定の顧客の最近の注文を指定した数だけ返す以下の関数を考えます。

USE Northwind
GO

CREATE FUNCTION fn_getnorders(@custid AS NCHAR(5), @n AS INT)
  RETURNS TABLE
AS
RETURN
  SELECT TOP(@n) *
  FROM Orders
  WHERE CustomerID = @custid
  ORDER BY OrderDate DESC
GO

CROSS APPLY 演算子を使用することにより、以下の簡単なクエリで、顧客ごとに最近の 2 つの注文を取得できます。

SELECT O.*
FROM Customers AS C
  CROSS APPLY fn_getnorders(C.CustomerID, 2) AS O

TOP の機能拡張の詳細については、この資料の後半の「TOP の機能強化」を参照してください。

OUTER APPLY

OUTER APPLY は CROSS APPLY によく似ていますが、テーブル値関数により空のセットが返された外部テーブルの行も返されます。 NULL 値は、テーブル値関数の列に対応する列の値として返されます。 一例として、上記のセクションの Departments テーブルに対するクエリを変更し、CROSS APPLY の代わりに OUTER APPLY を使用します。出力の最終行に注目してください。

SELECT *
FROM Departments AS D
  OUTER APPLY fn_getsubtree(D.deptmgrid) AS ST

結果セットは以下のようになります。

deptid      deptname   deptmgrid   empid       empname    mgrid       lvl
----------- ---------- ----------- ----------- ---------- ----------- ---
1           HR         2           2           Andrew     1           0
1           HR         2           5           Steven     2           1
1           HR         2           6           Michael    2           1
2           Marketing  7           7           Robert     3           0
2           Marketing  7           11          David      7           1
2           Marketing  7           12          Ron        7           1
2           Marketing  7           13          Dan        7           1
2           Marketing  7           14          James      11          2
3           Finance    8           8           Laura      3           0
4           R&D        9           9           Ann        3           0
5           Training   4           4           Margaret   1           0
5           Training   4           10          Ina        4           1
6           Gardening  NULL        NULL        NULL       NULL  
相関サブクエリでのテーブル値関数の使用

SQL Server 2000 では、相関サブクエリ内でテーブル値関数を参照できません。 APPLY リレーショナル演算子の提供に関連して、この制限が取り除かれます。 外部クエリからの列を引数として使用して、サブクエリ内でテーブル値関数を指定できるようになりました。 たとえば、少なくとも 3 人の従業員を部下として持つマネージャがいる部門だけを返す場合、以下のクエリを記述できます。

SELECT *
FROM Departments AS D
WHERE (SELECT COUNT(*)
       FROM fn_getsubtree(D.deptmgrid)) >= 3
deptid      deptname                  deptmgrid  
----------- ------------------------- -----------
1           HR                        2          
2           Marketing                 7    

新しい宣言参照整合性 (DRI) 操作の SET DEFAULT と SET NULL のサポート

ANSI SQL では、FOREIGN KEY 制約のサポートに、4 つの可能な参照操作が定義されています。 外部キーによって参照されるテーブルに対する DELETE 操作または UPDATE 操作への対応としてシステムが反応する方法を示すために、この 4 つの操作を指定します。 SQL Server 2000 は、このような 4 つの操作のうち NO ACTION と CASCADE の 2 つの操作をサポートします。 SQL Server "Yukon" Beta 1 では、SET DEFAULT と SET NULL の 2 つの参照操作に対するサポートが追加されます。

SET DEFAULT 参照操作および SET NULL 参照操作は、DRI 機能を拡張します。 これらのオプションを、外部キー宣言の ON UPDATE 句や ON DELETE 句と連携して使用します。 SET DEFAULT は、参照されるテーブルで行を削除 (ON DELETE) または参照されるキーを更新 (ON UPDATE) するときに、参照しているテーブルの関連行の参照している列値に SQL Server がその列の既定値を設定することを意味します。 同様に、SET NULL オプションを使用すると、参照している列が NULL 値を許容する場合に、SQL Server はそれに反応して値に NULL を設定できます。

たとえば、以下の Customers テーブルには、3 人の実在する顧客と 1 人のダミーの顧客が含まれています。

CREATE TABLE Customers
(
  customerid CHAR(5) NOT NULL,
  /* other columns */
  CONSTRAINT PK_Customers PRIMARY KEY(customerid)
)

INSERT INTO Customers VALUES('DUMMY')
INSERT INTO Customers VALUES('FRIDA')
INSERT INTO Customers VALUES('GNDLF')
INSERT INTO Customers VALUES('BILLY')

Orders テーブルは注文を監視します。 注文は、必ずしも実在の顧客に割り当てられる必要はありません。 注文を入力し、顧客 ID を指定しないと、既定では、その注文は DUMMY 顧客 ID に割り当てられます。 Customers テーブルからレコードが削除されるときに、SQL Server が Orders テーブルの関連行の customerid 列に NULL を設定することを希望するとします。 customerid 列に NULL を持つ注文は、"孤立する" ことになります。つまり、その注文はどの顧客にも所属しません。 Customers テーブルの customerid 列への更新も許可するとします。 Orders テーブルの関連行に連鎖更新を行いたいと考えますが、企業のビジネス ルールではそれが許可されておらず、変更された顧客 ID に所属する注文は、既定の顧客 (DUMMY) に関連付ける必要があります。 Customers テーブルの customerid 列を更新するときに、SQL Server が Orders テーブルの関連顧客 ID (customerid) に既定値 'DUMMY' を設定することを希望します。 この場合、以下のような外部キーを持つ Orders テーブルを作成し、いくつか注文を設定します。

CREATE TABLE Orders
(
  orderid    INT      NOT NULL,
  customerid CHAR(5)  NULL DEFAULT('DUMMY'),
  orderdate  DATETIME NOT NULL,
  CONSTRAINT PK_Orders PRIMARY KEY(orderid),
  CONSTRAINT FK_Orders_Customers
    FOREIGN KEY(customerid)
    REFERENCES Customers(customerid)
      ON DELETE SET NULL
      ON UPDATE SET DEFAULT
)

INSERT INTO Orders VALUES(10001, 'FRIDA', '20040101')
INSERT INTO Orders VALUES(10002, 'FRIDA', '20040102')
INSERT INTO Orders VALUES(10003, 'BILLY', '20040101')
INSERT INTO Orders VALUES(10004, 'BILLY', '20040103')
INSERT INTO Orders VALUES(10005, 'GNDLF', '20040104')
INSERT INTO Orders VALUES(10006, 'GNDLF', '20040105')

SET NULL オプションと SET DEFAULT オプションをテストするには、以下の DELETE ステートメントと UPDATE ステートメントを発行します。

DELETE FROM Customers
WHERE customerid = 'FRIDA'

UPDATE Customers
  SET customerid = 'DOLLY'
WHERE customerid = 'BILLY'

その結果、以下のように FRIDA の注文の customerid 列には NULL 値が割り当てられ、BILLY の注文には DUMMY が割り当てられます。

orderid     customerid orderdate             
----------- ---------- ----------------------
10001       NULL       1/1/2004 12:00:00 AM  
10002       NULL       1/2/2004 12:00:00 AM  
10003       DUMMY      1/1/2004 12:00:00 AM  
10004       DUMMY      1/3/2004 12:00:00 AM  
10005       GNDLF      1/4/2004 12:00:00 AM  
10006       GNDLF      1/5/2004 12:00:00 AM  

SET DEFAULT オプションを使用し、参照している列が NULL 以外の既定値を持ち、その既定値が参照されるテーブルに対応する値を持たない場合は、トリガとなる操作を実行したときに、エラーが発生します。 たとえば、Customers から DUMMY 顧客を削除し、次に、GNDLF の customerid を GLDRL に更新すると、エラーが発生します。 UPDATE が SET DEFAULT 操作のトリガとなり、GNDLF の元の注文を DUMMY 顧客 ID に割り当てようとしますが、Customers テーブルには対応する行がありません。

DELETE FROM Customers
WHERE customerid = 'DUMMY'

UPDATE Customers
  SET customerid = 'GLDRL'
WHERE customerid = 'GNDLF'

.Net SqlClient Data Provider:  Msg 547, Level 16, State 0, Line 1(メッセージ 547、レベル 16、状態 0、行 1)
UPDATE statement conflicted with COLUMN FOREIGN KEY constraint 'FK_Orders_Customers'.
(UPDATE ステートメントは COLUMN FOREIGN KEY で、制約 'FK_Orders_Customers' と矛盾しています。) 
The conflict occurred in database 'tempdb', table 'Customers', column 'customerid'.
(矛盾が発生したのは、データベース 'tempdb'、テーブル 'Customers'、列 'customerid' です。)
The statement has been terminated.
(ステートメントは終了されました。)

定義済みの参照動作を含む、外部キーの詳細については、sys.foreign_keys を参照してください。