PL/SQLのIF文は「条件が成立したら処理を実行する」基本制御構造ですが、Oracle特有の3値論理(TRUE/FALSE/NULL)と組み合わさって、他言語では見られないバグを生みます。IF v_col = 10 THEN ...がv_colがNULLだと何も実行されない——この挙動を知らないと本番で条件分岐を読み飛ばす事故が起きます。
入門記事ではIF/ELSIF/ELSEの基本構文までで止まるものが多いですが、実務ではNULL安全な書き方、CASE文との使い分け、短絡評価の活用、BOOLEANへの変数格納、バルク処理との組み合わせ、例外処理連携、SQL側のCASE式/DECODEとPL/SQL側のIFの役割分担まで押さえる必要があります。
この記事では、PL/SQLのIF文を基礎から実務パターンまで徹底的に解説します。3形態(IF/IF-ELSE/IF-ELSIF-ELSE)、NULL地雷6種、短絡評価、IF vs CASE使い分け表、ネスト vs ELSIFのパフォーマンス、Cursor/コレクション/例外との連携、実務10パターン、アンチパターンと対策まで網羅した決定版です。関連する【PL/SQL】例外処理の書き方/【PL/SQL】ループ処理を行う方法/【PL/SQL】変数・定数の使い方も併読推奨です。
この記事で学べること
IF文の3形態(単純/IF-ELSE/IF-ELSIF-ELSE)と書き分け- 3値論理のNULL地雷6種と
IS NULL/NVLでの回避策 - AND/OR の短絡評価でNULL事故を回避する実戦テクニック
- 比較演算子と
BETWEEN/IN/LIKE/IS [NOT] NULL IFvsCASE文vsCASE式の明確な使い分け- SQL内の
DECODE/CASEとPL/SQLIFの役割分担 - ネスト
IFvsELSIFの可読性&パフォーマンス - Cursor/コレクション/例外処理との組み合わせ実装
- 実務10パターン(バリデーション/ステータス遷移/バッチ分岐等)
- アンチパターン7選とリファクタリング方針
DBMS_OUTPUT/dbms_utility.format_call_stackでのデバッグ
30秒クイックリファレンス:IF文の3形態
-- 形態①:IF THEN END IF(単純分岐) IF 条件 THEN 処理; END IF; -- 形態②:IF THEN ELSE END IF(2分岐) IF 条件 THEN 処理A; ELSE 処理B; END IF; -- 形態③:IF THEN ELSIF ELSE END IF(多段分岐) IF 条件1 THEN 処理1; ELSIF 条件2 THEN -- ELSEIFではなく ELSIF(スペースなし) 処理2; ELSIF 条件3 THEN 処理3; ELSE デフォルト処理; END IF;
PL/SQLの綴り罠:ELSIF(スペースなし、EとSの間にスペース入れない)——他言語のelse if/elseif/elifと違う独特の綴り。これを間違えるとPLS-00103: Encountered the symbol "ELSE IF"エラー。終端もEND IF;(スペース有り+セミコロン)が必須。
最小動作サンプル
DECLARE
v_score NUMBER := 85;
BEGIN
IF v_score >= 90 THEN
DBMS_OUTPUT.PUT_LINE('優秀');
ELSIF v_score >= 70 THEN
DBMS_OUTPUT.PUT_LINE('合格');
ELSE
DBMS_OUTPUT.PUT_LINE('不合格');
END IF;
END;
/
-- 出力: 合格
最重要:PL/SQLの3値論理とNULL地雷
PL/SQL(およびSQL全般)は論理値にTRUE/FALSE/NULLの3種があります。IF文ではTRUEの時だけTHEN分岐、FALSE/NULLの時はTHEN分岐しない(ELSEに行く)という挙動を理解しないと条件が意図せずスキップされます。
真理値表(必須)
DECLARE
v_status VARCHAR2(10); -- 初期値なし → NULL
BEGIN
IF v_status = 'active' THEN
DBMS_OUTPUT.PUT_LINE('active処理');
ELSIF v_status = 'inactive' THEN
DBMS_OUTPUT.PUT_LINE('inactive処理');
ELSE
DBMS_OUTPUT.PUT_LINE('その他');
END IF;
END;
/
-- 出力: その他
-- NULL = 'active' は NULL(TRUEではない)、
-- NULL = 'inactive' も NULL(FALSEではない)、
-- どちらもTHENへ行かずELSEに落ちる
NULL安全な6つの回避テクニック
-- ①IS NULLで明示 IF v_status IS NULL THEN -- NULLの時の処理 END IF; -- ②NVLでデフォルト値を与える IF NVL(v_status, 'unknown') = 'active' THEN -- v_statusがNULLでも 'unknown' として評価される END IF; -- ③COALESCE(複数フォールバック) IF COALESCE(v_status, v_default, 'fallback') = 'active' THEN -- 最初の非NULL値を使う END IF; -- ④OR条件で両方カバー IF v_status = 'active' OR v_status IS NULL THEN -- NULLも含める END IF; -- ⑤DECODE(SQLライク、NULL同士もTRUE扱い) IF DECODE(v_status, 'active', 1, 0) = 1 THEN -- NULLの場合は0(FALSE相当) END IF; -- ⑥BOOLEAN変数に明示代入(可読性UP) v_is_active BOOLEAN := (v_status = 'active'); -- NULL時はNULL IF v_is_active = TRUE THEN -- 明示比較でNULL回避 -- 処理 END IF;
実務の黄金律:「値が来るかNULLか不明」な変数/カラムをIFに渡す時は必ずNVLまたはIS NULLで包む。特に外部データ由来(OCIリクエスト/CSV取り込み/外部API)はNULLが混入する前提で設計すると事故激減します。
AND/ORの短絡評価(Short-Circuit Evaluation)
PL/SQLは短絡評価を保証しており、左から順に評価し結果が確定した時点で残りを評価しません。NULLによる除算エラーやオブジェクト未初期化エラーの回避に使える重要なテクニックです。
-- AND:左がFALSEなら右は評価されない
IF v_collection IS NOT NULL AND v_collection.COUNT > 0 THEN
-- v_collectionがNULLなら右側の .COUNT は呼ばれない
-- ELSE系のNULL参照エラー回避
FOR i IN 1..v_collection.COUNT LOOP
...処理...
END LOOP;
END IF;
-- OR:左がTRUEなら右は評価されない
IF v_is_admin OR v_user_has_permission(v_id) THEN
-- v_is_adminがTRUEなら重いfunction呼び出しをスキップ
-- 性能最適化にも使える
PROCESS();
END IF;
-- 除算エラー回避
IF v_divisor != 0 AND (v_dividend / v_divisor) > 1 THEN
-- v_divisorが0なら右辺を評価しない → ZERO_DIVIDE例外を回避
...
END IF;
短絡評価を当て込まない書き方(アンチパターン)
他言語経験者が書きがちなIF v_coll.COUNT > 0 AND v_coll IS NOT NULL THENはNULL時に例外発生。条件の順序は「安全性のガード→本質の判定」の順に並べるのが鉄則。PL/SQLの短絡評価は明示的に保証されているため安心して頼れます。
比較演算子と特殊構文
-- 基本演算子
= != <> ^= -- 等価/不等価(!= と <> と ^= は全て同じ)
< > <= >= -- 大小比較
-- NULL専用
IF v_x IS NULL THEN ...
IF v_x IS NOT NULL THEN ...
-- 範囲
IF v_score BETWEEN 70 AND 89 THEN ... -- 境界を含む
IF v_score NOT BETWEEN 0 AND 59 THEN ...
-- 複数値
IF v_status IN ('active', 'pending', 'review') THEN ...
IF v_status NOT IN ('deleted', 'banned') THEN ...
-- 文字列パターン
IF v_email LIKE '%@example.com' THEN ...
IF v_code LIKE 'A__' THEN ... -- _ は1文字
IF v_code LIKE 'A\_%' ESCAPE '\' THEN ... -- リテラル_
-- 正規表現(REGEXP_LIKE)
IF REGEXP_LIKE(v_phone, '^\d{3}-\d{4}-\d{4}$') THEN ...
IN句のNULL挙動に注意:v_status IN ('a', 'b', NULL)はv_status = 'a' OR v_status = 'b' OR v_status = NULLに展開され、NULL同士の一致もNULLで判定されるためTRUEにはなりません。NOT INも同様の罠がある(SQL側ではさらに顕著)ので要注意。
IF文 vs CASE文 vs CASE式:使い分け完全版
PL/SQLにはIF文以外にも条件分岐手段が3つあり、それぞれ向き不向きがあります。適切に使い分けることで可読性と性能が向上します。
-- 各分岐で複数の処理がある時はIF文が自然
IF v_score >= 90 THEN
DBMS_OUTPUT.PUT_LINE('優秀');
UPDATE users SET rank = 1 WHERE id = v_id;
send_congrats_email(v_id);
ELSIF v_score >= 70 THEN
DBMS_OUTPUT.PUT_LINE('合格');
UPDATE users SET rank = 2 WHERE id = v_id;
ELSE
DBMS_OUTPUT.PUT_LINE('不合格');
UPDATE users SET rank = 3 WHERE id = v_id;
END IF;
-- 1つの変数を複数の値と比較する時はCASE文が読みやすい
CASE v_status
WHEN 'active' THEN
process_active();
WHEN 'pending' THEN
process_pending();
WHEN 'closed' THEN
process_closed();
ELSE
process_unknown();
END CASE;
-- IFと同じ表現力だが整列した記述が可能
CASE
WHEN v_score >= 90 AND v_attended THEN
DBMS_OUTPUT.PUT_LINE('優秀+皆勤');
WHEN v_score >= 70 THEN
DBMS_OUTPUT.PUT_LINE('合格');
ELSE
DBMS_OUTPUT.PUT_LINE('不合格');
END CASE;
-- 変数への代入が1行で完結
DECLARE
v_rank VARCHAR2(10);
BEGIN
v_rank := CASE
WHEN v_score >= 90 THEN 'A'
WHEN v_score >= 70 THEN 'B'
WHEN v_score >= 50 THEN 'C'
ELSE 'F'
END;
DBMS_OUTPUT.PUT_LINE('Rank: ' || v_rank);
END;
使い分けフロー:①各分岐で複数ステップの処理 → IF文、②単一変数の値で分岐 → CASE文(Simple)、③複雑条件で処理分岐 → CASE文(Searched)、④値を返す(代入/SQL内部)→ CASE式。特にCASE式はSQL側にも同じ構文があるためPL/SQL+SQLの統一感が出ます。
CASE文でもNULL注意
-- ❌ 意図通り動かない(Simple CASEはNULL比較に = を使う=NULL同士は一致しない)
CASE v_status
WHEN NULL THEN
DBMS_OUTPUT.PUT_LINE('NULLの処理'); -- ここは呼ばれない
ELSE
DBMS_OUTPUT.PUT_LINE('ELSE');
END CASE;
-- ⭕ Searched CASEで IS NULL を使う
CASE
WHEN v_status IS NULL THEN
DBMS_OUTPUT.PUT_LINE('NULLの処理');
WHEN v_status = 'active' THEN
DBMS_OUTPUT.PUT_LINE('active');
ELSE
DBMS_OUTPUT.PUT_LINE('ELSE');
END CASE;
CASE文でELSE省略時のCASE_NOT_FOUND例外:PL/SQLのCASE文(Statement)はどのWHENにも該当せずELSEも無いとCASE_NOT_FOUND例外が発生。SQLのCASE式はELSE省略時NULLを返すのと異なるので混乱しないよう注意。PL/SQL CASE文は必ずELSEを書くのが安全。
SQL内の条件分岐(DECODE/CASE)と使い分け
IF文はPL/SQLブロック内のみ使えます。SELECT文やWHERE句で条件分岐したい場合はDECODE(Oracle独自)かCASE式(標準SQL)を使います。
-- DECODE(Oracle独自、短い)
SELECT id, name,
DECODE(status, 'A', '有効',
'I', '無効',
'D', '削除',
'その他') AS status_label
FROM users;
-- CASE式(標準SQL、ほぼ同じ結果で読みやすい)
SELECT id, name,
CASE status
WHEN 'A' THEN '有効'
WHEN 'I' THEN '無効'
WHEN 'D' THEN '削除'
ELSE 'その他'
END AS status_label
FROM users;
-- WHERE句内でCASE式
SELECT * FROM orders
WHERE CASE WHEN priority = 'urgent' THEN days_open
ELSE days_open - 7
END > 30;
DECODE vs CASE式の選び方
- DECODE:Oracle特有、短く書ける、単一値比較に最適、NULL同士もマッチする(これは便利)
- CASE式:標準SQL(PostgreSQL等に移行可能)、範囲条件OK、可読性高
- 新規コードはCASE式推奨(他DBとの互換性)
- DECODEは既存コード保守時やOracle固有の場面でのみ
ネストIF vs ELSIF:可読性とパフォーマンス
-- ❌ 深いネストは可読性が落ちる
IF v_status = 'active' THEN
IF v_age >= 18 THEN
IF v_country = 'JP' THEN
IF v_verified THEN
process();
END IF;
END IF;
END IF;
END IF;
-- ⭕ ANDで1つの条件にまとめる
IF v_status = 'active' AND v_age >= 18
AND v_country = 'JP' AND v_verified THEN
process();
END IF;
-- ⭕ ガード句でearly return(PROCEDURE/FUNCTION内)
IF v_status != 'active' THEN RETURN; END IF;
IF v_age < 18 THEN RETURN; END IF;
IF v_country != 'JP' THEN RETURN; END IF;
IF NOT v_verified THEN RETURN; END IF;
process(); -- ← ここに着たら条件全部OK
ELSIF連鎖とCASE文の比較
-- ❌ 冗長な ELSIF IF v_code = 'A' THEN process_a(); ELSIF v_code = 'B' THEN process_b(); ELSIF v_code = 'C' THEN process_c(); ELSIF v_code = 'D' THEN process_d(); ... END IF; -- ⭕ CASE文で整理 CASE v_code WHEN 'A' THEN process_a(); WHEN 'B' THEN process_b(); WHEN 'C' THEN process_c(); WHEN 'D' THEN process_d(); ELSE process_default(); END CASE;
性能差はほぼなし(コンパイル時に最適化される)のですが、可読性は別次元で違います。保守性を上げるとバグ発見も早くなるので、ELSIF連鎖3段以上/ネスト2階層以上なら設計見直しのサインです。
IF文と例外処理の連携
DECLARE
v_user_count NUMBER;
e_no_user EXCEPTION;
e_too_many_users EXCEPTION;
BEGIN
SELECT COUNT(*) INTO v_user_count FROM users WHERE status = 'active';
IF v_user_count = 0 THEN
RAISE e_no_user;
ELSIF v_user_count > 10000 THEN
RAISE e_too_many_users;
END IF;
DBMS_OUTPUT.PUT_LINE('アクティブユーザー: ' || v_user_count);
EXCEPTION
WHEN e_no_user THEN
DBMS_OUTPUT.PUT_LINE('アクティブユーザーが存在しません');
WHEN e_too_many_users THEN
DBMS_OUTPUT.PUT_LINE('ユーザー数が上限を超えています');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('予期せぬエラー: ' || SQLERRM);
RAISE;
END;
/
RAISE_APPLICATION_ERROR でユーザーエラー
BEGIN
IF :new.amount < 0 THEN
RAISE_APPLICATION_ERROR(-20001, '金額はマイナスにできません');
ELSIF :new.amount > 1000000 THEN
RAISE_APPLICATION_ERROR(-20002, '上限(100万)を超えています');
END IF;
END;
例外処理との組み合わせ方針
IFで「ビジネスルール違反」を検出しRAISE_APPLICATION_ERRORで呼び出し側にエラー通知、という組み合わせが実務でも頻出。エラーコードは-20000〜-20999の範囲がユーザー定義用に予約されています。詳細は【PL/SQL】例外処理の書き方参照。
Cursor・コレクション × IF文の実戦パターン
Cursor FOR Loop内での分岐
BEGIN
FOR rec IN (SELECT id, status, amount FROM orders WHERE order_date >= TRUNC(SYSDATE) - 7) LOOP
IF rec.status = 'pending' AND rec.amount > 10000 THEN
-- 高額保留はマネージャ通知
notify_manager(rec.id);
ELSIF rec.status = 'failed' THEN
-- 失敗は再試行キューへ
INSERT INTO retry_queue VALUES (rec.id, SYSDATE);
END IF;
END LOOP;
COMMIT;
END;
コレクション × IF
DECLARE
TYPE t_ids IS TABLE OF NUMBER;
v_ids t_ids := t_ids();
BEGIN
-- コレクション空チェック
IF v_ids IS NULL OR v_ids.COUNT = 0 THEN
DBMS_OUTPUT.PUT_LINE('IDが空');
RETURN;
END IF;
-- ループ+分岐
FOR i IN 1..v_ids.COUNT LOOP
IF v_ids(i) IS NOT NULL AND MOD(v_ids(i), 2) = 0 THEN
DBMS_OUTPUT.PUT_LINE(v_ids(i) || ' は偶数');
END IF;
END LOOP;
END;
コレクション+IF の短絡評価は必須技:IF v_coll IS NOT NULL AND v_coll.COUNT > 0 THENの順で書く。逆にすると未初期化コレクションでNULL参照例外が発生します。詳細は【PL/SQL】コレクション(配列・ネスト表)の基本と活用例参照。
実務パターン10選
①ステータス遷移の妥当性チェック
-- 'draft' → 'review' → 'published' → 'archived' の順のみ許可
PROCEDURE change_status(p_id IN NUMBER, p_new_status IN VARCHAR2) IS
v_current VARCHAR2(20);
BEGIN
SELECT status INTO v_current FROM articles WHERE id = p_id;
IF v_current = 'draft' AND p_new_status NOT IN ('review', 'archived') THEN
RAISE_APPLICATION_ERROR(-20001, 'draftから遷移可能なのはreview/archivedのみ');
ELSIF v_current = 'review' AND p_new_status NOT IN ('draft', 'published') THEN
RAISE_APPLICATION_ERROR(-20002, 'reviewから遷移可能なのはdraft/publishedのみ');
ELSIF v_current = 'published' AND p_new_status != 'archived' THEN
RAISE_APPLICATION_ERROR(-20003, 'publishedから遷移可能なのはarchivedのみ');
END IF;
UPDATE articles SET status = p_new_status, updated_at = SYSDATE WHERE id = p_id;
END;
②バッチ処理の時間帯分岐
BEGIN
IF TO_CHAR(SYSDATE, 'HH24') BETWEEN '02' AND '05' THEN
-- 深夜2時〜5時は重いバッチOK
rebuild_indexes();
gather_full_stats();
ELSIF TO_CHAR(SYSDATE, 'DY') = 'SAT' THEN
-- 土曜は重めメンテOK
gather_full_stats();
ELSE
-- 平日日中は軽量処理のみ
gather_lite_stats();
END IF;
END;
③入力バリデーション(複合)
PROCEDURE register_user(
p_email IN VARCHAR2,
p_age IN NUMBER,
p_password IN VARCHAR2
) IS
BEGIN
IF p_email IS NULL OR NOT REGEXP_LIKE(p_email, '^[^@]+@[^@]+\.[^@]+$') THEN
RAISE_APPLICATION_ERROR(-20101, '無効なメールアドレス');
END IF;
IF p_age IS NULL OR p_age < 18 OR p_age > 120 THEN
RAISE_APPLICATION_ERROR(-20102, '年齢は18〜120');
END IF;
IF LENGTH(p_password) < 8 THEN
RAISE_APPLICATION_ERROR(-20103, 'パスワードは8文字以上');
END IF;
INSERT INTO users(email, age, password_hash)
VALUES (p_email, p_age, hash_password(p_password));
END;
④権限分岐
PROCEDURE delete_record(p_user_id IN NUMBER, p_record_id IN NUMBER) IS
v_role VARCHAR2(20);
v_owner_id NUMBER;
BEGIN
SELECT role INTO v_role FROM users WHERE id = p_user_id;
SELECT owner_id INTO v_owner_id FROM records WHERE id = p_record_id;
IF v_role = 'admin' THEN
DELETE FROM records WHERE id = p_record_id;
ELSIF v_role = 'manager' AND v_owner_id = p_user_id THEN
DELETE FROM records WHERE id = p_record_id;
ELSIF v_owner_id = p_user_id THEN
-- 一般ユーザーは自分のレコードのみ
UPDATE records SET status = 'deleted' WHERE id = p_record_id;
ELSE
RAISE_APPLICATION_ERROR(-20201, '削除権限がありません');
END IF;
END;
⑤デフォルト値の計算
FUNCTION calc_shipping_fee(p_order_id IN NUMBER) RETURN NUMBER IS
v_amount NUMBER;
v_country VARCHAR2(2);
v_fee NUMBER;
BEGIN
SELECT total_amount, country INTO v_amount, v_country
FROM orders WHERE id = p_order_id;
IF v_country = 'JP' THEN
IF v_amount >= 10000 THEN v_fee := 0;
ELSIF v_amount >= 5000 THEN v_fee := 300;
ELSE v_fee := 600;
END IF;
ELSE
-- 海外配送
v_fee := CASE
WHEN v_amount >= 20000 THEN 0
ELSE 2000
END;
END IF;
RETURN v_fee;
END;
⑥Feature Flag(機能切替)
DECLARE
v_feature_enabled VARCHAR2(1);
BEGIN
SELECT enabled INTO v_feature_enabled FROM feature_flags WHERE feature = 'new_pricing';
IF v_feature_enabled = 'Y' THEN
apply_new_pricing(p_order_id);
ELSE
apply_legacy_pricing(p_order_id);
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- デフォルトはlegacy
apply_legacy_pricing(p_order_id);
END;
⑦NULL許可/必須チェック
IF p_name IS NULL THEN RAISE_APPLICATION_ERROR(-20301, '名前は必須'); ELSIF TRIM(p_name) IS NULL THEN -- 空白のみはNULL扱い(OracleのVARCHAR2特性) RAISE_APPLICATION_ERROR(-20302, '名前は空白以外'); END IF;
⑧クエリ結果0件のハンドリング
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM orders WHERE user_id = p_user_id;
IF v_count = 0 THEN
DBMS_OUTPUT.PUT_LINE('注文履歴なし');
ELSIF v_count = 1 THEN
show_single_order(p_user_id);
ELSE
show_order_list(p_user_id);
END IF;
END;
⑨バッチ中のリトライ判定
DECLARE
v_retry_count NUMBER := 0;
v_max_retry CONSTANT NUMBER := 3;
v_success BOOLEAN := FALSE;
BEGIN
WHILE v_retry_count < v_max_retry AND NOT v_success LOOP
BEGIN
call_external_api(p_id);
v_success := TRUE;
EXCEPTION
WHEN OTHERS THEN
v_retry_count := v_retry_count + 1;
DBMS_OUTPUT.PUT_LINE('Retry ' || v_retry_count);
DBMS_LOCK.SLEEP(2 ** v_retry_count); -- Exponential backoff
END;
END LOOP;
IF NOT v_success THEN
RAISE_APPLICATION_ERROR(-20401, 'リトライ上限到達');
END IF;
END;
⑩トリガー内でのBefore/After判定
CREATE OR REPLACE TRIGGER trg_orders_validate
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
BEGIN
-- INSERTとUPDATEで処理を分ける
IF INSERTING THEN
:NEW.created_at := SYSDATE;
:NEW.updated_at := SYSDATE;
ELSIF UPDATING THEN
:NEW.updated_at := SYSDATE;
IF :OLD.status = 'closed' AND :NEW.status != 'closed' THEN
RAISE_APPLICATION_ERROR(-20501, 'closed状態から復帰できません');
END IF;
END IF;
END;
アンチパターン7選
①IF v_flag = TRUE THEN+v_flag未初期化。v_flagがNULLだとTHEN分岐されない。初期値を明示(v_flag BOOLEAN := FALSE;)。
②IF v_col = some_val THEN ELSEでNULL暗黙処理。v_colがNULLなら無条件でELSEへ落ちる。「ELSEはNULL含む」と誤解しないようNVL/IS NULLを併記。
③深すぎるネスト(4階層以上)。ANDで条件統合、early returnパターン、または関数抽出で平坦化。
④同じ変数の多段ELSIF。IF x=1 ELSIF x=2 ELSIF x=3 ...はCASE文(Simple CASE)でCASE x WHEN 1 ... WHEN 2 ... END CASE;にリファクタ。
⑤IF内で重複した副作用処理。各THEN/ELSIFで同じログ出力やINSERTを繰り返すなら共通処理をIF外に出す、もしくはl_ok BOOLEAN := FALSE;で最後に一括。
⑥PL/SQLのCASE文でELSEを省略。どのWHENにも該当しないとCASE_NOT_FOUND例外。必ずELSEを書く(SQLのCASE式とは挙動が違う)。
⑦ビジネスロジックのIFをPL/SQLに閉じ込める。アプリ層でやるべき判断までDBに押し込むとテスト困難&移行困難。DB内はデータ整合性チェック(NOT NULL/制約/トリガー)に留め、ビジネスロジックはアプリ層で書く方針が拡張しやすい。
IF文のデバッグ術
DECLARE
v_status VARCHAR2(20) := 'pending';
v_amount NUMBER := 5000;
BEGIN
DBMS_OUTPUT.PUT_LINE('v_status=' || v_status);
DBMS_OUTPUT.PUT_LINE('v_amount=' || v_amount);
IF v_status = 'active' THEN
DBMS_OUTPUT.PUT_LINE('→ active分岐');
...
ELSIF v_status = 'pending' AND v_amount > 3000 THEN
DBMS_OUTPUT.PUT_LINE('→ pending+高額分岐');
...
ELSE
DBMS_OUTPUT.PUT_LINE('→ ELSE');
...
END IF;
END;
/
-- 実行前に SET SERVEROUTPUT ON;
条件式を変数に出す(可読性+デバッグしやすい)
DECLARE
v_is_active BOOLEAN;
v_is_high_value BOOLEAN;
v_is_priority BOOLEAN;
BEGIN
v_is_active := (v_status = 'active');
v_is_high_value := (v_amount >= 10000);
v_is_priority := (v_type IN ('VIP', 'PREMIUM'));
DBMS_OUTPUT.PUT_LINE('active=' || CASE WHEN v_is_active THEN 'Y' ELSE 'N' END);
DBMS_OUTPUT.PUT_LINE('high=' || CASE WHEN v_is_high_value THEN 'Y' ELSE 'N' END);
DBMS_OUTPUT.PUT_LINE('priority=' || CASE WHEN v_is_priority THEN 'Y' ELSE 'N' END);
IF v_is_active AND (v_is_high_value OR v_is_priority) THEN
process_special();
END IF;
END;
BOOLEAN変数化の効果:①各条件が名前付きになり可読性UP、②DBMS_OUTPUTで状態を一覧でき、③複雑条件が真になる組み合わせを追跡しやすい、④単体テスト的なassertionも書きやすい。複雑な条件分岐はまず変数化してから組み立てるのが熟練者のパターン。
よくある質問
ELSIF(スペース無し)。他言語のelseif/else if/elifとは違うので注意。ELSE IFと書くとPLS-00103コンパイルエラー。NULL = NULLやNULL != NULLはTRUEでもFALSEでもなくNULL。IFはTRUEの時だけTHENに行くので、NULL比較は常にTHENを飛ばします。IS NULL/IS NOT NULL/NVL/COALESCEで明示的にNULLを扱うのが鉄則。CASE v WHEN ...)、③複雑条件で処理分岐→Searched CASE文(CASE WHEN v >= 10 ...)、④値を返す→CASE式(変数代入、SQL内)。基本はIF、多段比較ならCASEが読みやすい。CHAR(1)(’Y’/’N’)やNUMBER(1)(0/1)で代用が一般的。IF v_coll IS NOT NULL AND v_coll.COUNT > 0 THENの順でガードが効き、NULL参照例外を回避できます。条件の順序は意味を持つので注意深く書きましょう。EXITはループ内でのみ有効、RETURNはPROCEDURE/FUNCTION内で有効。IF文自体の脱出はEND IF;で自動的に行われます。早期脱出が必要ならガード句パターン(条件NGならRETURN)を使います。CASE文(Statement)でどのWHENにも該当せずELSEも無い時に発生。解決はELSE NULL;やELSE <デフォルト処理>を必ず書くこと。SQLのCASE式はELSE省略時NULLを返す別仕様なので混同しないよう注意。JOIN)。深ネスト&長ELSIFは設計見直しのサインです。IFで判定すると効きます。関連記事
- 【PL/SQL】例外処理の書き方と使い方|WHEN OTHERSの注意点も解説 — IFで検出→RAISEの連携
- 【PL/SQL】ループ処理を行う方法 — LOOP内のIF分岐
- 【PL/SQL】カーソルでSQLクエリで取得した複数の行を1行ずつ処理する方法 — Cursor + IF実戦
- 【PL/SQL】変数・定数の使い方 — BOOLEAN型含む変数の基礎
- 【PL/SQL】初心者でもわかる基本構文とブロック構造の書き方 — PL/SQLの土台
- 【PL/SQL】コレクション(配列・ネスト表)の基本と活用例 — IFと組み合わせる配列操作
- 【PL/SQL】ストアドプロシージャとファンクションの違いと作り方 — IFを含む実装パターン
- 【PL/SQL】IN・OUT・IN OUTパラメータの完全ガイド — パラメータのバリデーションIF
まとめ
- IF文の3形態:IF THEN/IF THEN ELSE/IF THEN ELSIF ELSE(綴りは
ELSIF、スペース無し) - 3値論理(TRUE/FALSE/NULL)でNULL比較は全てNULL→分岐スキップ
- NULL対策6種:
IS NULL/NVL/COALESCE/OR .. IS NULL/DECODE/BOOLEAN変数明示 - AND/ORの短絡評価は保証、NULL参照・除算エラー回避に活用
- 比較演算子:
=/<>/!=/BETWEEN/IN/LIKE/IS NULL/REGEXP_LIKE - 使い分け:IF文(複数処理)/Simple CASE(単変数)/Searched CASE(複雑)/CASE式(値を返す)
- SQL内は
DECODE/CASE式、PL/SQLブロック内はIF/CASE文 - 深ネスト回避:AND統合/early return/関数抽出/BOOLEAN変数化
- ELSIF連鎖3段以上は
CASE文へのリファクタを検討 - PL/SQL
CASE文はELSE省略でCASE_NOT_FOUND例外→必ずELSEを書く - 実務10パターン:状態遷移/時間帯分岐/バリデーション/権限/デフォルト計算/Feature Flag/必須チェック/0件ハンドリング/リトライ/トリガー
- アンチパターン回避:NULL前提の書き方/ANDで統合/CASEでリファクタ/必ずELSE/ビジネスロジックはアプリ層
- デバッグ:
DBMS_OUTPUT+BOOLEAN変数化で条件トレース
PL/SQLのIF文は「条件分岐」の表面機能ですが、3値論理/短絡評価/CASEとの使い分けを押さえることで、保守性と安全性が劇的に向上します。本記事のNULL対策6種と実務10パターンをベースに、チーム内のコーディング規約(必ずELSE/NVLでガード/深ネスト禁止)を整えれば、レビューの質も上がり本番事故も激減します。例外処理(例外処理)、ループ(LOOP)、Cursor(Cursor)と組み合わせて、堅牢なPL/SQLコードを書いてください。
