japan.internet.com
japan.internet.com メンバーID
Twitter
Facebook
RSS
ピックアップ
2005年9月13日 10:00

Transact-SQLを使用したデータベース開発に役立つ13のヒント

著者Kevin S. Goffオリジナル版を読む海外海外発

はじめに

 アプリケーション開発者の多くは、SQL Server 2000を使用する中で、厄介な問題に直面します。たとえば、大量のデータの取得とグループ化、結果セットの作成、データ変更の追跡などです。いずれも、無限に近い選択肢の中から、絶妙かつ妥当な戦略をとる必要があります。その際に、ヒントがあれば助けとなるはずです。ここでは、初級開発者と熟練開発者の両方に役立つT-SQLのヒントをいくつか紹介します。

目的

 本稿は「The Baker’s Dozen」シリーズの一記事です。「The Baker’s Dozen」シリーズの主目的の1つは、特定の技術についてもっと上達したいと考えている人に対してヒントを提供することです。経験豊富な方にも、何かの役に立つヒントが1つくらいあるのではないかと思います。本シリーズの名前はVan Amsterdamというパン屋の話に由来しますが、私もこの数か月間、開発者の皆さんのお口に合うようなヒント集を焼き上げるためにいろいろ努力してきました。

 大部分のヒントの解説では、まず業務アプリケーションの要件を1つ提示し、それを解決するためのTransact-SQLのコードを紹介するという形を取ります。今回紹介するヒントは次のとおりです。

  • 単一のストアドプロシージャから複数の結果セットを返す[1]
  • ユーザー定義関数(UDF:User Defined Function)を作成して、スカラー値を返す[2]
  • 選択キーのコンマ区切りリストを解析するユーザー定義関数を作成し、それ以降のJOINステートメントで使用できるテーブルを返す[3]
  • LIKEを使用してテキスト検索ルーチンを作成する[4]
  • CASE構文を理解し、これを使用して実行時に条件を評価する[5]
  • サブクエリと派生テーブルを使用して要件に対処する方法を示し、これらの手法を比較検討する[6]
  • テーブル変数を使用し、テーブル変数と一時テーブルの違いを理解する[7]
  • DATEPART関数を使用して、週の末日に基づいて日別データを集計する[8]
  • CASTCONVERTなどの関数を使用して、異なるデータ型を併用する[9]
  • トリガを使用して、データベースの行が変更されたときに行う固有のアクションを定義する[10]
  • トリガを拡張して、基本的な監査証跡機能を実装する[11]
  • ダイナミックSQLを使用して、実行時までキー条件が不明なクエリを作成する[12]
  • 特定のデータベースから、テーブルおよび列のリストを取得する[13]

 では、実際に13のヒントを見ていきましょう。

ヒント1:複数の結果セットを返す

 要件:ある顧客のすべての注文情報を返す簡単なストアドプロシージャを作成する必要があります。結果セットには、「注文ヘッダー」「注文詳細」「注文メモ」という3つのテーブルを含めなくてはなりません。

 SQL Server 2000の単一のストアドプロシージャで複数の結果セットを返すのは簡単です。ストアドプロシージャのSQL SELECTステートメントでは、明示的な出力宣言(テーブルや変数などに対する出力)がない場合には、結果は呼び出し元のルーチンに返るというのが一般的な規則です。

 リスト1に示すのは、1つの引数(顧客のアカウントに対応する整数キー)を持つ、基本的なストアドプロシージャです。このプロシージャでは、「注文ヘッダー」「注文詳細」「注文メモ」の各テーブルに対してクエリを行っています。このプロシージャでは、table型の変数を作成し、3つのクエリすべてで使用する注文キーのリストを格納しています。テーブル変数の詳細については、ヒント7で説明します。

リスト1:複数の結果セットを返す単純なストアドプロシージャ
CREATE PROCEDURE GetOrdersByAcct (@iAcctKey integer) AS
   -- Retrieve Orders for a single customer
      -- Since we’re querying the order table three times,
      -- let’s create a table variable (@tOrderList) of all
   -- order keys for the specific customer. That way, we
   -- can JOIN off the temporary order list 
   DECLARE @tOrderList TABLE (OrderKey integer)
      INSERT INTO @tOrderList 
         SELECT OrderKey FROM OrderHdr 
            WHERE AcctKey = @iAcctKey
      SELECT OH.*, AM.AcctName 
         FROM OrderHdr OH
         JOIN AcctMast    AM ON AM.AcctKey = OH.AcctKey
         JOIN @tOrderList TMP ON TMP.OrderKey = OH.OrderKey
      SELECT OD.*, PM.ProdName
         FROM OrderDtl OD
         JOIN ProdMast PM ON PM.ProdKey = OD.ProdKey
         JOIN @tOrderList TMP ON TMP.OrderKey = OD.OrderKey
      SELECT * FROM OrderNotes ON
         JOIN @tOrderList TMP ON TMP.OrderKey = ON.OrderKey
   GO

 これら3つのクエリには出力先が指定されていないので、結果は呼び出し元のプロシージャに返ります。このストアドプロシージャはクエリアナライザのスクリプトから実行でき、3つの結果セットが表示されます。このストアドプロシージャをC#から呼び出す場合には、データアダプタを使用して、結果セット(データセット)に対して3つのデータテーブルを次のように格納します。

SqlDataAdapter oDa =
   new SqlDataAdapter(cSQLString, oMyConn);
oDa.Fill(DsReturn,"MyResults");
// Table 0 will contain the order headers
// Table 1 will contain the order details
// Table 2 will contain the order notes

 ストアドプロシージャの呼び出しでは、何らかの種類のデータアクセス手法を用いるのが普通です。「The Baker’s Dozen」シリーズでも、分散コンピューティング環境におけるこれらの種類の開発手法についていずれ解説します。

ヒント2:ユーザー定義関数で値を返す

 要件:締め日に基づいて顧客の借方と貸方の和を求め、顧客残高として返す必要があります。この計算を、複数のクエリで使用したいと考えています。

 SQL Server 2000では、開発者がユーザー定義関数を作成して呼び出すことができます。ユーザー定義関数を使用すると、ロジックと機能をカプセル化して、スカラー値を返すことができ、その値を複数のクエリで使用できます。リスト2は、顧客と締め日に応じて借方と貸方のテーブルから和を求めて結果を返す簡単なユーザー定義関数です。

リスト2:顧客残高を返すユーザー定義関数
CREATE FUNCTION dbo.GetCustomerBalance  (@iAcctKey integer, 
                        @dCutOffDate DateTime)
   -- Calculate and return balance for a Customer, 
   -- for a given point in time
   RETURNS Decimal AS  
   BEGIN
      DECLARE @nCreditAmount Decimal
      DECLARE @nDebitAmount  Decimal
      SET @nCreditAmount = (SELECT SUM(Amount) FROM Credits 
                  WHERE AcctKey = @iAcctKey
                  AND CompDate <= @dCutOffDate)
      SET @nDebitAmount = (SELECT SUM(Amount) FROM Debits
                  WHERE AcctKey = @iAcctKey
                  AND CompDate <= @dCutOffDate)
   -- depending on how data is stored, you may want to 
      -- handle the signs differently
      RETURN (ISNULL(@nCreditAmount,0) - ISNULL(@nDebitAmount,0))
   END

 この関数はクエリ内で次のように使用できます。

DECLARE @dCutOffDate DATETIME
SET @dCutOffDate = CAST(’11/30/2004’ AS DateTime)
SELECT CustID, dbo.GetCustomerBalance(CustID, @dCutOffDate)
AS CustBalance
FROM...

 顧客残高を必要とするクエリがアプリケーション内に5つある場合には、それぞれの場所でこのユーザー定義関数を使用すれば済みます。同じコードを各クエリに含める必要はありません。

 もちろんこれは、ユーザー定義関数の基本的な使用例に過ぎません。次は少しひねりを加えてみましょう。今回の残高計算ユーザー定義関数では、COMPDATE列に基づいて、「Debits」テーブルと「Credits」テーブルをクエリしています。ここでもし、「Debits」テーブルと「Credits」テーブルに、レコードの各段階を示す3つの日付、つまり、振り出し段階の日付を表すDRDATE、進行中の日付を表すWIPDATE、完了日付を表すCOMPDATEが格納されているとしたらどうでしょうか。残高計算ユーザー定義関数は、レコードのステータスを調べたうえで、どの日付と締め日を比較すべきかを判断しなくてはなりません。

 この残高計算ユーザー定義関数で、一連のIFステートメントまたはCASEステートメントを使用してステータスを読み取り、対応する日付列を使用するという方法も不可能ではありません。しかし、同じロジックが他のプロセスでも必要になる可能性があります。リスト3は、別のユーザー定義関数の例です。顧客のレコードを取得して、顧客のステータスに応じた適切な日付を返します。

リスト3:ステータスに基づいて日付を使用するユーザー定義関数
CREATE FUNCTION dbo.GetDate (@nStatus integer, @dDrDate DateTime
               @dWIPDate DateTime, @dCompDate DateTime) 
   RETURNS DateTime
   AS
   BEGIN
      -- This returns one of three dates, based on a status
      DECLARE @dReturnDate DateTime
      SET @dReturnDate = (SELECT CASE
                  WHEN @nStatus = 1 THEN @dDrDate
                  WHEN @nStatus = 2 THEN @dWIPDate
                  WHEN @nStatus = 3 THEN @dCompDate
                  ELSE @dDrDate
               END)
      RETURN (ISNULL(@dReturnDate,CAST(’01-01-1901’ AS DateTime))) 
   END

 これなら、次のように記述する代わりに、

AND CompDate <= @dCutOffDate

 次のように記述することができます。

AND dbo.GetDate(Status,DrDate,WIPDate,CompDate)
   <= @dCutOffDate

 もちろん、この場合には、元々のクエリがユーザー定義関数を呼び出し、そこからさらに別のユーザー定義関数を呼び出すことになります。コードを切り分ける方がよいかどうかは、パフォーマンスに照らしたうえで考察する必要があります。このロジックを使用して日付を判断するクエリや関数が他にない場合には、この2番目のユーザー定義関数は必要ないかもしれません。この判断は、アプリケーションのすべての技術要件をはっきりさせたうえで行う方がよいでしょう。

 SQL Server 2000のユーザー定義関数にはいくつかの制約があるという点は頭に入れておいてください。既存のテーブルに含まれているデータをユーザー定義関数の中で変更することや、ユーザー定義関数の中で一時テーブルを作成することはできません。ユーザー定義関数の一般的な目的は、コードを利用して値を返すことであり、データを変更することではないのです。

ヒント3:ユーザー定義関数でテーブルを返す

 要件:ヒント1で、単一のアカウントに対応する複数の結果セットを返すストアドプロシージャを実装しました。今度は、この要件を拡張する必要が生じたとしましょう。つまり、ユーザーが選択するアカウントが1個でも、10個でも、あるいは50個でも、それらのアカウントすべてに対応する注文情報を表示できるようにしたいとします。

 開発者によっては、INステートメントと、動的SQLや一時テーブルなどの手法を組み合わせて、この状況に対処する人もいます。しかし、ユーザー定義関数は、スカラー値のみならずテーブルも返すことができます。その機能を使えば、可変個のアカウントに対してストアドプロシージャを実行するというこの要件に、もっとすっきりと対応できます。具体的には、ヒント1のストアドプロシージャに手を加え、次のようにします。

  • 選択されたアカウントのリストを、たとえば「1,2,17,912,1091」のようなコンマ区切りのパラメータとしてストアドプロシージャに渡すようにします。
  • ユーザー定義関数を作成します。その中では、コンマ区切りのキーのリストを受け取り、リストに含まれている各エントリをそれぞれ1つの行の整数キーとして格納したテーブル変数を作成して、そのテーブル変数を返します(リスト4を参照)。「Orders」テーブルに対するJOINの中でユーザー定義関数を呼び出し、アカウントのリストに対応する注文情報を取得します。
リスト4:キーセットをテーブル変数に変換するユーザー定義関数
CREATE FUNCTION dbo.CsvToTable ( @cList varchar(8000)) 
   -- Converts a comma-delimited list of integer keys to a TABLE
   -- Used to take a variable list of selections               
   -- (accounts, products, etc.) and put them into a TABLE that can
   -- be used in a subsequent JOIN
   RETURNS @IntKeyTable TABLE
      (IntKey INT)
   AS
   BEGIN
      DECLARE @nPosition INT
      DECLARE @cTempValue VARCHAR(8000) 
      DECLARE @nIntKey int
      SET @cList = RTRIM(@cList) + ’,’
      -- So right now we might have  ’1111,2222,’
      -- (Careful if the CSV already ended with a comma,
      -- you’ll wind up with an extra 0 in the key table)
      -- see if comma exists in list
      -- (use PATINDEX to return pattern position within a string
      WHILE PATINDEX(’%,%’ , @cList) <> 0     
      BEGIN
         -- get the position of the comma
         SELECT @nPosition =  PATINDEX(’%,%’ , @cList)    
         -- get the key, from beginning of string to the comma
         SELECT @cTempValue = LEFT(@cList, @nPosition - 1)   
         SET @nIntKey = CAST(@cTempValue AS INT)
         -- Write out to the Keys table (convert to integer)
         INSERT INTO @IntKeyTable 
            VALUES (@nIntKey)      
         -- wipe out the value we just inserted  
         SELECT @cList = STUFF(@cList, 1, @nPosition, ’’)     
      END
      RETURN
   END

 開発者は、可変個のアカウントや製品、あるいはその他の可変個のキーに対してクエリを実行するときに、このユーザー定義関数をいつでも利用できます。リスト5は、ヒント1のストアドプロシージャを修正し、ユーザー定義関数を使用するようにしたバージョンです。

リスト5:テーブル値を返すユーザー定義関数を使用して複数のアカウントをクエリする
CREATE PROCEDURE GetOrdersByMultipleAccts 
         @cAcctList varchar(8000) AS
   -- Retrieve Orders for a single customer
      DECLARE @tOrderList TABLE (OrderKey integer)
      INSERT INTO @tOrderList 
         SELECT OrderKey FROM OrderHdr OH
            JOIN dbo.CsvToTable(@cAcctList) IK
               ON IK.IntKey = OH.AcctKey
      SELECT OH.*, AM.AcctName 
         FROM OrderHdr OH
         JOIN AcctMast    AM ON AM.AcctKey = OH.AcctKey
         JOIN @tOrderList TMP ON TMP.OrderKey = OH.OrderKey
      SELECT OD.*, PM.ProdName
         FROM OrderDtl OD
         JOIN ProdMast PM ON PM.ProdKey = OD.ProdKey
         JOIN @tOrderList TMP ON TMP.OrderKey = OD.OrderKey
      SELECT * FROM OrderNotes ON
         JOIN @tOrderList TMP ON TMP.OrderKey = ON.OrderKey
   GO

 なお、リスト4のユーザー定義関数が持つvarcharパラメータは、最大8,000バイトです。コンマ区切りリストで渡すのが整数のキー値だとすると、数百個のキーであれば、varcharの8,000バイトの制限に達しないで渡せるはずです。もっと大きなデータ(数千個のキーや、文字サイズの大きなデータ型など)が必要な場合には、XMLなど他の手法を検討する必要があります。

ヒント4:LIKEを使用した検索の実行

 要件:求人情報アプリケーションのユーザーから、キーワード検索に合致した情報すべてを一覧表示する機能を追加してほしいという要望がありました。

 T-SQLのLIKEコマンドを使用すると、文字列内のパターンに合致するものを検索できます。たとえば、ユーザーが、「Windows XP、FrontPage等のスキル」という表現が含まれているメモ列から、「XP」という単語を検索したいとします。開発者は、LIKEコマンドと、ワイルドカードのパーセント文字(%)を使用して、パターンマッチングを実行できます。

 LIKEには、実行する検索の種類に応じて、複数の使用方法があります。たいていは、列内のいずれかの場所に含まれているパターンを調べるという検索です。しかし、列の先頭が検索パターンに合致する行だけを検索することが必要な場合もあります。あるいは、列の末尾が検索パターンに合致する行だけを検索したい場合もあります。

-- Search anywhere in the column
SELECT * FROM Applicants WHERE
   Skills LIKE ’%XP%’ 
-- Search where skills begins with XP
SELECT * FROM Applicants WHERE
   Skills LIKE ’XP%’
-- Search where skills ends with XP
SELECT * FROM Applicants WHERE
   Skills LIKE ’%XP’

 また、単一のワイルドカード文字を表すアンダーバー(_)も使用できます(たとえばName LIKE ’_EVIN’)。

ヒント5:CASEの利用

 要件:顧客の口座収支を、標準の日付区分(1〜30日、31〜60日等)に基づいて取りまとめた口座年齢表を生成する必要があります。また、口座ごとや収支の種類ごとの集計も必要です。

 ヒント2で、CASEの基本的な機能を使用して顧客のステータスコードを評価し、対応する日付値を返すという例が出てきました。今度は、CASEを使用して要件を満たす別の例を見てみましょう。

 開発ソリューションによっては、詳細な結果セットや一部のみが集計された結果セットを取得し、それ以上の集計はビジネスコンポーネントで行うという方法をとる場合があります。その方法でも間違いではないですが、実はSQL Server 2000だけでも最終的に必要な結果セットを構築できます。たとえば、ソリューションによっては、SQLから口座データを取得し、その結果を処理して、適切な日付区分に取りまとめるという方法をとっている場合があります。リスト6は、口座の各収支を、対応する日付区分に分類する例です。CASEステートメントを使用して、日付範囲(たとえば「基準日−90」日と「基準日−61」日の間かどうか)に照らして日付列を評価することにより、対応する区分に分類できます。

 これにより、顧客の口座収支を日付区分で分類するという最初の要件は達成されます。また、CASEステートメントをGROUP BY句で使用すれば、口座や種類ごとに結果の和を求めるという処理も可能です。動的SQLや、わずかに異なるだけの複数のクエリを作成することを完全に避けるのは困難ですが、CASEを効果的に使用すれば、そうした必要性を確実に減らせます。

リスト6:CASEを何回も使用して結果セットを作成する
CREATE PROCEDURE GetAgingResults (@cAcctList varchar(8000), 
            @dAgeDate DateTime, @nGroupOption int)
   AS
   SELECT CASE WHEN @nGroupOption = 1 THEN CT.Description
            WHEN @nGroupOption = 2 THEN AM.Description
    END AS GroupDesc,
   -- Set up CASE statements for the aging brackets
       SUM(CASE WHEN CDate BETWEEN @dAgeDate-30 AND @dAgeDate       
      THEN CBalance ELSE 0 END) AS Age30 ,
       SUM(CASE WHEN CDate BETWEEN @dAgeDate-60 AND @dAgeDate-31  
      THEN CBalance ELSE 0 END) AS Age60 ,
       SUM(CASE WHEN CDate BETWEEN @dAgeDate-90 AND @dAgeDate-61  
         THEN CBalance ELSE 0 END) AS  Age90 ,
       SUM(CASE WHEN CDate BETWEEN @dAgeDate-120 AND @dAgeDate-91  
         THEN CBalance ELSE 0 END) AS  Age120 ,
       SUM(CASE WHEN CDate < @dAgeDate - 120 
         THEN CBalance ELSE 0 END) AS  AgeGT120, 
       SUM(CASE WHEN CDate > @dAgeDate
               THEN CBalance ELSE 0 END) AS NotAged,
       SUM(CBalance) AS TotBalance
   FROM OpenBalances OB
      JOIN dbo.CsvToTable(@cAcctList) IK   
        ON IK.IntKey = OB.AcctKey   
      JOIN Category CT 
         ON CT.CatCode = OB.CatCode   
   JOIN AcctMast AM
         ON AM.AcctKey = OB.AcctKey
      GROUP BY 
      CASE WHEN @nGroupOption = 1 THEN CT.Description
              WHEN @nGroupOption = 2 THEN AM.Description 
   END
      ORDER BY TotBalance DESC

ヒント6:サブクエリと派生テーブル

 要件:ある建築会社のデータベースには、建築ジョブのテーブル、ジョブあたりの作業時間のテーブル、ジョブで購入した資材のテーブルがそれぞれ1つずつあります。これらから、各ジョブ、そのジョブの総作業時間、そのジョブで購入した資材の合計コストを取りまとめた簡単なリストを生成する必要があります。なお、ジョブによっては、作業時間はあるが資材コストはない場合や、資材コストはあるが作業時間はない場合があります。

 これは一見、かなり簡単な要件のように思われるかもしれません。しかし実際には、なかなか難しい問題であり、クエリの手法についても意見が分かれる可能性があります。

 開発者によっては、この問題に対処するときに、次のようにOUTER JOINステートメントのペアを使用するかもしれません。

SELECT J.JobID, SUM(H.Hours) AS TotHours,
   SUM(C.Costs) AS TotCosts
FROM JobMast J
   LEFT JOIN JobHours H ON H.JobID = J.JobID
   LEFT JOIN JobCosts C ON C.JobID = J.JobID
GROUP BY J.JobID

 あいにく、このクエリでは、正しい結果は生成されません。このデータベースでは、単一のジョブに対応する作業時間の行が2つあったり、資材コストの行が3つあったりする可能性があります。そのため、上記の集計方法では、作業時間やコストが二重三重にカウントされて、非常に高い値になってしまう可能性があるのです。

 幸い、SQL-92の標準では、スカラー値を返すサブクエリをSELECTステートメントの中に実装する方法で、この問題に対処できます。次のクエリなら、信頼の置ける結果が生成されます。

SELECT J.JobID,
   (SELECT SUM(H.Hours) FROM JobHours H
      WHERE H.JobID = H.JobID) AS TotHours,
   (SELECT SUM(C.Costs) FROM JobCosts C
      WHERE C.JobID = H.JobID) AS TotCosts
FROM JobMast J
GROUP BY J.JobID

 上記のコードでは、相関サブクエリを使用しています。相関サブクエリは、外部クエリの結果に依存します。したがって、単独で実行することはできません。

 この方法では、外部クエリの各レコードに対してサブクエリが実行されます。人によっては、これでは効率が悪いと考え、代わりに派生テーブルを使うことがあります。

SELECT J.JobID, H.TotHours, C.TotCost
   FROM JobMast J
   LEFT JOIN (SELECT JobID, SUM(Hours) AS TotHours
              FROM JobHours
              GROUP BY JobID) H
   ON H.JobID = J.JobID
   LEFT JOIN (SELECT JobID, SUM(Costs) AS TotCosts
              FROM JobCosts
              GROUP BY JobID) C
   ON C.JobID = J.JobID

 上記のクエリでは、かっこで囲まれた2つのサブクエリで派生テーブルが生成されます。派生テーブルとは要するに、そのクエリの間だけ存在する、一時的な臨時のビューです。派生テーブルの内容は、他のテーブルと同様に、外部クエリで参照できます(なお、派生テーブルを参照するときには、必ず別名を使用する必要があります)。また、派生テーブルは、FROM句の中のサブクエリでも作成できます。派生テーブルは主に、当該のクエリのみで一時的な結果セットが必要な場合に使用します。

 「エコノミストを10人集めれば、経済について10通りの見解が得られる」という古い戯言があります。これはいくぶん誇張した言い方ですが、相関サブクエリと派生テーブルについても、どちらの方法が好ましいかを10人の開発者に聞いてみたら、意見はきっと分かれるはずです。つまり、読みやすいという理由で相関サブクエリを選ぶ人もいれば、パフォーマンス面で優位かもしれないという理由で派生テーブルを選ぶ人もおり、さらにはもっと別の方法を選ぶ人も必ずいるはずです。

 技能や経験が同程度の開発者であっても、パフォーマンス(の有望性)に重きを置くか、それともメンテナンスや読みやすさに重きを置くかで、意見はきっと分かれます。時には、正解は1つでないこともあります。鍵となるのは、この種の状況に対処するときに一貫性を保つということです。

 話を先へ進める前に、要件をもう1つ追加してみましょう。私が作成した結果セットには、各ジョブの作業時間の集計が含まれていました。ここで、時間を集計するのではなく、作業時間に従業員の時給を掛けたものを合計して、労働賃金の総額を求めるには、どうすればいいでしょうか。

 さらに複雑なことに、1つのジョブの中で時給が変わることもあるものとします(たとえば、あるジョブの半分の作業までの時給より、残りの半分の時給の方が高い場合があります)。リスト7は、上記のクエリを拡張して、作業日に対応する作業時間とその日の有効な時給とをマッチさせる方法を示したものです。追加したロジックでは、当該の作業日以前の有効な日付の最大値(MAX)における時給を検索しています。追加したこのサブクエリは、前述のクエリのどちらでも使用できます。

リスト7:作業日と適切な時給を対応付けるためのサブクエリ
-- JobMast  (JobID)
-- JobHours (JobID, EmpKey, Hours, DateWorked)
-- JobCosts (JobID, Costs)
-- EmpRate  (EmpKey, EffDate, WorkRate)
-- Use subquery to create derived tables, to sum/group hours 
-- and costs
-- Inside the subquery for hours, perform another subquery
-- to match up an employee’s work date with the rate for that day
-- Uses EmpRate twice (ER1 and ER2, once to apply the correct rate
-- and a second time in a subquery to get the rate based on date
SELECT J.JobID, H.TotLabor, H.TotHours, C.TotCost 
   FROM JobMast J
   LEFT JOIN 
       (SELECT JobID, SUM(JH.Hours*ER1.WorkRate) AS TotLabor,
                SUM(JH.Hours) AS TotHours
              FROM JobHours JH
         JOIN EmpRate ER1 ON ER1.EmpKey = JH.EmpKey
         WHERE ER1.EffDate = (SELECT MAX(EffDate) FROM
            EmpRate ER2 WHERE EffDate <= DateWorked
            AND ER2.EmpKey = JH.EmpKey)
         AND DateWorked >= EffDate
              GROUP BY JobID) H 
   ON H.JobID = J.JobID
   LEFT JOIN (SELECT JobID, SUM(Costs) AS TotCosts
              FROM JobCosts
              GROUP BY JobID) C 
   ON C.JobID = J.JobID

ヒント7:テーブル変数と一時テーブル

 ヒント1および3のリストでは、一時的な結果セットを保持してストアドプロシージャ内で繰り返し利用するために、テーブル変数を導入しました。これらの例では、テーブル値を返すユーザー定義関数(アカウントキーのリストに対して処理を行うもの)の結果を、注文ヘッダーテーブルに対して直接結合しました。アカウントキーが格納されているテーブルを別の結合で使用したい場合なら、次のような方法が可能です。

DECLARE @tAcctKeys TABLE (IntKey int)
INSERT INTO @tAcctKeys
   SELECT IntKey = IntKey FROM
      CsvToTable(@cAcctKeyList)
-- now you can use @tAcctKeys in any subsequent
-- joins in the stored procedure

 Microsoftのドキュメントによると、テーブル変数を使用すると、一時テーブルと同様のメリットがある程度得られるのに加え、パフォーマンスも若干向上します。テーブル変数の一般的なスコープは、他の変数と同じです。つまり、プロシージャまたは関数の最後で自動的にクリアされます。SQL Serverでは、ストアドプロシージャの再コンパイルの回数は、テーブル変数を使用している場合の方が、一時テーブルを使用している場合より少なくなります。加えて、テーブル変数が絡んでいるトランザクションの存続期間は、テーブル変数の更新の間だけです。したがって、テーブル変数の方が、リソースのロックやログ記録の必要性が少なくなります。テーブル変数のスコープは限定されており、永続的なデータベースの一部ではないので、トランザクションのロールバックによる影響を受けません。

 確かに、テーブル変数のパフォーマンスは多少優れているかもしれませんが、その内容がシステムデータベースではなくメモリに格納されるおかげで一時テーブルよりパフォーマンスが優れているというのは、若干の誤解があります。Microsoftによると、テーブル変数はメモリのみの構造ではありません。テーブル変数には、メモリに収まりきらないデータを保持することも不可能ではありません。したがって、データを格納するための場所がディスク上に必要です。テーブル変数は、一時テーブルと同様に「tempdb」データベースに作成されます。メモリでまかなえる場合は、テーブル変数と一時テーブルのいずれも、メモリ(データキャッシュ)内で作成および処理されます。

 この事実をふまえても、前述のような理由から、開発者は一時テーブルよりテーブル変数の方を好むのが一般的です。しかし、テーブル変数には、見過ごせない制約がいくつかあります。テーブル変数のTRUNCATEは不可能ですし、テーブル変数の作成後にその構造を変更することもできません。さらには、テーブル変数に対しては、次のいずれのステートメントも使用できません。

-- neither line will work with table variables
INSERT @tTable EXEC <sp_mystoredproc>
SELECT * INTO @tTable FROM <mytable>

ヒント8:日付関数

 要件:日別の注文情報を取得して、週ごとに取りまとめ、週報や週単位のグラフを作成する必要があります。

 次に示す2つの短いコードは、SQL ServerのDATEPART関数を使用してこの要件を満たす方法を示したものです。1つ目のコードは、週の中の任意の日付を、その週の土曜日の日付に変換するユーザー定義関数です。2つ目のコードでは、注文情報に対するクエリの中でそのユーザー定義関数を使用し、週末の土曜日の日付ごとにデータを取りまとめています。この結果セットを、週報やグラフに使用できます。この例も、ビジネス層でデータをほとんど(あるいはまったく)いじらずに結果セットを作成できるようにTransact SQLを使用した例と言えます。

 UPDATEトリガを作成するときには、UPDATEステートメントで更新された行の数にかかわらず、トリガは1回のみ起動されるということを頭に入れておく必要があります。

CREATE FUNCTION dbo.GetEndOfWeek
(@dDate DateTime)
-- Converts date to the Saturday date for the week
RETURNS DateTime AS 
BEGIN
   DECLARE @dRetDate DateTime
   SET @dRetDate =
      @dDate + ( 7-DATEPART(weekday,@dDate))
   RETURN @dRetDate
END
SELECT dbo.GetEndOfWeek(OrderDate) AS WeekEnding,
   SUM(Amount) AS WeekAmount
   FROM OrderHdr
   GROUP BY dbo.GetEndOfWeek(OrderDate)

 DATEPARTは、分析対象の日付要素に基づく整数を返します。SQL Server Books Onlineには、DATEPART関数で分析できる日付要素(datepart)の一覧が掲載されています。たとえば、日付要素にquarterを指定すると、データを四半期ごとに取りまとめることができます。

 weekdayという日付要素では、SQL Serverでの週の開始日の設定に応じて結果が左右されます。既定の設定では、週の開始日は日曜日です。週の開始日を別の曜日に設定するには、SET DATEFIRSTコマンドを使用します。たとえば、売上の集計を、日曜〜土曜という区切りではなく月曜〜日曜という区切りで行う場合には、次のように設定します。

-- Change first day of week from default of
-- Sunday (7) to Monday (1)
SET DATEFIRST 1

 日付について1つ申し沿えておきます。日付を比較するクエリを作成するときに、日付列の値には午前0時以外の時刻が設定されているのに対し、比較対象の日付変数の日付には時刻が定義されていない、というケースがよくあります。

 たとえば、Date <= CAST(’05-26-2003’) AS DATETIMEという条件を満たす行を取得するクエリの場合、「05-26-2003 22:10:00」という日付の行は取得されません。なぜなら、この場合には、「2003年5月26日の22:10:00」と「2003年5月26日の00:00:00(午前0時)」とが比較され、「前者<=後者」は成り立たないと判断されるからです。5月26日までの全データを、その日の午後11時59分ぎりぎりのものまで含めて取得したい場合には、比較日に1を加えたうえで、それ未満の日付のものを取得する(Date < CAST(’05-26-2003’) + 1)というロジックに変える手があります。

ヒント9:CASTとCONVERTによるデータ変換

 要件:経理処理の結果を伝えるメッセージを、通常の英文に近い形で生成する必要があります。たとえば、"100 Employee Checks were generated on 11/15/2004 at 5:07:02 PM, for a total dollar value of $134,123.11"といったメッセージです。

 SQL Serverに備わっているCAST関数とCONVERT関数を使用すると、異なるデータ型を組み合わせて利用できます。リスト8は、異なるデータ型(DATETIMEdecimal)の変数を使用して、ログや表示に利用できるメッセージ文字列を組み立てる方法の例です。

リスト8:CAST関数とCONVERT関数を使用して異なるデータ型を併用する
DECLARE @nRowCount integer
   DECLARE @dReportDate DATETIME
   DECLARE @nDollars decimal(13,2)
   SET @nRowCount = 156
   SET @dReportDate = GETDATE()
   SET @nDollars = 156134.11
   DECLARE @cDateString char(20)
   DECLARE @cRowCount char(20)
   DECLARE @cMessage varchar(8000)
   SET @cRowCount = LTRIM(RTRIM(CONVERT(int,@nRowCount,20)))
   SET @cDateString = LTRIM(RTRIM(CONVERT(datetime,@dReportDate,20)))
   SET @cMessage = ’Generated ’ + @cRowCount +
                ’ Employee Checks on ’  + @cDateString +
          ’ for a total amount of $’ + 
                CAST(@nDollars AS Varchar(20))
   SELECT @cMessage

ヒント10:更新トリガ

 要件:テーブルに対してUPDATEが実行されたときに、タイムスタンプ列を自動的に更新する必要があります。

 UPDATEトリガは、テーブルに対してUPDATEステートメントが実行されるたびに毎回起動される、特別な種類のストアドプロシージャです。トリガを使用すると、特定の規則を適用したり、特定の列を更新したりできます。

 UPDATEトリガでは、更新前(DELETED)と更新後(INSERTED)の行の状態を保持する2つの重要なシステムテーブルにアクセスします。これらのテーブルの内容を使用すると、基本的な監査証跡機能を実装できます(ヒント11で取り上げます)。とりあえずは、LASTUPDATED列の自動更新の実現に話を絞ることにします。

 UPDATEトリガを作成するときには、UPDATEステートメントで更新された行の数にかかわらず、トリガは1回のみ起動されるということを頭に入れておく必要があります。SQL Serverの経験が浅い人がトリガのコードを作成すると、1つの行のみが更新されたという想定でコードを作成してしまうというミスを犯すことがあります。

CREATE TRIGGER Upd_Client ON dbo.Client
FOR UPDATE
AS
   UPDATE Client
      SET LastUpdate = GETDATE()
   FROM Client C
      JOIN Inserted I
       ON I.PrimaryKey = C.PrimaryKey

ヒント11:UPDATEトリガを使用して監査証跡の更新を実装する

 要件:特定の列に対する変更を、監査証跡ログに記録する必要があります。ログに含める必要があるのは、変更されたテーブル、変更された行の主キー、変更された列の名前、変更前および変更後の値(古い値/新しい値)、更新の日付/時刻です。

 特定のデータベース項目に対する変更を追跡したいと考えている顧客は多くいます。UPDATEトリガでは、前のヒントで使用した「INSERTED」テーブルと「DELETED」テーブルを使用して監査証跡を実装できます。

 リスト9では、製品マスターテーブルに対してUPDATEトリガを使用して、項目に対する変更を追跡しています。ここでは、「INSERTED」テーブルと「DELETED」テーブルをクエリして、特定の列に変更があるかどうか調べています。この例では、PRICE列とDESCRIPTION列の変更をチェックした後で、「INSERTED」および「DELETED」からアプリケーションログテーブルに対して値を書き込んでいます。

リスト9:基本的な監査証跡機能を実装する
CREATE TRIGGER Upd_Price ON dbo.Price
   FOR  UPDATE  
   AS
      DECLARE @dLastUpdate DATETIME
      SET @dLastUpdate = GETDATE()
         -- Set the last Update for rows updated
      UPDATE Price 
         SET LastUpdate = @dLastUpdate
      FROM Price P
         JOIN Inserted I
          ON I.PrimaryKey = P.PrimaryKey 
         -- Write out to the Audit Log, for rows where Price changed 
      INSERT INTO AuditLog (TableName, PrimaryKey, LastUpdate, 
                             ColName, OldValue, NewValue, UserKey)
             SELECT ’PRICE’ AS TableName, I.PrimaryKey, 
                    @dLastUpdate AS LastUpdate, ’Price’ AS ColName,
                    CONVERT(CHAR(20),D.Price) AS OldValue, 
                    CONVERT(CHAR(20),I.Price) AS NewValue, 
                    I.UserKey
             FROM Inserted I
             JOIN Deleted D ON D.PrimaryKey = I.PrimaryKey
             WHERE I.Price <> D.Price
      -- Now check for any changes to the description
      INSERT INTO AuditLog (TableName, PrimaryKey, LastUpdate, 
                             ColName, OldValue, NewValue, UserKey)
             SELECT ’PRICE’ AS TableName, I.PrimaryKey, 
                    @dLastUpdate AS LastUpdate, ’Descr’ AS ColName,
                    D.Descr AS OldValue, 
                    I.Descr AS NewValue, 
                    I.UserKey
             FROM Inserted I
             JOIN Deleted D ON D.PrimaryKey = I.PrimaryKey
             WHERE I.Descr <> D.Descr

 メモ:このヒントでは、監査証跡ログの基本機能を示しています。完全に自動化された監査証跡ソリューションを構築するには、ログへの記録が必要なテーブル/列に対してUPDATEトリガを生成するようなデータドリブンスクリプトの作成を検討するとよいでしょう。

 自動化された監査証跡ソリューションの構築に役立つ製品は、いくつかの会社から出ています。私の個人的なおすすめは、Red Matrix TechnologiesのSQLAuditという製品です。上記のような機能やその他の機能が備わっています。包括的な監査証跡ソリューションの開発に要する苦労を考えると、SQLAuditなどのサードパーティ製ツールを購入するのは、経済面から見て賢い選択かもしれません。

ヒント12:動的SQL

 データドリブンアプリケーションにおいて、重要なクエリ構文を実行時まで確定できない場合に、動的SQLステートメントを使用して値を渡せることがあります。ただしこれは賛否両論がある方法です。リスト10は、動的SQLの使用例を2つ示したものです。1つ目の例は、テーブル名が変数となっている簡単なクエリです。2つ目の例は、1つの行を返すクエリの結果を出力変数に設定しています。

リスト10:動的SQLの基本的な使用例
-- Must use Unicode data
DECLARE @cSQLSyntax nvarchar(2000)
DECLARE @cTableName varchar(20)
SET @cTableName = ’EMPLOYEE’
SET @cSQLSyntax = N’SELECT * FROM ’ + @cTableName  + ’ WHERE 
      EmployeeID = 1’ 
EXEC sp_executesql @cSQLSyntax
-- Return a column into an output variable
-- Query must only contain one row, else an error will occur
-- (query could also be a SUM that returns a scalar value)
DECLARE @cValue nVarChar(30)
DECLARE @cColumnName varchar(20)
SET @cColumnName = ’firstname’
SET @cSQLSyntax = N’ SELECT @cValue = ’ +  @cColumnName + ’   FROM 
   Employee where employeeid = 1’
EXECUTE SP_EXECUTESQL @cSQLSyntax, N’@cValue nVarChar(30) OUTPUT’,
    @cValue OUTPUT
SELECT @cValue

 メモ:リスト10の動的SQLではUnicodeデータ(nCharおよびnVarChar)を使用する必要があります。

 Googleで「dynamic SQL」(動的SQL)と検索してみると、動的SQLについての優れた説明がいくつも見つかります。ネット上には、動的SQLのさまざまな手法を説明した文章がたくさんあり、動的SQLとの関連でSQLインジェクションについて説明したものまであります。

ヒント13:データベースのテーブルと列の一覧を取得する

 初心者がよく抱く疑問の1つに、データベースのテーブルと列の一覧を取得するにはどうすればいいかというものがあります。次のように、情報スキーマをクエリすれば、一覧を簡単に取得できます。

-- Return a list of table names
SELECT DISTINCT Table_Name FROM
   Northwind.INFORMATION_SCHEMA.COLUMNS
   ORDER BY Table_Name
-- Return a list of columns
-- (perform a SELECT * to see a full list)
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE,
      CHARACTER_MAXIMUM_LENGTH FROM
   Northwind.INFORMATION_SCHEMA.COLUMNS
   ORDER BY Table_Name
-- You could combine the capability of LIKE
-- to find columns with a particular search
-- pattern

まとめ

 私のWebサイトでは、SQL Server 2000とTransact-SQLについての良書をいくつか紹介しています。また、SQL Server、Transact-SQL、SQL-92などについてのさまざまなオンラインリソースも紹介しています。

 また、これは現在も進行中のプロジェクトですので、折に触れて加筆を行う場合があります。

著者紹介

Kevin S. Goff(Kevin S. Goff)
.NET、Visual FoxPro、SQL Server、Crystal Reportsによる独自のWebソリューション/デスクトップソリューションを提供するコンサルティンググループ「Common Ground Solutions」の創業者兼主任コンサルタント。ソフトウェアアプリケーションの開発経験は17年に及ぶ。米農務省からシステムオートメーション関連の賞をいくつか受賞。また、6桁の投資見返りを実現するソリューションを開発したことによりFortune 500企業から特別表彰を受ける。保険、会計、環境衛生、不動産、出版、広告、製造、金融、日用品、貿易振興など多様な業界に携わった経験を持ち、さまざまな形態で独自のトレーニングも行っている。

プリンター用
記事を転送
この記事をクリップ!
【特別連載企画】大艦巨砲主義にして卓越したレスポンス--GALAXY S II WiMAX
【特別連載企画】大艦巨砲主義にして卓越したレスポンス--GALAXY S II WiMAX 1月20日より販売が開始されたサムスン製スマートフォン「GALAXY S II WiMAX」。カタログスペックでは、他メーカーのハイエンド機と同じように見えても、実際に使うと卓越したレスポンスに驚かされる。
⇒詳細記事はこちら
⇒連載記事一覧はこちら
注目のトピックス
最新コラム一覧
百式のネットビジネス研究
百式のネットビジネス研究
次のフライトでお好みの座席が空いたら教えてくれる「Expert Flyer」
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
12月の主婦層、ベルメゾンが首位を維持(VRI 調査)
アウンのグローバルマーケティング動向
アウンのグローバルマーケティング動向
Web プロモーションにおいて大切なこと―年度末編―
多言語×Web×海外マーケティング情報
多言語×Web×海外マーケティング情報
海外発、注目 AR プロモーション
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
楽天が目指す変革──Globalization、Agile、Big Data
中国・台湾ネットビジネス情報最前線
中国・台湾ネットビジネス情報最前線
中国から Web を見てもらいたいならば
マーケティングに活用できる最新トレンド
マーケティングに活用できる最新トレンド
改めて、「導線」最適化に目を向ける
次世代マーケティングチェーンの視点
次世代マーケティングチェーンの視点
ソーシャル時代における BtoC 型 Eコマース成功のポイント
Copyright 2012 internet.com K.K. (Japan) All Rights Reserved.