goo blog サービス終了のお知らせ 

I Love DB

DBを愛する人のブログ

カーソル処理とバグを防ぐ記述

2010-06-02 22:24:33 | 開発サイドから見たOracleとSQL Server
ストアドといえば,カーソル処理は必須科目です。勿論使わないで済むのであれば,それはよいことです。でも,カーソル処理をDB内部で行わないと困るという状況なので,ストアドを使う場合も多いでしょう。
カーソル処理の記述方法によって,潜在的なバグを減らすことができます。どのようにできるかも後半で考えます。まずは,T-SQLのカーソル処理についてです。
カーソル処理については,機能的にOracleとT-SQLには結構な開きがあるので,T-SQLでの実装は,Oracleでの同機能の実装より手間がかかります。差がある部分としては,以下のようなものを挙げることができます。
  1. レコード型が存在しない。
  2. カーソルFORループ構文が存在しない。
  3. %TYPEの型指定ができない。
1.ですが,レコード型(Cでいうところの構造体ですね。但しホールについての心配はいりませんが)が存在しないため,カーソルのカラム数分だけ変数を定義する必要が出てきます。この部分の手間が余分にかかります。それと各変数毎の型も合わせる必要があるので,大変です。SQL Assistantがあれば,一旦SELECT INTO XXXを使ってワークテーブルを作ったり,CREATE VIEW でワークのビューを作成し,コードスニペットで変数を一気に作成するという手もあります。項目数が多い場合は,こちらの方が素早く,ミスが減ります。

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 
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
注目すべきは,16,22行目のFETCH文です。同じ内容のFETCHが2カ所に分散しています。これは,FETCHの後でないと@@FETCH_STATUSの判定ができないためです。とはいえ,サンプルコードでは,FETCHの項目数は数個ですが,実作業では,百以上になることも珍しくありません。その場合,カーソルの項目が追加されたり削除されたとしたらどうなるでしょうか。あなたは2カ所に分かれたFETCHの全ての変数とその順序を一致させ続ける自信がありますか。
これはどう見ても,問題のあるコードです。どうしたらよいでしょうか。WHILEの条件を書き換えて以下のようにすることができます。
<title>FETCHを調整したSQL.sql</title> <style type="text/css"> </style>
0001 DECLARE cur_main           CURSOR FAST_FORWARD READ_ONLY 
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
WHILEの条件を 1=1にして無限ループを作成します。そして,FETCH直後に@@FETCH_STATUSを判定することによりループを脱出しています。これによって,FETCH文を1つにすることができました。後はカーソル宣言とこのFETCHの記述が一致するよう努力するだけです。
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 IS
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;
10-14行が通常のカーソルFORループです。暗黙のカーソル変数Cとプロシージャ宣言部で宣言したREC_MAINを両用していますが,ここは必要に応じてどちらかに一本化することができます。

16-26行はOPENを利用せずに該当レコードが存在したかどうかを判定しています。EXITによりFORループを抜けると共にOracleがカーソルを閉じてくれるので,余分なコードがいりません。通常レコード型のいずれかのフィールドはNOT NULLになっていると思うので,その項目のIS NULLを確認することで,存在チェックができます。ただし,ループ前のREC_MAIN := NULL;は忘れないようにしましょう。

29行以降は カーソルFOR部分にSELECTを記述した例です。勿論これでも正しく動きます。ただ,保守性が落ちます。プログラムの不具合や機能追加時の調査をするとき,最初に見るのは内部ロジックではなくカーソルSQLだったりします。このカーソルSQLがプログラムの先頭部分にまとまっているのと,数千行のプログラム中に散在しているのでは,作業効率に差が出て来ることは容易に想像できると思います。



NULLの扱いに起因する問題-文字連結,空文字

2010-06-01 23:33:38 | 開発サイドから見たOracleとSQL Server
一度でもOracleとSQL Serverの双方でSQLを書いていれば,NULLの扱いが,両者で異なっていることに気づくと思います。まずは,復習です。以下のようなテーブルがあったとします。<style type="text/css"> </style>
0001 create table TT_ITEMS
0002 (
0003 ITEM_ID NUMBER,
0004 ITEM_CODE VARCHAR2(50),
0005 RELEASE_DATE DATE
0006 )
この テーブルにのITEM_ID=1のレコードを以下のように更新すると,OracleとSQL Serverのレコードの状態は一致しなくなります。
<style type="text/css">? </style> 0001 UPDATE TT_ITEMS
0002 SET ITEM_CODE = ''
0003 WHERE ITEM_ID = 1
0004

Oracleの場合は, ITEM_CODEがNULLになっています。SQL ServerはNULLではなく,空文字となっています。

さて,ここからが本論です。ここでSQL Serverの結果グリットをExcelにコピペするとどうなるでしょうか。比較用に11レコード目にITEM_CODEがNULLのレコードを作っておきます。

Excel上は結果グリットと同じく,空文字がブランク,NULL値がNULLと表示されています。では,Excelデータをグリットに貼り付ける場合はどうでしょうか。

12行目にITEM_CODEをNULLと書き込んだデータを作り,SQL Server Management Studioに貼り付けてみます。

NULLがNULL値として保存されました。では文字列NULLを入力したいときは...とどんどん想像がふくらんでしまいます。(答えは'NULL' ) SQL Server Management Studioで直接編集する場合は,英数大文字で,NULLと入力するかctl+0になります。それにしてもExcel上のNULL文字列がDBのNULLになるとは,普通はわからないでしょう。
こうしたことが起こるのは,ブランクとNULLをシート上自然な仕方で区分けすることができないからです。リレーショナルデータベースはテーブルという二次元形式でデータを管理するところから始まっていますから,その意味では,ブランクとNULLがシートという二次元形式上区別できないのであれば,これを別のものとして扱う必然性がどの程度あるのか,という疑問も発生してきます。どっちが正しいかを議論し始めると宗教論争になるので,この辺にしておきましょう。

ただ,SQL ServerのNULLの扱いで困る事例を挙げておきましょう。
Oracleにおいて,文字列連結の途中にNULLが含まれても,NULLは無視されます。

しかし,SQL Serverでは,NULLが文字連結に含まれると,連結結果がNULLになってしまします。

これが困るのは,文字列処理の場合です。 ストアド内でデバッグ用にPRINT文を使用して,変数の値を出力させようとして以下のように記述したとします。
<style type="text/css"> </style> 0001 DECLARE @a VARCHAR(100) = 'a';
0002 DECLARE @b VARCHAR(100) = 'b';
0003 DECLARE @null VARCHAR(100) = NULL;
0004
0005 PRINT 'a=' + @a + 'b=' + @b + 'null=' + @null;
0006 PRINT 'a=' + @a;
0007 PRINT 'b=' + @b;
0008 PRINT 'null=' + @null;
この実行結果はどうなるでしょうか。
 
a=a
b=b
 
と1,4行目がブランクの結果となります。T-SQLの実装に慣れてくると,ブランク=NULLと直感できるようになるかと思います。でも,この記述で本来期待する出力とは異なっています。期待通りに出力するにはisnull関数を利用することになります。同様に,氏名をfirst,middle,lastの3項目に分けて格納し,SELECT時に文字列連結をしようとすると,同じようなことが起こります。最終的には,文字列連結をする全ての項目に対して,isnull関数の使用が必須になってしまったりします。(not null制約がある項目は不要ですが,それを一々考えるくらいなら,全部に適用した方がましという考え方です。)

NULLと空文字を区別するということが業務要件上,必要になる場合があるでしょうか。考えられるのは,NULLを未入力。空文字を「該当なし」と見なす方法です。しかし,テキストボックスで両者の違いを区別するには,どうしたらよいでしょうか。Windowsアプリであれば,キー入力によりNULLと空文字を判別できますし,テキストボックスの背景色をNULLで別な色にするとか,テキストボックスの隣に未入力/該当なしといった出力を行う方法が考えられます。しかし,Webのテキストボックスでは,入力時にNULLと空文字を区別させることは難しくなります。Excelに倣って''を空文字とするといった特別ルールが必要になってしまいます。結局,1テキストボックスでは表現しきれず,別に該当なしのチェックボックスを追加することになるでしょう。
DBの1項目が,画面上2コントロールに分かれて表現される...やっぱり何かおかしいと思いませんか。

入力内容がマスタ化できるのであれば,該当なしもマスタに登録して対応できるはずですし,自由入力形式がどうしても必要であれば,別項目として該当なしフラグを設けてあげれば,チェックボックスとの対応もうまくいきます。こちらの方がすっきりしていると感じるのは私だけでしょうか。

直感的な記述方法が期待通り動かず,単純なことを複雑な方法で表現しなければならない,というのは,仕様としてどうなのだろうかと思います。

さて,NULLと空文字が併存する場合,SQL上の違いがあるでしょうか。
先ほどのデータに対してITEM_ID=1のレコードのITEM_CODEをNULLに設定して
SELECT *
FROM
TT_ITEMS ti
WHERE
ti.ITEM_CODE LIKE '%'
を実行すると,空文字のレコードは検索されますが,NULLのレコードは検索結果に含まれません。NULL値が like '%'に該当しない,というのはOracleも同じです。

データベースのNULLの考え方としては,SQL Serverの方が本来の考え方なのかもしれませんが,実用性から見るとOracleの実装は納得させられます。プログラムは理念ではないので。



ROWIDはどこ

2010-06-01 16:34:11 | 開発サイドから見たOracleとSQL Server
「最初に学んだDataBaseがOracleだった」という方にとっては,ROWIDは必然的な存在に思えるかもしれません。しかし,これはOracleの実装であり,SQL ServerにはROWIDがありません。ですので,それぞれに応じた開発が必要になります。

固定値テーブルを以下のように作成したとします。本来は主キーがkey1,key2,key3に設定されますが,今は,キーを付けていない状態です。
Oracle
<style type="text/css">? </style>
0001 -- Create table
0002 create table TT_FIX_VALUE
0003 (
0004 KEY1 VARCHAR2(50),
0005 KEY2 VARCHAR2(50),
0006 KEY3 VARCHAR2(50),
0007 FIX_VALUE VARCHAR2(500)
0008 )

SQL Server
<style type="text/css"> </style>
0001 CREATE TABLE [dbo].[TTFixValue](
0002 [key1] [varchar](50) NULL,
0003 [key2] [varchar](50) NULL,
0004 [key3] [varchar](50) NULL,
0005 [fix_value] [varchar](500) NULL
0006 ) ON [PRIMARY]
さて,固定値をExcelのシート上に作成し,PL/SQL Developer,SQL Server Management Studioそれぞれを使いコピペしたとしましょう。PL/SQL Developerは,select t.*, rowid from TT_FIX_VALUE tを実行させ,空白の実行結果に対してexport to Excelのコンテキストメニューを使うことにより,ヘッダ部分のみのExcelシートを作成でき,便利です。

既に気づかれたかもしれませんが,4件目(Excel5行目)のデータは1件目と同じです。コピペ増殖させようとして変更漏れがあったと思ってください。これを作成した固定値テーブルに貼り付け登録します。どちらも登録ができます。さて,登録結果をセレクトして確認してみます。

ここで先ほどの変更漏れに気づいたとしましょう。Oracleは4レコード目をPL/SQL Developer上で変更し,反映-コミットをすれば修正が完了します。SQL Serverはどうでしょうか。

エラーメッセージを見るとSQL Server Management Studioが変更結果をUPDATEしようとしたものの,変更対象行が複数となるため,処理できないといっていることがわかります。こうなるとSQL Serverではお手上げです。UPDATEしようにも常に2行が同時に変更されるため,DELETEするしかありません。しかも,DELETEすると,1行目のレコードも削除されてしまうので,それも復元してやらねばなりません。
結論は何でしょうか。SQL Serverでは,主キーの設定が必須だということです。別にこれはSQL Serverが劣っているというわけではなく,通常のDBの仕様であるというだけです。このためにROWIDが存在しているわけではありませんが,ROWIDの実装とSQL上にROWIDを公開しているメリットは大きい,とも言えます。






プロシージャ呼び出し時のパラメータ

2010-06-01 10:01:43 | 開発サイドから見たOracleとSQL Server
OracleもSQL Serverもプロシージャでの実行結果を,引数に格納して返却することができるのは当然ですが,その記述方法は異なります。

プロシージャの宣言時にパラメータの入出力を明記するのは共に同じです。
T-SQLの場合
<style type="text/css"> </style>
0001 CREATE PROCEDURE uspGetCustomerSubID
0002 (@CustomerID INT, @CustomerSubID INT OUTPUT)
0003 AS
Oracleの場合
<style type="text/css"> </style>
0001 CREATE OR REPLACE PROCEDURE HOGE(P_PRAM1  IN NUMBER,
0002 P_PARAM2 OUT NUMBER,
0003 P_PARAM IN OUT NUMBER) IS
0004 BEGIN
しかし,T-SQLのINパラメータは,Call by Value(値渡し)の変数と見なされるのに対し,PL/SQLのINパラメータは,定数と見なされます。
ですから,上の例では,T-SQLの@CustomerIDに対して代入ができますが,PL/SQLのP_PRAM1の値をプロシージャ内で変更しようとするエラーとなります。
Oracleの定数と見なす仕様は,内部実装を増大させる場合があります。

一例として,抽出条件に日付型のパラメータを利用する場合を考えてみましょう。抽出条件に以下のような仕様が記載されていることがあります。
(1)日付が指定された場合は,その日以降のデータを取得する。
(2)日付が指定されていない場合,月初n日までは,前月1日からのデータを,それ以降は当月のデータを取得する。
(1)の条件だけならば,引数のディフォルト値を記述することで対応できそうですが,(2)も入って来ると,引数の対象開始日のパラメータをストアド内部で変換する必要が出てきます。そのため,PL/SQLで上記のような処理をするには,別途対象開始日の変数を準備しなければなりません。
<style type="text/css"> </style>
0001 CREATE OR REPLACE PROCEDURE TT_PARAM(P_ORDER_DATE_FROM IN DATE) IS
0002 V_ORDER_DATE_FROM DATE;
0003 C_THRESHOLD CONSTANT INT := 10; -- 閾値
0004 BEGIN
0005 IF P_ORDER_DATE_FROM IS NULL THEN
0006 IF TO_NUMBER(TO_CHAR(SYSDATE, 'DD')) <= C_THRESHOLD THEN
0007 V_ORDER_DATE_FROM := TRUNC(ADD_MONTHS(SYSDATE, -1), 'MONTH');
0008 ELSE
0009 V_ORDER_DATE_FROM := TRUNC(ADD_MONTHS(SYSDATE, -1), 'MONTH') + 15;
0010 END IF;
0011 ELSE
0012 V_ORDER_DATE_FROM := P_ORDER_DATE_FROM;
0013 END IF;
0014 DBMS_OUTPUT.PUT_LINE('P_ORDER_DATE_FROM=' || P_ORDER_DATE_FROM ||
0015 ' V_ORDER_DATE_FROM' || V_ORDER_DATE_FROM);
0016 END TT_PARAM;
上記の例では,内部で変数に置き換えるパラメータが1個だけでしたが,これが複数個になってくるとややこしくなってきます。また,開発中に仕様変更が入ったりすることにより当初は,パラメータで処理できていたものが,変数化する必要が出てくる場合もあります。そうなるとV_XXXとP_XXXが併存するようになり,バグのリスクが増大します。都合の悪いことに,V_XXXとP_XXXのどちらを使ってもコンパイルエラーともなりません。とは言っても,パラメータと内部変数のプリフィックス以外を一致させていないと,パラメータの置き換えということがはっきりしなくなります。いずれにしても,実装側で言語仕様の問題を回避しようとすると,うまくいかなくなることは間違いありません。JavaにしろC#にしろパラメータの値渡しが存在するということからすれば,開発言語として必要な機能であるということです。

この例を見ると,T-SQLの方がストアド呼び出し処理において洗練されているように思えますが,そうとも言えません。逆にT-SQLからストアドを呼び出す場合を考えてみましょう。以下の例では,9行目で,@CustomerID = @CustomerIDとしていますが,この@CustomerIDは5行目で,計算されています。この一時変数@CustomerIDは本来不要で,EXECの呼び出し部分を
EXEC uspGetCustomerSubID
     @CustomerID = @CustomerID = ROUND(@CustomerFullID/100,0),
     @CustomerSubID = @subID OUTPUT
と書けばいいはずです。ところがこのように変更してT-SQLをコンパイルしようとするとエラーとなります。そのエラー内容も誤解を招きやすいものです。
メッセージ 102、レベル 15、状態 1、行 10
'@CustomerFullID' 付近に不適切な構文があります。

このエラーメッセージを見たとき,最初に考えるのは,変数名のタイプミスか,ROUND関数のパラメータ指定を間違えたかということでしょう。私もこの最初にこの問題に直面したときにはそのように考え,あれこれと調べたり,試行錯誤しました。そして,初めて一時変数を使用しないといけないという事実を理解しました。
<style type="text/css"> </style>
0001 DECLARE @subID           INT;
0002 DECLARE @CustomerFullID INT = 1099;
0003 DECLARE @CustomerID INT;
0004
0005 SET @CustomerID = ROUND(@CustomerFullID/100,0);
0006 PRINT @CustomerID;
0007
0008 EXEC uspGetCustomerSubID
0009 @CustomerID = @CustomerID,
0010 @CustomerSubID = @subID OUTPUT
0011
0012 PRINT @subID;
0013 GO
これは,T-SQLの欠点であると思います。なんでストアドプロシージャを実行させるためだけに変数の宣言が必要となるのでしょう。プロパティキャッシュのようにレスポンス向上が見込めるわけでもないのですから。

以上,PL/SQLとT-SQLのパラメータに関わるトピックスでした。





PL/SQLとT-SQL-02 CREATEとREPLACE

2010-05-31 20:58:22 | 開発サイドから見たOracleとSQL Server
ストアドプロシージャの書き出しは,Oracleの場合, <title>SQL.sql</title> <style type="text/css"> </style>
0001 CREATE OR REPLACE PROCEDURE HOGE(NAME IN OUT TYPE,
0002 NAME IN OUT TYPE,
0003 .. .) IS
0004 BEGIN
0005
0006 END HOGE;
で書き始めます。
それに対してT-SQLでは,CREATE OR REPLACE文が使えません。
CREATE か REPLACE のいずれかはOKなのですが,CREATE OR REPLACEと書くとエラーになってしまします。
とはいえ,DB中に既にオブジェクトが存在しているかどうかで一々書き分けてはいられないので,SQL Serverの場合,同一名称のオブジェクトがある場合一旦DROPするスクリプトを付けます。

0001 IF OBJECT_ID('uspGetCustomerSubID') IS NOT NULL
0002 BEGIN
0003 PRINT 'Dropping procedure'
0004 DROP PROCEDURE uspGetCustomerSubID;
0005 IF @@ERROR = 0
0006 PRINT 'Procedure dropped'
0007 END
0008 GO
0009
0010 CREATE PROCEDURE uspGetCustomerSubID
0011 /***********************************************************
0012 * Procedure description:
0013 * Date: 2010/05/31
0014 * Author:
0015 *
0016 * Changes
0017 * Date Modified By Comments
0018 ************************************************************
0019 *
0020 ************************************************************/
0021 (@CustomerID INT, @CustomerSubID INT OUTPUT)
0022 AS


SQL Serverのこの仕様は,SQL Serverの初期の頃から変わっていません。
大差ないような気がしますか。もし,ストアドの名称が変更になったときにはどうでしょうか。
置換箇所が多いということは修正漏れが発生するリスクがあるということです。OracleのCREATE OR REPLACE文の方が簡潔に表記でき,トラブルも少ないので,SQL Serverでも仕様として取り込んで欲しいといつも思っています。