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 |
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 を使う場合は常にこのリスクがあります。
-- 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
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 を返すファンクション
-- 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)の問題があり非推奨です。
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 倍になる
-- 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 条件に使うと発生しやすいです。
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;
/
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;
/
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 ループを使います。
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;
/
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 ループは最もシンプルで安全な複数行処理パターン
-- 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 で確認 |
-- 規則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 数値または値のエラー)の記事も合わせて参照してください。

