1. はじめに
レガシーシステムのバッチプログラムを解析していると、VB.NETなどでSQLを文字列として組み立てているコードに出会うことがあります。
特に分かりづらいのが、次のようなバインド変数を使ったUPDATE文です。
.AppendLine("UPDATE T_MEMBER SET ") .AppendLine(" LAST_UPDATE_YMD = CASE ") .AppendLine(" WHEN NVL(LAST_UPDATE_YMD, '1999/01/01') > TO_DATE(" & pLastUpdateYmd & ") ") .AppendLine(" THEN LAST_UPDATE_YMD ") .AppendLine(" ELSE " & pLastUpdateYmd & " END ")
さらに、そのバインド変数に対して、別のテーブルから取得した値を設定しているとします。
DBAccess.CreateParameter( cmd, pLastUpdateYmd, dr("UPDATE_YMD"), DbType.Date )
ここで、
pLastUpdateYmd という名前なのだから、T_MEMBER.LAST_UPDATE_YMD の値が入っているのでは?
と考えてしまうことがあります。
しかし、実際にはそうとは限りません。
このようなコードを正しく理解するには、
「バインド変数の名前」ではなく、「そのバインド変数に実際に何の値を設定しているか」
を見る必要があります。
本記事では、一般的なサンプルを使って、
- バインド変数とは何か
- 変数名とデータソースの関係
- 別テーブルの値がUPDATE先に入る仕組み
- 複雑な動的SQLを読むときのポイント
を整理します。
対象読者
- VB.NETなどの既存システムを解析している方
- レガシーシステムの保守・改修を担当している方
- 動的SQLを読むのが苦手な方
- バインド変数の仕組みを理解したい方
- 「この値はどこから来ているの?」というデータの流れを追いたい方
- Javaなど別言語へのシステム移行で、既存コードの処理を読み解く必要がある方
動的SQLで「なぜ別テーブルの値が更新される?」を読み解く
1. はじめに
レガシーシステムを解析していると、VB.NETなどでSQLを文字列として組み立てているコードに出会うことがあります。
例えば、次のようなUPDATE文です。
.AppendLine("UPDATE T_MEMBER SET ")
.AppendLine(" LAST_UPDATE_YMD = CASE ")
.AppendLine(" WHEN NVL(LAST_UPDATE_YMD, '1999/01/01') > TO_DATE(" & pLastUpdateYmd & ") ")
.AppendLine(" THEN LAST_UPDATE_YMD ")
.AppendLine(" ELSE " & pLastUpdateYmd & " END ")
さらに、pLastUpdateYmd に対して、別のテーブルから取得した値を設定しているとします。
DBAccess.CreateParameter(
cmd,
pLastUpdateYmd,
dr("UPDATE_YMD"),
DbType.Date
)
ここで、
UPDATEしているのは
T_MEMBERなのに、なぜ別テーブルのUPDATE_YMDが入るの?
と疑問に思うことがあります。
実は、このようなコードを読むときは、変数名と実際の値を切り分けて考えることが重要です。
2. 対象読者
この記事は、以下のような方を対象としています。
- VB.NETなどの既存システムを解析している方
- レガシーシステムの保守・改修を担当している方
- 動的SQLが分かりづらいと感じている方
- バインド変数の仕組みを理解したい方
- 「この値はどこから来たの?」とデータの流れを追う必要がある方
3. 変数名と「実際の値」は別物
例えば、次のような定義があったとします。
Dim pLastUpdateYmd As String = ":LAST_UPDATE_YMD"
このコードを見ると、
pLastUpdateYmd
↓
LAST_UPDATE_YMDの日付
と思ってしまうかもしれません。
しかし、実際に入っているのは日付ではなく、
":LAST_UPDATE_YMD"
というバインド変数の名前です。
バインド変数は、SQLの中で後から値を受け取るための「受け皿」のようなものです。
例えばSQLが、
UPDATE T_MEMBER
SET LAST_UPDATE_YMD = :LAST_UPDATE_YMD
WHERE MEMBER_ID = :MEMBER_ID
だった場合、
:LAST_UPDATE_YMD
は「ここに値を渡してください」という目印に過ぎません。
4. 実際に何の値が入るのか?
では、pLastUpdateYmd に実際の値を設定している処理を見てみます。
DBAccess.CreateParameter(
cmd,
pLastUpdateYmd,
dr("UPDATE_YMD"),
DbType.Date
)
ここで重要なのは、
dr("UPDATE_YMD")
です。
例えば、前段のSELECTで別テーブルから、
SELECT UPDATE_YMD
FROM T_UPDATE_HISTORY
WHERE MEMBER_ID = 1001
というデータを取得していたとします。
取得した値が、
2026/09/01
なら、
T_UPDATE_HISTORY.UPDATE_YMD
↓
dr("UPDATE_YMD")
↓
:LAST_UPDATE_YMD
↓
T_MEMBER.LAST_UPDATE_YMD
という流れになります。
つまり、
更新先と値の取得元は、同じテーブルである必要はありません。
今回の場合、
【値の取得元】
T_UPDATE_HISTORY.UPDATE_YMD
↓
【バインド変数】
:LAST_UPDATE_YMD
↓
【更新先】
T_MEMBER.LAST_UPDATE_YMD
となっています。
バインド変数の名前が LAST_UPDATE_YMD だからといって、そこに T_MEMBER.LAST_UPDATE_YMD の値が入っているとは限りません。
実際に何の値が入るかは、パラメータ設定処理を見る必要があります。
5. CASE文では何をしているのか?
今回のSQLでは、単純に値を上書きしているわけではありません。
CASE
WHEN NVL(LAST_UPDATE_YMD, DATE '1999-01-01')
> :LAST_UPDATE_YMD
THEN LAST_UPDATE_YMD
ELSE :LAST_UPDATE_YMD
END
これは、
現在登録されている日付と、新しく取得した日付を比較して、新しいほうを残す
という処理です。
例えば、
現在の値
2026/08/20
新しく取得した値
2026/09/01
なら、
2026/08/20 > 2026/09/01
は偽なので、
ELSE :LAST_UPDATE_YMD
が選ばれ、
2026/09/01
に更新されます。
逆に、
現在の値
2026/09/10
新しく取得した値
2026/09/01
なら、現在の値のほうが新しいため、
THEN LAST_UPDATE_YMD
が選ばれます。
そのため、古い値で新しい値を上書きすることを防げます。
6. 動的SQLを読むときのポイント
このようなコードを解析するときは、変数名だけを追わないことがポイントです。
次の順番で確認すると、データの流れを追いやすくなります。
① UPDATE先を確認
↓
② 更新対象のカラムを確認
↓
③ SQL内のバインド変数を確認
↓
④ そのバインド変数に何を設定しているか確認
↓
⑤ その値がどこから取得されたものか確認
特に重要なのは、
CreateParameter(
cmd,
pLastUpdateYmd,
dr("UPDATE_YMD"),
DbType.Date
)
のようなパラメータ設定部分です。
pLastUpdateYmd という名前を見るだけではなく、
「この変数に、実際には何の値を渡しているのか?」
を見ることで、データの出どころが分かります。
7. まとめ
動的SQLを解析するときに覚えておきたいのは、
「バインド変数の名前」と「そこに設定される値」は別物
ということです。
例えば、
Dim pLastUpdateYmd As String = ":LAST_UPDATE_YMD"
とあっても、これは日付データではなく、SQLで使用するバインド変数名です。
実際の値は、
DBAccess.CreateParameter(
cmd,
pLastUpdateYmd,
dr("UPDATE_YMD"),
DbType.Date
)
のような処理によって決まります。
そのため、
「なぜこのテーブルの値が、別のテーブルのカラムに入るのか?」
と疑問に思ったときは、変数名ではなく、
「値がどこから来て、どのバインド変数を経由して、どこへ入るのか」
というデータの流れを追ってみましょう。
レガシーシステムの解析では、変数名だけでは分からない「値のリレー」を見つけることが、処理を理解する大きな手がかりになります。