【Oracle】ORA-01403・ORA-01422 完全ガイド|SELECT INTO の NO_DATA_FOUND・TOO_MANY_ROWS 原因と対処・安全なパターンまで解説

【Oracle】ORA-01403・ORA-01422 完全ガイド|SELECT INTO の NO_DATA_FOUND・TOO_MANY_ROWS 原因と対処・安全なパターンまで解説 Oracle

PL/SQL の SELECT INTO 文は、SELECT 結果を変数に代入するための基本構文ですが、取得できる行数が「ちょうど1行」でなければなりません。0行の場合は ORA-01403(NO_DATA_FOUND)、2行以上の場合は ORA-01422(TOO_MANY_ROWS)が発生します。

これらは PL/SQL 開発で最も頻繁に遭遇するランタイムエラーのひとつです。本記事では発生原因・例外処理パターン・安全な代替手法を実務的なコード例で解説します。

この記事でわかること

  • SELECT INTO の仕様と ORA-01403 / ORA-01422 が発生する条件
  • NO_DATA_FOUND / TOO_MANY_ROWS 例外を正しくハンドリングする方法
  • 事前 COUNT チェックが推奨されない理由(TOCTOU 問題)
  • BULK COLLECT INTO で複数行を安全に取得する方法
  • カーソル FOR ループとの使い分け
  • 行数が不定なケースに対応する実務パターン
スポンサーリンク

SELECT INTO の仕様:なぜ 1 行限定なのか

SQL の SELECT は複数行を返しますが、PL/SQL の SELECT ... INTO 変数ちょうど 1 行だけを変数に代入できます。PL/SQL の変数はスカラー値(単一の値)を格納するため、複数行を受け取ることができません。

返却行数 発生する結果 エラーコード
0行 ORA-01403: データが見つかりません NO_DATA_FOUND
1行 正常終了(変数に値が代入される)
2行以上 ORA-01422: 選択がFETCH行より多くのデータを戻しました TOO_MANY_ROWS
SELECT INTO の基本構文と発生ケース
DECLARE
    v_name   employees.last_name%TYPE;
    v_salary employees.salary%TYPE;
BEGIN
    -- 正常ケース: employee_id は主キーなので必ず 1 行
    SELECT last_name, salary
    INTO v_name, v_salary
    FROM employees
    WHERE employee_id = 100;

    -- ORA-01403 ケース: 存在しない ID を指定
    SELECT last_name INTO v_name FROM employees WHERE employee_id = 99999;
    -- → ORA-01403: データが見つかりません

    -- ORA-01422 ケース: WHERE 条件が複数行にマッチ
    SELECT last_name INTO v_name FROM employees WHERE department_id = 10;
    -- → ORA-01422: 選択がFETCH行より多くのデータを戻しました
END;
/

ORA-01403(NO_DATA_FOUND)の原因と対処

WHERE 条件に一致する行が存在しない場合に発生します。主キー・ユニークキー以外の条件で SELECT INTO を使う場合は常にこのリスクがあります。

NG: 例外処理なしの SELECT INTO
-- ORA-01403 が発生するとそのまま例外が伝播してプログラムが中断される
DECLARE
    v_name employees.last_name%TYPE;
BEGIN
    SELECT last_name INTO v_name
    FROM employees
    WHERE employee_id = 99999;  -- 存在しない ID

    DBMS_OUTPUT.PUT_LINE(v_name);  -- ここには到達しない
END;
/
-- ORA-01403: データが見つかりません
-- ORA-06512: "XXX", 行 4
OK: EXCEPTION で NO_DATA_FOUND をハンドリング
DECLARE
    v_name employees.last_name%TYPE;
BEGIN
    SELECT last_name INTO v_name
    FROM employees
    WHERE employee_id = 99999;

    DBMS_OUTPUT.PUT_LINE('社員名: ' || v_name);

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        -- 「見つからなかった」という業務的なハンドリング
        DBMS_OUTPUT.PUT_LINE('該当する社員が見つかりませんでした');
        -- 呼び出し元に伝えたい場合は RAISE_APPLICATION_ERROR で変換する
        -- RAISE_APPLICATION_ERROR(-20001, '社員が見つかりません: ' || 99999);
END;
/
実務パターン:NULL を返す関数(行がないとき NULL)
-- 行がないときに NULL を返すファンクション
-- SELECT INTO + NO_DATA_FOUND のラッパーとして定義
CREATE OR REPLACE FUNCTION get_employee_name(p_emp_id IN NUMBER)
RETURN employees.last_name%TYPE IS
    v_name employees.last_name%TYPE;
BEGIN
    SELECT last_name INTO v_name
    FROM employees
    WHERE employee_id = p_emp_id;
    RETURN v_name;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RETURN NULL;  -- 見つからなければ NULL を返す
END;
/

-- 呼び出し元
DECLARE
    v_name employees.last_name%TYPE;
BEGIN
    v_name := get_employee_name(99999);
    IF v_name IS NULL THEN
        DBMS_OUTPUT.PUT_LINE('社員は存在しません');
    ELSE
        DBMS_OUTPUT.PUT_LINE('社員名: ' || v_name);
    END IF;
END;
/

事前 COUNT チェックが推奨されない理由

「SELECT INTO の前に COUNT で件数確認してから実行する」アプローチがあります。直感的ですが、競合状態(TOCTOU: Time Of Check To Time Of Use)の問題があり非推奨です。

NG: 事前 COUNT チェックパターン(非推奨)
DECLARE
    v_count NUMBER;
    v_name  employees.last_name%TYPE;
BEGIN
    -- STEP 1: 件数確認
    SELECT COUNT(*) INTO v_count
    FROM employees WHERE employee_id = 100;

    -- STEP 2: 存在確認してから SELECT INTO
    IF v_count = 1 THEN
        SELECT last_name INTO v_name
        FROM employees WHERE employee_id = 100;
        -- 問題点: STEP 1 と STEP 2 の間に別セッションがこの行を削除した場合
        -- STEP 2 で ORA-01403 が発生する(COUNT = 1 でも安全ではない)
    END IF;
END;
/
-- さらに: SQL を 2 回実行するため I/O コストが 2 倍になる
OK: EXCEPTION ハンドリングが正しいパターン
-- EXCEPTION を使う方が:
-- 1. SQL が 1 回で済む(パフォーマンス向上)
-- 2. 競合状態が発生しない(原子的な操作)
-- 3. コードがシンプルになる

DECLARE
    v_name employees.last_name%TYPE;
BEGIN
    SELECT last_name INTO v_name
    FROM employees WHERE employee_id = 100;

    DBMS_OUTPUT.PUT_LINE(v_name);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('見つかりませんでした');
END;
/

ORA-01422(TOO_MANY_ROWS)の原因と対処

SELECT INTO に対して2行以上が返ったときに発生します。主キーや UNIQUE 制約のない列を WHERE 条件に使うと発生しやすいです。

ORA-01422 の典型的なシナリオ
DECLARE
    v_name employees.last_name%TYPE;
BEGIN
    -- department_id は UNIQUE ではないため複数行が返る可能性がある
    SELECT last_name INTO v_name
    FROM employees
    WHERE department_id = 50;
    -- → 部門 50 に社員が複数いれば ORA-01422

    -- 集計関数(MAX/MIN)を使えばスカラー値になる
    SELECT MAX(salary) INTO v_name    -- ← これはシングル行になる
    FROM employees WHERE department_id = 50;
END;
/
対処法1:集計関数・ROWNUM で必ず 1 行にする
DECLARE
    v_salary   NUMBER;
    v_max_name employees.last_name%TYPE;
BEGIN
    -- MAX/MIN/AVG/COUNT などを使う
    SELECT MAX(salary) INTO v_salary
    FROM employees WHERE department_id = 50;

    -- ROWNUM = 1 で最初の 1 行だけ取得(ORDER BY と組み合わせる)
    SELECT last_name INTO v_max_name
    FROM (
        SELECT last_name
        FROM employees
        WHERE department_id = 50
        ORDER BY salary DESC
    )
    WHERE ROWNUM = 1;  -- 給与が最も高い 1 人

    -- Oracle 12c 以降: FETCH FIRST 1 ROW ONLY
    SELECT last_name INTO v_max_name
    FROM employees
    WHERE department_id = 50
    ORDER BY salary DESC
    FETCH FIRST 1 ROW ONLY;
END;
/
対処法2:TOO_MANY_ROWS を例外でキャッチ
DECLARE
    v_name employees.last_name%TYPE;
BEGIN
    SELECT last_name INTO v_name
    FROM employees
    WHERE department_id = 50;

EXCEPTION
    WHEN NO_DATA_FOUND THEN
        DBMS_OUTPUT.PUT_LINE('対象データなし');
    WHEN TOO_MANY_ROWS THEN
        -- 「複数件あった」ことを業務ロジックで処理
        DBMS_OUTPUT.PUT_LINE('複数件ヒットしました。処理方法を変更してください');
        RAISE_APPLICATION_ERROR(-20002, '対象データが一意に決まりません');
END;
/

複数行を安全に取得する方法:BULK COLLECT INTO とカーソル FOR ループ

SELECT INTO は「ちょうど 1 行」専用です。複数行を取得したい場合は BULK COLLECT INTO またはカーソル FOR ループを使います。

BULK COLLECT INTO:複数行をコレクションに格納
DECLARE
    -- コレクション型(TABLE OF)を宣言
    TYPE name_list IS TABLE OF employees.last_name%TYPE;
    TYPE sal_list  IS TABLE OF employees.salary%TYPE;

    v_names   name_list;
    v_salaries sal_list;
BEGIN
    -- 複数行を一括取得(ORA-01403/ORA-01422 が発生しない)
    SELECT last_name, salary
    BULK COLLECT INTO v_names, v_salaries
    FROM employees
    WHERE department_id = 50
    ORDER BY salary DESC;

    -- 0 件でもエラーにならない(空のコレクションになる)
    IF v_names.COUNT = 0 THEN
        DBMS_OUTPUT.PUT_LINE('該当データなし');
    ELSE
        DBMS_OUTPUT.PUT_LINE('取得件数: ' || v_names.COUNT);
        FOR i IN 1 .. v_names.COUNT LOOP
            DBMS_OUTPUT.PUT_LINE(v_names(i) || ': ' || v_salaries(i));
        END LOOP;
    END IF;
END;
/
BULK COLLECT LIMIT 句:大量データをバッチ分割して取得
DECLARE
    TYPE emp_list IS TABLE OF employees%ROWTYPE;
    v_emps   emp_list;
    v_offset NUMBER := 0;
    c_batch  NUMBER := 1000;  -- 1バッチ 1000 件

    CURSOR c_emp IS
        SELECT * FROM employees WHERE department_id = 50 ORDER BY employee_id;
BEGIN
    OPEN c_emp;
    LOOP
        -- LIMIT で一度に取得する最大件数を制限(メモリ対策)
        FETCH c_emp BULK COLLECT INTO v_emps LIMIT c_batch;
        EXIT WHEN v_emps.COUNT = 0;  -- 取得できなくなったら終了

        -- バッチ処理
        FOR i IN 1 .. v_emps.COUNT LOOP
            -- v_emps(i).employee_id などで各列にアクセス
            NULL;
        END LOOP;

        EXIT WHEN c_emp%NOTFOUND AND v_emps.COUNT < c_batch;
    END LOOP;
    CLOSE c_emp;
END;
/
カーソル FOR ループ:シンプルで安全な反復処理
-- カーソル FOR ループは最もシンプルで安全な複数行処理パターン
-- OPEN / FETCH / CLOSE を自動管理し、ORA-01403/01422 も発生しない
BEGIN
    FOR rec IN (
        SELECT employee_id, last_name, salary
        FROM employees
        WHERE department_id = 50
        ORDER BY salary DESC
    ) LOOP
        -- rec.employee_id / rec.last_name / rec.salary でアクセス
        DBMS_OUTPUT.PUT_LINE(
            rec.last_name || ': ' || TO_CHAR(rec.salary, '999,999')
        );
    END LOOP;

    -- ループ終了後: 0 件でも正常終了(エラーなし)
    DBMS_OUTPUT.PUT_LINE('処理完了');
END;
/

SELECT INTO・BULK COLLECT・カーソル FOR ループの使い分け

手法 取得行数 向いているケース ORA-01403/01422
SELECT INTO 必ず 1 行 主キー検索・集計関数結果の代入 発生する(例外処理必須)
BULK COLLECT INTO 0 行以上 複数行をまとめてメモリに格納して処理 発生しない
カーソル FOR ループ 0 行以上 行ごとに順次処理(シンプルで安全) 発生しない
明示的カーソル 0 行以上 FETCH/CLOSE を手動制御したい場合 %NOTFOUND で確認
まとめ:SELECT INTO の安全なコーディング規則
-- 規則1: SELECT INTO には必ず EXCEPTION 節で NO_DATA_FOUND / TOO_MANY_ROWS をキャッチする
BEGIN
    SELECT col INTO v_var FROM t WHERE pk_col = v_pk;
EXCEPTION
    WHEN NO_DATA_FOUND  THEN /* 0件の処理 */
    WHEN TOO_MANY_ROWS  THEN /* 複数件の処理 */
END;

-- 規則2: 複数行の可能性がある SELECT は最初から BULK COLLECT またはカーソル FOR を使う
FOR rec IN (SELECT col FROM t WHERE non_unique_col = v_val) LOOP
    -- 行ごとの処理
END LOOP;

-- 規則3: 変数宣言は %TYPE / %ROWTYPE を使って型の不一致を防ぐ(ORA-06502 も予防)
-- NG:  v_name VARCHAR2(30);
-- OK:  v_name employees.last_name%TYPE;

-- 規則4: SELECT INTO のデフォルト値が必要なら関数でラップして NULL を返す
v_name := NVL(get_employee_name(p_id), '不明');

まとめ

ORA-01403(NO_DATA_FOUND)と ORA-01422(TOO_MANY_ROWS)は、PL/SQL の SELECT INTO が「ちょうど 1 行」返す保証がないときに発生します。

解決策の選択基準は次の通りです。

  • SELECT INTO を使う場合:EXCEPTION 節で必ず NO_DATA_FOUND と TOO_MANY_ROWS をハンドリングする
  • 複数行の可能性がある場合:最初から BULK COLLECT INTO またはカーソル FOR ループを使う
  • 「1件だけ取りたい」場合:ORDER BY + FETCH FIRST 1 ROW ONLY(Oracle 12c 以降)や ROWNUM = 1 で確実に 1 行にする

PL/SQL の例外処理については、ストアドプロシージャ・ファンクションの例外処理セクションと、ORA-06502(PL/SQL 数値または値のエラー)の記事も合わせて参照してください。