ストアドといえば,カーソル処理は必須科目です。勿論使わないで済むのであれば,それはよいことです。でも,カーソル処理をDB内部で行わないと困るという状況なので,ストアドを使う場合も多いでしょう。
カーソル処理の記述方法によって,潜在的なバグを減らすことができます。どのようにできるかも後半で考えます。まずは,T-SQLのカーソル処理についてです。
カーソル処理については,機能的にOracleとT-SQLには結構な開きがあるので,T-SQLでの実装は,Oracleでの同機能の実装より手間がかかります。差がある部分としては,以下のようなものを挙げることができます。
2.については,あきらめてOPEN ~ FETCH ~ CLOSE ~ DEALLOCATEの手順を踏まなければなりません。例外時のカーソルクローズ処理も必要となるので,実装するコード量が増大します。
3.については,Oracleの場合カーソルを定義し cur_name%rowtypeでレコード型の各カラムの型を個別指定することなく定義できるという,強力な構文があります。また各項目についても table_name.col_name%typeで型を意識せず(宣言だけは,ですが)変数の宣言ができます。でも,T-SQLにはこれらの構文が存在しないので,あきらめることにしましょう。
とは言っても%TYPEを使っても,数値型と日付型,文字列型をプログラム中で意識しないで済むことはないわけです。T-SQLの場合varchar(xxx)を始めから大きめに確保しておくというのも一つの手です。
カーソル処理の記述方法:T-SQL
T-SQLのカーソル処理は上記の1から3の理由で,記述量が増大します。さらにMicrosoftのヘルプに載っているサンプルはコード量をさらに増大させます。以下は,Microsoftのサンプルコードの方式でカーソルループを行っているプログラムです。
<title>Microsoftのサンプルに倣ったSQL</title> <style type="text/css"> </style>
これはどう見ても,問題のあるコードです。どうしたらよいでしょうか。WHILEの条件を書き換えて以下のようにすることができます。
<title>FETCHを調整したSQL.sql</title> <style type="text/css"> </style>
Microsoftのサンプルは,確かにWHILEの条件として@@FETCH_STATUSを見ていることを明示できるのでわかりやすいですが,実用的には上記のコードの方がバグを減らすことができます。本質的にはT-SQLの流れ制御構文が貧弱であるということが問題の原因と思われます。
両方のコードに言えることですが,FETCH文と@@FETCH_STATUSの判定部分の間に別の処理を記述しないようにするのはよいことです。最初のサンプルでいれば,22,23行の間,後のサンプルでは,18から22行の部分です。単にSET文やPRINT文を記述しても問題が発生するわけではありませんが,一度こうした追記が入ると,以後の追加処理がさらに加わり,最終的にEXECで別カーソルを回すストアドがコールされたりする危険が発生します。
Oracleのカーソル
Oracleはカーソル処理の構文が複数あり,どれを利用しようか迷ってしまいます。贅沢といえば贅沢な悩みですが,それが元で不要なバグを生まないように気をつけたいと思います。
OPEN ~ FETCH ~ CLOSEとカーソルFOR文については,OracleのPL/SQLユーザースガイド・リファレンスにも記載されている使い分けが一つの指針となります。しかし,さらに付け加えるなら,基本はカーソルFOR文を使う。ということになります。理由は(1)コードが簡潔に表現できる,(2)カーソルの閉じ忘れを防ぐことができる,の2点です。以下にいくつかのカーソルFORサンプルをまとめてみました。
<title>OracleのカーソルFORサンプル.sql</title> <style type="text/css"> </style>
16-26行はOPENを利用せずに該当レコードが存在したかどうかを判定しています。EXITによりFORループを抜けると共にOracleがカーソルを閉じてくれるので,余分なコードがいりません。通常レコード型のいずれかのフィールドはNOT NULLになっていると思うので,その項目のIS NULLを確認することで,存在チェックができます。ただし,ループ前のREC_MAIN := NULL;は忘れないようにしましょう。
29行以降は カーソルFOR部分にSELECTを記述した例です。勿論これでも正しく動きます。ただ,保守性が落ちます。プログラムの不具合や機能追加時の調査をするとき,最初に見るのは内部ロジックではなくカーソルSQLだったりします。このカーソルSQLがプログラムの先頭部分にまとまっているのと,数千行のプログラム中に散在しているのでは,作業効率に差が出て来ることは容易に想像できると思います。
カーソル処理の記述方法によって,潜在的なバグを減らすことができます。どのようにできるかも後半で考えます。まずは,T-SQLのカーソル処理についてです。
カーソル処理については,機能的にOracleとT-SQLには結構な開きがあるので,T-SQLでの実装は,Oracleでの同機能の実装より手間がかかります。差がある部分としては,以下のようなものを挙げることができます。
- レコード型が存在しない。
- カーソルFORループ構文が存在しない。
- %TYPEの型指定ができない。
2.については,あきらめてOPEN ~ FETCH ~ CLOSE ~ DEALLOCATEの手順を踏まなければなりません。例外時のカーソルクローズ処理も必要となるので,実装するコード量が増大します。
3.については,Oracleの場合カーソルを定義し cur_name%rowtypeでレコード型の各カラムの型を個別指定することなく定義できるという,強力な構文があります。また各項目についても table_name.col_name%typeで型を意識せず(宣言だけは,ですが)変数の宣言ができます。でも,T-SQLにはこれらの構文が存在しないので,あきらめることにしましょう。
とは言っても%TYPEを使っても,数値型と日付型,文字列型をプログラム中で意識しないで済むことはないわけです。T-SQLの場合varchar(xxx)を始めから大きめに確保しておくというのも一つの手です。
カーソル処理の記述方法:T-SQL
T-SQLのカーソル処理は上記の1から3の理由で,記述量が増大します。さらにMicrosoftのヘルプに載っているサンプルはコード量をさらに増大させます。以下は,Microsoftのサンプルコードの方式でカーソルループを行っているプログラムです。
<title>Microsoftのサンプルに倣ったSQL</title> <style type="text/css"> </style>
0001 DECLARE cur_main CURSOR FAST_FORWARD READ_ONLY注目すべきは,16,22行目のFETCH文です。同じ内容のFETCHが2カ所に分散しています。これは,FETCHの後でないと@@FETCH_STATUSの判定ができないためです。とはいえ,サンプルコードでは,FETCHの項目数は数個ですが,実作業では,百以上になることも珍しくありません。その場合,カーソルの項目が追加されたり削除されたとしたらどうなるでしょうか。あなたは2カ所に分かれたFETCHの全ての変数とその順序を一致させ続ける自信がありますか。
0002 FOR
0003 SELECT ITEM_ID,
0004 ITEM_CODE,
0005 RELEASE_DATE
0006 FROM TT_ITEMS
0007 ;
0008
0009 DECLARE @rec_ITEM_ID INT;
0010 DECLARE @rec_ITEM_CODE VARCHAR(50);
0011 DECLARE @rec_RELEASE_DATE DATETIME;
0012
0013
0014 OPEN cur_main
0015
0016 FETCH FROM cur_main INTO @rec_ITEM_ID,@rec_ITEM_CODE,@rec_RELEASE_DATE
0017
0018 WHILE @@FETCH_STATUS = 0
0019 BEGIN
0020 PRINT CONVERT(VARCHAR, @rec_ITEM_ID) + ' ' + ISNULL(@rec_ITEM_CODE, '');
0021
0022 FETCH FROM cur_main INTO @rec_ITEM_ID,@rec_ITEM_CODE,@rec_RELEASE_DATE
0023 END
0024
0025 CLOSE cur_main
0026 DEALLOCATE cur_main
これはどう見ても,問題のあるコードです。どうしたらよいでしょうか。WHILEの条件を書き換えて以下のようにすることができます。
<title>FETCHを調整したSQL.sql</title> <style type="text/css"> </style>
0001 DECLARE cur_main CURSOR FAST_FORWARD READ_ONLYWHILEの条件を 1=1にして無限ループを作成します。そして,FETCH直後に@@FETCH_STATUSを判定することによりループを脱出しています。これによって,FETCH文を1つにすることができました。後はカーソル宣言とこのFETCHの記述が一致するよう努力するだけです。
0002 FOR
0003 SELECT ITEM_ID,
0004 ITEM_CODE,
0005 RELEASE_DATE
0006 FROM TT_ITEMS
0007 ;
0008
0009 DECLARE @rec_ITEM_ID INT,
0010 @rec_ITEM_CODE VARCHAR(50),
0011 @rec_RELEASE_DATE DATETIME;
0012
0013
0014 OPEN cur_main
0015
0016 WHILE 1 = 1
0017 BEGIN
0018 FETCH FROM cur_main INTO @rec_ITEM_ID,@rec_ITEM_CODE,@rec_RELEASE_DATE
0019 IF @@FETCH_STATUS <> 0
0020 BEGIN
0021 BREAK;
0022 END
0023
0024 PRINT CONVERT(VARCHAR, @rec_ITEM_ID) + ' ' + ISNULL(@rec_ITEM_CODE, '');
0025 END
0026
0027 CLOSE cur_main
0028 DEALLOCATE cur_main
Microsoftのサンプルは,確かにWHILEの条件として@@FETCH_STATUSを見ていることを明示できるのでわかりやすいですが,実用的には上記のコードの方がバグを減らすことができます。本質的にはT-SQLの流れ制御構文が貧弱であるということが問題の原因と思われます。
両方のコードに言えることですが,FETCH文と@@FETCH_STATUSの判定部分の間に別の処理を記述しないようにするのはよいことです。最初のサンプルでいれば,22,23行の間,後のサンプルでは,18から22行の部分です。単にSET文やPRINT文を記述しても問題が発生するわけではありませんが,一度こうした追記が入ると,以後の追加処理がさらに加わり,最終的にEXECで別カーソルを回すストアドがコールされたりする危険が発生します。
Oracleのカーソル
Oracleはカーソル処理の構文が複数あり,どれを利用しようか迷ってしまいます。贅沢といえば贅沢な悩みですが,それが元で不要なバグを生まないように気をつけたいと思います。
OPEN ~ FETCH ~ CLOSEとカーソルFOR文については,OracleのPL/SQLユーザースガイド・リファレンスにも記載されている使い分けが一つの指針となります。しかし,さらに付け加えるなら,基本はカーソルFOR文を使う。ということになります。理由は(1)コードが簡潔に表現できる,(2)カーソルの閉じ忘れを防ぐことができる,の2点です。以下にいくつかのカーソルFORサンプルをまとめてみました。
<title>OracleのカーソルFORサンプル.sql</title> <style type="text/css"> </style>
0001 CREATE OR REPLACE PROCEDURE TT_CURSOR IS10-14行が通常のカーソルFORループです。暗黙のカーソル変数Cとプロシージャ宣言部で宣言したREC_MAINを両用していますが,ここは必要に応じてどちらかに一本化することができます。
0002 CURSOR CUR_MAIN IS
0003 SELECT TI.ITEM_ID, TI.ITEM_CODE, TI.RELEASE_DATE, TI.CATEGORY01, TI.CATEGORY02,
0004 TI.DISCON_DATE
0005 FROM TT_ITEMS TI
0006 WHERE TI.RELEASE_DATE > SYSDATE - 30
0007 AND ROWNUM <= 10;
0008 REC_MAIN CUR_MAIN%ROWTYPE;
0009 BEGIN
0010 -- 通常のカーソルFORループ
0011 FOR C IN CUR_MAIN LOOP
0012 REC_MAIN := C;
0013 DBMS_OUTPUT.PUT_LINE(REC_MAIN.ITEM_CODE);
0014 END LOOP;
0015
0016 -- NOTFUNDの代替
0017 REC_MAIN := NULL;
0018 FOR C IN CUR_MAIN LOOP
0019 REC_MAIN := C;
0020 EXIT;
0021 END LOOP;
0022 IF REC_MAIN.ITEM_ID IS NULL THEN
0023 DBMS_OUTPUT.PUT_LINE('見つかりません。');
0024 ELSE
0025 DBMS_OUTPUT.PUT_LINE('見つかりました:' || REC_MAIN.ITEM_CODE);
0026 END IF;
0027
0028 -- 暗黙カーソルでFOR 部分にSELECTを記述:非推奨
0029 FOR C IN (SELECT TI.ITEM_ID, TI.ITEM_CODE, TI.RELEASE_DATE, TI.CATEGORY01,
0030 TI.CATEGORY02, TI.DISCON_DATE
0031 FROM TT_ITEMS TI
0032 WHERE TI.RELEASE_DATE > SYSDATE - 30
0033 AND ROWNUM <= 10) LOOP
0034 DBMS_OUTPUT.PUT_LINE(C.ITEM_ID);
0035 END LOOP;
0036
0037 END TT_CURSOR;
16-26行はOPENを利用せずに該当レコードが存在したかどうかを判定しています。EXITによりFORループを抜けると共にOracleがカーソルを閉じてくれるので,余分なコードがいりません。通常レコード型のいずれかのフィールドはNOT NULLになっていると思うので,その項目のIS NULLを確認することで,存在チェックができます。ただし,ループ前のREC_MAIN := NULL;は忘れないようにしましょう。
29行以降は カーソルFOR部分にSELECTを記述した例です。勿論これでも正しく動きます。ただ,保守性が落ちます。プログラムの不具合や機能追加時の調査をするとき,最初に見るのは内部ロジックではなくカーソルSQLだったりします。このカーソルSQLがプログラムの先頭部分にまとまっているのと,数千行のプログラム中に散在しているのでは,作業効率に差が出て来ることは容易に想像できると思います。