【PL/SQL】IF文完全ガイド|3値論理・NULL地雷6種・CASEとの使い分け・短絡評価・実務10パターン

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 NULLNVLでの回避策
  • AND/OR の短絡評価でNULL事故を回避する実戦テクニック
  • 比較演算子とBETWEENINLIKEIS [NOT] NULL
  • IF vs CASE文 vs CASE式の明確な使い分け
  • SQL内のDECODECASEとPL/SQLIFの役割分担
  • ネストIF vs ELSIF可読性&パフォーマンス
  • Cursor/コレクション/例外処理との組み合わせ実装
  • 実務10パターン(バリデーション/ステータス遷移/バッチ分岐等)
  • アンチパターン7選とリファクタリング方針
  • DBMS_OUTPUTdbms_utility.format_call_stackでのデバッグ
スポンサーリンク

30秒クイックリファレンス:IF文の3形態

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 ifelseifelifと違う独特の綴り。これを間違えると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に行く)という挙動を理解しないと条件が意図せずスキップされます。

真理値表(必須)

結果 IF内挙動
NULL = NULL NULL(TRUEではない) 分岐スキップ(ELSEへ)
NULL <> NULL NULL 分岐スキップ
NULL = 10 NULL 分岐スキップ
NULL IS NULL TRUE 分岐実行
TRUE AND NULL NULL 分岐スキップ
FALSE AND NULL FALSE 分岐スキップ
TRUE OR NULL TRUE 分岐実行
NOT NULL NULL 分岐スキップ
NULLによる意図しない分岐スキップ
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つの回避テクニック

NULL対策パターン
-- ①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 &gt; 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) &gt; 1 THEN
  -- v_divisorが0なら右辺を評価しない → ZERO_DIVIDE例外を回避
  ...
END IF;

短絡評価を当て込まない書き方(アンチパターン)

他言語経験者が書きがちなIF v_coll.COUNT > 0 AND v_coll IS NOT NULL THENはNULL時に例外発生。条件の順序は「安全性のガード→本質の判定」の順に並べるのが鉄則。PL/SQLの短絡評価は明示的に保証されているため安心して頼れます。

比較演算子と特殊構文

PL/SQLで使える比較演算子
-- 基本演算子
=   !=   &lt;&gt;   ^=        -- 等価/不等価(!= と &lt;&gt; と ^= は全て同じ)
&lt;   &gt;   &lt;=   &gt;=        -- 大小比較

-- 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文 処理(複数ステップ)の分岐 なし(procedural)
CASE文(Statement) 値でPL/SQL処理を分岐 なし(procedural)
CASE式(Expression) 値を返す(変数代入/SQL内部) あり(式として使える)
IF文
-- 各分岐で複数の処理がある時はIF文が自然
IF v_score &gt;= 90 THEN
  DBMS_OUTPUT.PUT_LINE('優秀');
  UPDATE users SET rank = 1 WHERE id = v_id;
  send_congrats_email(v_id);
ELSIF v_score &gt;= 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;
CASE文(Simple CASE:単一変数の値で分岐)
-- 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;
CASE文(Searched CASE:複雑条件)
-- IFと同じ表現力だが整列した記述が可能
CASE
  WHEN v_score &gt;= 90 AND v_attended THEN
    DBMS_OUTPUT.PUT_LINE('優秀+皆勤');
  WHEN v_score &gt;= 70 THEN
    DBMS_OUTPUT.PUT_LINE('合格');
  ELSE
    DBMS_OUTPUT.PUT_LINE('不合格');
END CASE;
CASE式(値を返す)
-- 変数への代入が1行で完結
DECLARE
  v_rank VARCHAR2(10);
BEGIN
  v_rank := CASE
             WHEN v_score &gt;= 90 THEN 'A'
             WHEN v_score &gt;= 70 THEN 'B'
             WHEN v_score &gt;= 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比較できない
-- ❌ 意図通り動かない(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)を使います。

SQL側の分岐:DECODE vs CASE式
-- 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 &gt; 30;

DECODE vs CASE式の選び方

  • DECODE:Oracle特有、短く書ける、単一値比較に最適、NULL同士もマッチする(これは便利)
  • CASE式:標準SQL(PostgreSQL等に移行可能)、範囲条件OK、可読性高
  • 新規コードはCASE式推奨(他DBとの互換性)
  • DECODEは既存コード保守時やOracle固有の場面でのみ

ネストIF vs ELSIF:可読性とパフォーマンス

ネストIFは読みにくくなりがち
-- ❌ 深いネストは可読性が落ちる
IF v_status = 'active' THEN
  IF v_age &gt;= 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 &gt;= 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 &lt; 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が長いなら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 &gt; 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 &lt; 0 THEN
    RAISE_APPLICATION_ERROR(-20001, '金額はマイナスにできません');
  ELSIF :new.amount &gt; 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 &gt;= TRUNC(SYSDATE) - 7) LOOP
    IF rec.status = 'pending' AND rec.amount &gt; 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 &lt; 18 OR p_age &gt; 120 THEN
    RAISE_APPLICATION_ERROR(-20102, '年齢は18〜120');
  END IF;

  IF LENGTH(p_password) &lt; 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 &gt;= 10000 THEN v_fee := 0;
    ELSIF v_amount &gt;= 5000 THEN v_fee := 300;
    ELSE v_fee := 600;
    END IF;
  ELSE
    -- 海外配送
    v_fee := CASE
               WHEN v_amount &gt;= 20000 THEN 0
               ELSE 2000
             END;
  END IF;

  RETURN v_fee;
END;

⑥Feature Flag(機能切替)

設定テーブルで機能ON/OFF
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件のハンドリング

COUNT→IF で存在確認
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;

⑨バッチ中のリトライ判定

リトライ上限+IF
DECLARE
  v_retry_count NUMBER := 0;
  v_max_retry CONSTANT NUMBER := 3;
  v_success BOOLEAN := FALSE;
BEGIN
  WHILE v_retry_count &lt; 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判定

トリガーでIF分岐
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含む」と誤解しないようNVLIS NULLを併記。

③深すぎるネスト(4階層以上)。ANDで条件統合、early returnパターン、または関数抽出で平坦化。

④同じ変数の多段ELSIFIF 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文のデバッグ術

DBMS_OUTPUTで条件トレース
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 &gt; 3000 THEN
    DBMS_OUTPUT.PUT_LINE('→ pending+高額分岐');
    ...
  ELSE
    DBMS_OUTPUT.PUT_LINE('→ ELSE');
    ...
  END IF;
END;
/
-- 実行前に SET SERVEROUTPUT ON;

条件式を変数に出す(可読性+デバッグしやすい)

BOOLEAN変数化
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 &gt;= 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も書きやすい。複雑な条件分岐はまず変数化してから組み立てるのが熟練者のパターン。

よくある質問

QELSIFは正しい綴り?
A正しいです。PL/SQLではELSIF(スペース無し)。他言語のelseifelse ifelifとは違うので注意。ELSE IFと書くとPLS-00103コンパイルエラー。
QなぜNULLは条件式で特別扱いなのか
AOracle(およびSQL全般)の3値論理によります。NULL = NULLNULL != NULLTRUEでもFALSEでもなくNULLIFはTRUEの時だけTHENに行くので、NULL比較は常にTHENを飛ばします。IS NULLIS NOT NULLNVLCOALESCEで明示的にNULLを扱うのが鉄則。
QIF文とCASE文、どちらを使うべき?
A①複数ステップの処理分岐→IF文、②単一変数の値で分岐→Simple CASE文CASE v WHEN ...)、③複雑条件で処理分岐→Searched CASE文CASE WHEN v >= 10 ...)、④値を返す→CASE式(変数代入、SQL内)。基本はIF、多段比較ならCASEが読みやすい。
QBOOLEAN型カラムをCREATE TABLEできる?
A標準SQLではNG。OracleのテーブルカラムにBOOLEAN型は使えません(23c以降は一部対応)。PL/SQL変数ではBOOLEAN使用可。テーブルに入れる場合はCHAR(1)(’Y’/’N’)やNUMBER(1)(0/1)で代用が一般的。
Q短絡評価は保証されている?
A保証されています。AND は左がFALSEなら右を評価せず、ORは左がTRUEなら右を評価しません。このためIF v_coll IS NOT NULL AND v_coll.COUNT > 0 THENの順でガードが効き、NULL参照例外を回避できます。条件の順序は意味を持つので注意深く書きましょう。
QIF文でEXITやRETURNできる?
AEXITはループ内でのみ有効、RETURNはPROCEDURE/FUNCTION内で有効。IF文自体の脱出はEND IF;で自動的に行われます。早期脱出が必要ならガード句パターン(条件NGならRETURN)を使います。
QDECODEとCASE式、どっちが速い?
A実装面ではほぼ同じ。現代のOracleは両方最適化され体感差はありません。DECODEは短くてOracle独自、CASE式は標準SQLで可読性高く他DBへの移植性もあります。新規はCASE式推奨。
QCASE_NOT_FOUND例外が出た
APL/SQLのCASE文(Statement)でどのWHENにも該当せずELSEも無い時に発生。解決はELSE NULL;ELSE <デフォルト処理>を必ず書くこと。SQLのCASE式はELSE省略時NULLを返す別仕様なので混同しないよう注意。
Q条件分岐が長くなりすぎた
A①ANDで条件統合、②ガード句でearly return、③関数抽出、④同じ変数の多段ELSIFならCASE文に置換、⑤BOOLEAN変数化で状態を名前付きに、⑥ルックアップテーブル化(STATIC データをテーブルに出してJOIN)。深ネスト&長ELSIFは設計見直しのサインです。
QIF文のパフォーマンスは?
AIF文単体はコンパイラ最適化でほぼ無視できるコスト。ボトルネックになるのは条件内で実行されるSQL/function呼び出しです。重い関数を条件に入れるなら短絡評価で前段でガードするか事前に変数キャッシュしてからIFで判定すると効きます。

関連記事

まとめ

  • IF文の3形態:IF THEN/IF THEN ELSE/IF THEN ELSIF ELSE(綴りはELSIF、スペース無し)
  • 3値論理(TRUE/FALSE/NULL)でNULL比較は全てNULL→分岐スキップ
  • NULL対策6種:IS NULLNVLCOALESCEOR .. IS NULLDECODE/BOOLEAN変数明示
  • AND/ORの短絡評価は保証、NULL参照・除算エラー回避に活用
  • 比較演算子:=<>!=BETWEENINLIKEIS NULLREGEXP_LIKE
  • 使い分け:IF文(複数処理)/Simple CASE(単変数)/Searched CASE(複雑)/CASE式(値を返す)
  • SQL内はDECODECASE式、PL/SQLブロック内はIFCASE文
  • 深ネスト回避: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コードを書いてください。