PL/SQLの変数・定数は「DECLAREで宣言して:=で代入」という基本だけなら10分で覚えられますが、実務で差がつくのはデータ型の選択です。数値をNUMBERで宣言するかPLS_INTEGERで宣言するかでループ処理が3〜10倍速くなり、VARCHAR2(4000)を乱用するとPGAメモリを無駄に消費します。
さらに%TYPE/%ROWTYPEでテーブル変更に自動追従する書き方、CONSTANTによる定数化、NOT NULL/DEFAULT制約、複合型(RECORD/コレクション/SUBTYPE)、変数スコープ(ローカル/パッケージ/グローバル)、命名規則(v_/p_/g_/c_)まで押さえることで、保守性と性能の両方が向上します。
この記事では、PL/SQL変数・定数を基礎から運用レベルまで徹底解説します。データ型体系(スカラー/複合/参照/LOB)、NUMBER vs PLS_INTEGER vs BINARY_DOUBLEの性能比較、%TYPE/%ROWTYPE/SUBTYPE活用、定数とNOT NULL設計、パッケージ変数でセッション保持、実務10パターン、アンチパターン7選まで網羅した2026年版の決定版。関連は【PL/SQL】IF文完全ガイド/【PL/SQL】例外処理完全ガイド/【PL/SQL】ループ処理完全ガイド/【PL/SQL】カーソル完全ガイドも併読推奨。
この記事で学べること
- PL/SQLデータ型の全体系マップ(スカラー/複合/参照/LOB)
- 数値型4種(
NUMBER/PLS_INTEGER/BINARY_INTEGER/BINARY_DOUBLE)の性能比較 - 文字列型(
VARCHAR2/CHAR/CLOB/NVARCHAR2)の使い分け - 日付型(
DATE/TIMESTAMP/INTERVAL)の違い BOOLEAN型(PL/SQL限定、テーブルには使えない)%TYPE/%ROWTYPEでテーブル変更に自動追従NOT NULL制約・DEFAULT初期値で未初期化NULL事故を防ぐCONSTANT定数とマジックナンバー排除- 複合型:
RECORD/TABLE/VARRAY/連想配列 SUBTYPEで独自型を定義して可読性UP- 変数スコープ:ブロック/サブプログラム/パッケージ変数でセッション永続
- 命名規則(
v_/p_/g_/c_/l_) - 実務10パターン+アンチパターン7選
- 30秒クイックリファレンス:変数・定数の基本テンプレ
- PL/SQLデータ型の全体系マップ
- 数値型4種の性能と使い分け
- 文字列型の使い分け
- 日付・時刻型:DATE/TIMESTAMP/INTERVAL
- BOOLEAN型:PL/SQL限定の落とし穴
- LOB型:大容量テキスト/バイナリ
- %TYPE/%ROWTYPE でテーブル変更に自動追従
- NOT NULL制約・DEFAULT初期値
- 定数(CONSTANT)でマジックナンバーを排除
- 複合型:RECORD/コレクション/SUBTYPE
- 変数スコープ:ブロック/サブプログラム/パッケージ
- 命名規則:v_/p_/g_/c_/l_
- 型変換:暗黙 vs 明示
- 実務パターン10選
- アンチパターン7選
- よくある質問
- 関連記事
- まとめ
30秒クイックリファレンス:変数・定数の基本テンプレ
DECLARE -- ① 基本宣言 v_name VARCHAR2(100); v_age NUMBER; v_flag BOOLEAN; -- ② DEFAULT初期値(未初期化NULL回避) v_count NUMBER DEFAULT 0; v_status VARCHAR2(20) := 'pending'; -- := でも DEFAULT でも可 -- ③ NOT NULL制約(初期値必須) v_id NUMBER NOT NULL := 0; -- ④ 定数(CONSTANT) c_tax_rate CONSTANT NUMBER := 0.10; -- ⑤ テーブル列の型を継承(%TYPE) v_email users.email%TYPE; -- ⑥ テーブル行全体の型を継承(%ROWTYPE) v_user users%ROWTYPE; BEGIN -- := で代入 v_name := 'Taro'; -- SELECT INTOで取得 SELECT email INTO v_email FROM users WHERE id = 1; SELECT * INTO v_user FROM users WHERE id = 1; DBMS_OUTPUT.PUT_LINE(v_user.email); END;
変数宣言の3つの黄金律:①テーブル列を受ける変数は必ず%TYPE(将来のALTER TABLEに自動追従)、②整数計算はPLS_INTEGER(NUMBERより3〜10倍速)、③不変値はCONSTANTで宣言(マジックナンバー排除)。
PL/SQLデータ型の全体系マップ
PL/SQLデータ型
├── スカラー型(単一値)
│ ├── 数値型:NUMBER / PLS_INTEGER / BINARY_INTEGER /
│ │ BINARY_FLOAT / BINARY_DOUBLE / SIMPLE_INTEGER
│ ├── 文字列型:VARCHAR2 / CHAR / NVARCHAR2 / NCHAR / LONG
│ ├── 日付型:DATE / TIMESTAMP / INTERVAL
│ ├── 真偽型:BOOLEAN(PL/SQL専用、テーブル不可)
│ └── RAW / LONG RAW / ROWID / UROWID
├── 複合型
│ ├── RECORD(構造体)
│ ├── TABLE(ネスト表・連想配列)
│ └── VARRAY(可変長配列)
├── 参照型
│ ├── REF CURSOR(結果セットハンドル)
│ └── REF オブジェクト型
└── LOB型(大容量)
├── CLOB(最大128TB文字)
├── NCLOB
├── BLOB(最大128TBバイナリ)
└── BFILE(外部ファイル参照)
選択の基本方針
- テーブル列と対応する変数→
%TYPEで列の型をそのまま継承 - 整数計算・ループカウンタ→
PLS_INTEGER(高速) - 金額・会計→
NUMBER(p, s)(精度保証) - 科学計算・浮動小数点→
BINARY_DOUBLE(IEEE 754) - 文字列→原則
VARCHAR2(CHARは使わない) - 大容量テキスト/バイナリ→
CLOB/BLOB
数値型4種の性能と使い分け
-- ❌ NUMBERでループカウンタ(遅い)
DECLARE
v_sum NUMBER := 0;
BEGIN
FOR i IN 1..10000000 LOOP
v_sum := v_sum + 1;
END LOOP;
END;
-- 約 3〜5秒
-- ⭕ PLS_INTEGERでループカウンタ(速い)
DECLARE
v_sum PLS_INTEGER := 0;
BEGIN
FOR i IN 1..10000000 LOOP
v_sum := v_sum + 1;
END LOOP;
END;
-- 約 0.3〜0.5秒(10倍速い)
NUMBER(p, s) の意味
DECLARE v_price NUMBER(8, 2); -- 総桁8、小数2(例: 999999.99) v_rate NUMBER(5, 4); -- 総桁5、小数4(例: 9.9999) v_qty NUMBER(10); -- 総桁10、整数のみ v_pct NUMBER(3, 0); -- 3桁整数(0〜999) BEGIN v_price := 123.456; -- → 123.46(小数2桁に丸め) v_price := 99999999; -- ORA-06502 桁あふれ END;
PLS_INTEGER の上限を超えると例外(VALUE_ERRORまたはNUMERIC_OVERFLOW)。21億を超える可能性があるカウンタはNUMBERまたはLONG(64bit整数が必要ならNUMBER(19))を使う。またSIMPLE_INTEGERはオーバーフロー時にラップアラウンド(最大値→最小値)する仕様で例外が出ないため、確実に範囲内と分かっている場合のみ使用してください。
文字列型の使い分け
-- ❌ CHAR(10) で格納 → 末尾パディング
INSERT INTO t(code) VALUES ('ABC'); -- 実体は 'ABC '(7空白)
SELECT * FROM t WHERE code = 'ABC'; -- マッチしない! → 末尾空白で比較失敗
-- CHARは空白比較セマンティクスが特殊で事故を招きやすい
-- 末尾空白を切って比較するには TRIM(code) = 'ABC' が必要
-- ⭕ VARCHAR2(10) は格納された長さそのまま
INSERT INTO t(code) VALUES ('ABC'); -- 実体は 'ABC'(3文字)
SELECT * FROM t WHERE code = 'ABC'; -- マッチする
DECLARE v1 VARCHAR2(10); -- デフォルト=NLS_LENGTH_SEMANTICS(通常BYTE) v2 VARCHAR2(10 BYTE); -- 10バイトまで v3 VARCHAR2(10 CHAR); -- 10文字まで(マルチバイト対応) BEGIN v1 := 'あいうえお'; -- UTF-8で15バイト → ORA-06502 エラー v3 := 'あいうえお'; -- 10文字内OK END;
日本語/絵文字を扱うなら CHAR 単位
日本語(UTF-8で1文字3バイト)や絵文字(4バイト)を格納するならVARCHAR2(100 CHAR)のようにCHAR単位指定が安全。BYTE単位のままだと「100文字確保したつもりが33文字しか入らない」事故が起きます。データベース全体の既定はNLS_LENGTH_SEMANTICSパラメータで設定可能ですが、PL/SQL変数では明示CHAR指定が読みやすく事故を防ぐ。
日付・時刻型:DATE/TIMESTAMP/INTERVAL
DECLARE
v_date DATE; -- 秒まで(ミリ秒なし)
v_ts TIMESTAMP; -- 小数秒対応(最大9桁)
v_ts_tz TIMESTAMP WITH TIME ZONE; -- タイムゾーン付き
v_ts_ltz TIMESTAMP WITH LOCAL TIME ZONE;-- セッションTZに自動変換
v_iym INTERVAL YEAR TO MONTH; -- 期間(年月)
v_ids INTERVAL DAY TO SECOND; -- 期間(日時秒)
BEGIN
v_date := SYSDATE; -- 例: 2026-04-23 14:30:00
v_ts := SYSTIMESTAMP; -- 例: 2026-04-23 14:30:00.123456789 +09:00
-- 文字列→DATE変換
v_date := TO_DATE('2026-04-23', 'YYYY-MM-DD');
-- 日数加算
v_date := v_date + 1; -- 翌日
v_date := v_date + 1/24; -- 1時間後
-- INTERVAL演算
v_date := v_date + INTERVAL '3' DAY;
v_ts := v_ts + INTERVAL '1 12:00:00' DAY TO SECOND;
END;
TIMESTAMP vs DATE 選び方:①ミリ秒精度が必要→TIMESTAMP、②タイムゾーン保存→TIMESTAMP WITH TIME ZONE、③セッションTZ変換→TIMESTAMP WITH LOCAL TIME ZONE、④単純な日付→DATE。実務で迷ったら精度向上のみが必要なら TIMESTAMP、ストレージ最小化とシンプル比較なら DATE。
BOOLEAN型:PL/SQL限定の落とし穴
DECLARE
v_is_active BOOLEAN;
v_flag BOOLEAN := FALSE; -- DEFAULT初期値
v_verified BOOLEAN := TRUE;
BEGIN
-- 比較はTRUE/FALSE/NULLの3値論理
IF v_flag THEN
DBMS_OUTPUT.PUT_LINE('flag TRUE');
END IF;
IF v_is_active IS NULL THEN
DBMS_OUTPUT.PUT_LINE('未初期化');
END IF;
-- 組み合わせ
IF v_flag AND NOT v_verified THEN
DBMS_OUTPUT.PUT_LINE('パターンA');
END IF;
END;
BOOLEAN型はテーブル列に使えない(Oracle 22c未満):CREATE TABLE t (flag BOOLEAN);はPLS-00553 / ORA-00902エラー。テーブルにはCHAR(1)(’Y’/’N’)またはNUMBER(1)(1/0)で代用。SQL文にBOOLEAN変数を直接バインドする場合もエラーのため、SELECT ... INTOの前後で型変換が必要。※Oracle 23c以降はSQLレベルでもBOOLEAN対応(ただしまだ普及途上)。
DECLARE v_bool BOOLEAN; v_char CHAR(1); BEGIN -- BOOLEAN → CHAR v_char := CASE WHEN v_bool THEN 'Y' ELSE 'N' END; -- CHAR → BOOLEAN v_bool := (v_char = 'Y'); -- BOOLEAN値をSQL内で直接使うとエラー -- INSERT INTO t(flag) VALUES (v_bool); -- PLS-00306 INSERT INTO t(flag) VALUES (CASE WHEN v_bool THEN 'Y' ELSE 'N' END); -- OK END;
LOB型:大容量テキスト/バイナリ
DECLARE
v_article CLOB; -- 最大128TB文字(記事本文・JSON)
v_image BLOB; -- 最大128TBバイナリ(画像・PDF)
v_nclob NCLOB; -- Unicode CLOB
v_bfile BFILE; -- 外部ファイル参照(DBに保存しない)
BEGIN
-- CLOB初期化(空文字と NULL は別物)
v_article := EMPTY_CLOB();
-- 追記
DBMS_LOB.APPEND(v_article, 'これは記事本文の先頭');
DBMS_LOB.APPEND(v_article, CHR(10) || '続き...');
-- 長さ取得
DBMS_OUTPUT.PUT_LINE('length=' || DBMS_LOB.GETLENGTH(v_article));
-- 部分読み出し
DBMS_OUTPUT.PUT_LINE(DBMS_LOB.SUBSTR(v_article, 100, 1));
END;
CLOB操作の3大API
DBMS_LOB.GETLENGTH:長さ取得DBMS_LOB.SUBSTR:部分取得DBMS_LOB.APPEND/WRITEAPPEND:追記- 詳細は【PL/SQL】予約語一覧とあわせてOracle公式DBMS_LOBリファレンスを参照
%TYPE/%ROWTYPE でテーブル変更に自動追従
%TYPE:単一列の型を継承
-- ❌ 明示型宣言(テーブル変更時に全変数を書き換え必要) DECLARE v_email VARCHAR2(100); v_name VARCHAR2(50); BEGIN SELECT email, name INTO v_email, v_name FROM users WHERE id = 1; END; -- ↑ usersテーブルのemail/name列の型が変わったら全箇所修正 -- ⭕ %TYPEで継承(テーブル変更に自動追従) DECLARE v_email users.email%TYPE; v_name users.name%TYPE; BEGIN SELECT email, name INTO v_email, v_name FROM users WHERE id = 1; END; -- ↑ usersテーブルのALTER TABLEで長さ変更→変数定義は無修正でOK
%ROWTYPE:テーブル/カーソルの全列を継承
-- テーブル%ROWTYPE DECLARE v_user users%ROWTYPE; BEGIN SELECT * INTO v_user FROM users WHERE id = 1; DBMS_OUTPUT.PUT_LINE(v_user.id || ' ' || v_user.email); END; -- カーソル%ROWTYPE(SELECTした列だけ軽量) DECLARE CURSOR cur IS SELECT id, email FROM users; v_row cur%ROWTYPE; BEGIN OPEN cur; FETCH cur INTO v_row; DBMS_OUTPUT.PUT_LINE(v_row.id || ' / ' || v_row.email); CLOSE cur; END;
%TYPE/%ROWTYPEは保守性の革命:テーブルのALTER TABLE MODIFY email VARCHAR2(200)を実行してもPL/SQLコードを1行も書き直す必要がありません。大規模なスキーマ変更でPL/SQL側の修正範囲がゼロになるメリットは計り知れません。テーブルと対応する変数は原則%TYPEを使うのがプロの習慣。詳しくは【PL/SQL】カーソル完全ガイドの%ROWTYPEセクションも参照。
NOT NULL制約・DEFAULT初期値
DECLARE -- ❌ NULL初期化(意図せずNULLで演算してバグる) v_count NUMBER; -- ⭕ DEFAULT で明示初期化 v_sum NUMBER DEFAULT 0; v_name VARCHAR2(100) := '(unknown)'; -- := でも DEFAULT でも同義 -- ⭕ NOT NULL:NULL代入を禁止(初期値必須) v_id NUMBER NOT NULL := 0; v_flag BOOLEAN NOT NULL DEFAULT FALSE; -- NOT NULL宣言なのに初期値無しはエラー -- v_bad NUMBER NOT NULL; -- PLS-00218 BEGIN -- NOT NULL変数にNULL代入は実行時エラー(VALUE_ERROR) -- v_id := NULL; -- NULL同士の演算は結果もNULL v_count := v_count + 1; -- NULL + 1 = NULL(v_countは永遠にNULL) END;
NULLで演算すると結果もNULL:v_count NUMBER;のままでv_count := v_count + 1;しても永遠に NULL のまま(NULL + 1 = NULL)。初期化忘れは実務で最頻出の「静かなバグ」で、後々集計が全部NULLで返ってきて発覚することが多い。数値カウンタはDEFAULT 0/文字列はDEFAULT ''またはNULLを意識的に宣言。
定数(CONSTANT)でマジックナンバーを排除
-- ❌ マジックナンバーが散らばる DECLARE v_tax NUMBER; BEGIN v_tax := v_price * 0.10; -- 0.10 ってなんだっけ? UPDATE orders SET tax = price * 0.10 WHERE id = v_id; -- 税率変更時に全コピペ箇所を修正する必要 END; -- ⭕ CONSTANTで一元管理 DECLARE c_tax_rate CONSTANT NUMBER := 0.10; c_max_retry CONSTANT PLS_INTEGER := 3; c_session_ttl CONSTANT NUMBER := 3600; -- 秒 c_error_code CONSTANT NUMBER := -20001; v_tax NUMBER; BEGIN v_tax := v_price * c_tax_rate; -- 税率変更時は c_tax_rate の定義だけ変えればOK END;
パッケージ定数でプロジェクト全体共有
CREATE OR REPLACE PACKAGE pkg_constants IS
-- ビジネスロジック
c_tax_rate CONSTANT NUMBER := 0.10;
c_shipping_free CONSTANT NUMBER := 10000;
-- 技術パラメータ
c_batch_size CONSTANT PLS_INTEGER := 1000;
c_max_retry CONSTANT PLS_INTEGER := 3;
c_retry_delay CONSTANT PLS_INTEGER := 2; -- 秒
-- エラーコード
c_err_not_found CONSTANT NUMBER := -20001;
c_err_invalid CONSTANT NUMBER := -20002;
-- 文字列定数
c_status_active CONSTANT VARCHAR2(20) := 'active';
c_status_deleted CONSTANT VARCHAR2(20) := 'deleted';
END;
/
-- 利用側
BEGIN
IF v_retry_count < pkg_constants.c_max_retry THEN
process_retry();
END IF;
END;
CONSTANTのベストプラクティス:①プレフィックスc_で一目で定数と分かる、②ビジネスルール値(税率・料金・閾値)はハードコーディング禁止、③プロジェクト全体の定数はパッケージに集約、④エラーコード(-20000〜-20999)も定数化すると例外処理完全ガイドと組み合わせやすい。
複合型:RECORD/コレクション/SUBTYPE
RECORD(構造体)
DECLARE
-- カスタムレコード型を定義
TYPE t_user_summary IS RECORD (
id NUMBER,
email VARCHAR2(100),
order_count NUMBER DEFAULT 0,
total_amt NUMBER DEFAULT 0
);
v_user t_user_summary;
BEGIN
v_user.id := 1;
v_user.email := 'test@example.com';
v_user.order_count := 10;
v_user.total_amt := 50000;
DBMS_OUTPUT.PUT_LINE(v_user.email || ' / ' || v_user.order_count);
END;
コレクション3種
DECLARE
-- ① ネスト表(大きさ可変)
TYPE t_ids IS TABLE OF NUMBER;
v_ids t_ids := t_ids(1, 2, 3);
-- ② VARRAY(固定上限の可変長配列)
TYPE t_scores IS VARRAY(10) OF NUMBER;
v_scores t_scores := t_scores();
-- ③ 連想配列(インデックス任意・最頻出)
TYPE t_user_map IS TABLE OF users%ROWTYPE INDEX BY PLS_INTEGER;
v_users t_user_map;
TYPE t_attr_map IS TABLE OF VARCHAR2(100) INDEX BY VARCHAR2(50);
v_attrs t_attr_map;
BEGIN
-- 操作
v_ids.EXTEND; v_ids(v_ids.COUNT) := 4;
v_attrs('color') := 'red';
v_attrs('size') := 'M';
DBMS_OUTPUT.PUT_LINE(v_ids(1));
DBMS_OUTPUT.PUT_LINE(v_attrs('color'));
END;
SUBTYPE で独自型を定義
DECLARE -- 基本型にエイリアス SUBTYPE user_id_t IS NUMBER(10); SUBTYPE email_t IS VARCHAR2(255); SUBTYPE percentage_t IS NUMBER(5, 2) NOT NULL; SUBTYPE positive_int_t IS PLS_INTEGER RANGE 1..2147483647; v_uid user_id_t := 100; v_email email_t := 'user@example.com'; v_tax percentage_t := 10.00; v_count positive_int_t := 1; BEGIN -- v_count := -1; -- RANGE制約で実行時エラー -- v_tax := NULL; -- NOT NULL制約で実行時エラー NULL; END;
SUBTYPEで型にドメインの意味を持たせる
SUBTYPE user_id_t IS NUMBER(10);と宣言すれば引数や変数の型をuser_id_tと書けて、読み手に「これはユーザーID」と伝わります。単なるNUMBERより意図が明確でプロジェクト共通語彙になります。コレクションとの組み合わせは【PL/SQL】コレクション(配列・ネスト表)の基本と活用例参照。
変数スコープ:ブロック/サブプログラム/パッケージ
ブロックスコープ(ネストブロック)
DECLARE
v_outer NUMBER := 10;
BEGIN
DECLARE
v_inner NUMBER := 20;
BEGIN
-- 内側ブロックからは外側が見える
DBMS_OUTPUT.PUT_LINE(v_outer); -- 10
DBMS_OUTPUT.PUT_LINE(v_inner); -- 20
END;
-- 外側からは内側は見えない
-- DBMS_OUTPUT.PUT_LINE(v_inner); -- PLS-00201
END;
パッケージ変数でセッション永続
-- パッケージ仕様
CREATE OR REPLACE PACKAGE pkg_session IS
g_user_id NUMBER;
g_session_id VARCHAR2(100);
g_debug BOOLEAN := FALSE;
PROCEDURE set_user(p_id NUMBER);
FUNCTION get_user RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY pkg_session IS
PROCEDURE set_user(p_id NUMBER) IS
BEGIN
g_user_id := p_id;
END;
FUNCTION get_user RETURN NUMBER IS
BEGIN
RETURN g_user_id;
END;
END;
/
-- 使用(同一セッション内で値が持続)
BEGIN
pkg_session.set_user(100);
END;
/
BEGIN
DBMS_OUTPUT.PUT_LINE(pkg_session.get_user); -- 100
END;
パッケージ変数の特徴
- セッション毎に独立した値を保持(他セッションと共有しない)
- ログイン〜ログアウトまで値が持続
- プロシージャ間で値を受け渡せる(引数不要)
- ユーザーコンテキスト・キャッシュ・設定値の保持に便利
PRAGMA SERIALLY_REUSABLEで呼出しごとにリセットすることも可能- 詳細は【PL/SQL】パッケージを使ったコード管理と再利用性向上
命名規則:v_/p_/g_/c_/l_
命名規則の効果:①コード中の識別子がパラメータ(p_)か変数(v_)か定数(c_)か一目で分かる、②テーブル列名と変数名の衝突を防ぐ(email列とv_email変数)、③WHERE id = p_idのような書き方で意図が明確。最低でもv_/p_/c_/e_の4つはプロジェクト共通規約にしておくと保守性が劇的向上。
型変換:暗黙 vs 明示
DECLARE
v_num NUMBER;
v_str VARCHAR2(50);
v_dt DATE;
BEGIN
-- 文字列 → 数値
v_num := TO_NUMBER('1234.56');
v_num := TO_NUMBER('1,234.56', '9,999.99');
-- 数値 → 文字列
v_str := TO_CHAR(1234.56);
v_str := TO_CHAR(1234.56, 'FM9,999.99');
-- 日付変換(必ずフォーマット指定)
v_dt := TO_DATE('2026-04-23', 'YYYY-MM-DD');
v_str := TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS');
END;
DECLARE v_num NUMBER; v_str VARCHAR2(20); BEGIN -- 暗黙変換(動くが実行時エラーリスク) v_num := '123'; -- OK(暗黙のTO_NUMBER) v_num := 'abc'; -- 実行時エラー(INVALID_NUMBER) -- セッションNLS_NUMERIC_CHARACTERSに依存するトラブル v_num := '1,234'; -- セッション設定によって成功/失敗が変わる -- 日付の暗黙変換は特に危険 v_str := SYSDATE; -- セッションNLS_DATE_FORMAT依存 -- 環境が変わると出力フォーマットが変わって動作変更 END;
暗黙変換を使わない理由:①NLS設定(言語・地域)に依存して環境で挙動が変わる、②エラーが実行時まで分からない(型不一致)、③インデックスが使えなくなる(WHERE numeric_col = '100'等)、④読み手に意図が伝わらない。変換は必ずTO_NUMBER/TO_CHAR/TO_DATEでフォーマット明示が鉄則。
実務パターン10選
①%ROWTYPE+デフォルト値
DECLARE v_user users%ROWTYPE; BEGIN v_user.id := NULL; -- INSERTでシーケンス使う v_user.email := p_email; v_user.created_at := SYSDATE; v_user.status := pkg_constants.c_status_active; INSERT INTO users VALUES v_user; END;
②定数カタログで税率・閾値管理
CREATE OR REPLACE PACKAGE pkg_config IS c_tax_rate CONSTANT NUMBER := 0.10; c_shipping_free CONSTANT NUMBER := 10000; c_premium_discount CONSTANT NUMBER := 0.15; END; -- 呼出し側 v_tax := v_subtotal * pkg_config.c_tax_rate;
③PLS_INTEGERで高速ループ
DECLARE
v_count PLS_INTEGER := 0;
BEGIN
FOR i IN 1..10000000 LOOP
v_count := v_count + 1;
END LOOP;
-- NUMBERの10倍速
END;
④NOT NULL+DEFAULTで初期化徹底
DECLARE v_total_count NUMBER NOT NULL := 0; v_error_count NUMBER NOT NULL := 0; v_processed BOOLEAN NOT NULL DEFAULT FALSE; BEGIN -- NULLによる演算バグを物理的に防ぐ v_total_count := v_total_count + 1; v_processed := TRUE; END;
⑤RECORDで構造化引数
DECLARE
TYPE t_order_input IS RECORD (
user_id NUMBER,
items VARCHAR2(4000),
amount NUMBER,
notes VARCHAR2(1000)
);
v_input t_order_input;
BEGIN
v_input.user_id := 100;
v_input.items := '[{"id":1},{"id":2}]';
v_input.amount := 5000;
create_order(v_input);
END;
⑥SUBTYPEでドメイン型
DECLARE SUBTYPE amount_t IS NUMBER(12, 2) NOT NULL; SUBTYPE jpy_yen_t IS NUMBER(12, 0) NOT NULL; -- 円は小数なし v_price amount_t := 1500.00; v_yen_qty jpy_yen_t := 1500; BEGIN NULL; END;
⑦連想配列でキャッシュ
DECLARE
TYPE t_cache IS TABLE OF VARCHAR2(100) INDEX BY VARCHAR2(50);
v_cache t_cache;
FUNCTION lookup(p_key VARCHAR2) RETURN VARCHAR2 IS
BEGIN
IF v_cache.EXISTS(p_key) THEN
RETURN v_cache(p_key);
END IF;
-- キャッシュミス → DBから取得
SELECT value INTO v_cache(p_key)
FROM config WHERE key = p_key;
RETURN v_cache(p_key);
END;
BEGIN
DBMS_OUTPUT.PUT_LINE(lookup('site_name'));
END;
⑧パッケージ変数でユーザーコンテキスト保持
CREATE OR REPLACE PACKAGE pkg_ctx IS g_current_user NUMBER; g_audit_source VARCHAR2(100); END; -- ログイン時にセット BEGIN pkg_ctx.g_current_user := 100; pkg_ctx.g_audit_source := 'web_app'; END; -- 以降のすべての処理から参照可能 BEGIN INSERT INTO audit_log(action_by, source) VALUES (pkg_ctx.g_current_user, pkg_ctx.g_audit_source); END;
⑨明示的型変換で事故防止
DECLARE
v_date_str VARCHAR2(20) := '2026-04-23';
v_date DATE;
v_amount NUMBER;
v_input VARCHAR2(20) := '1234.56';
BEGIN
v_date := TO_DATE(v_date_str, 'YYYY-MM-DD');
v_amount := TO_NUMBER(v_input);
EXCEPTION
WHEN INVALID_NUMBER OR VALUE_ERROR THEN
RAISE_APPLICATION_ERROR(-20001, '数値変換エラー: ' || v_input);
END;
⑩BOOLEAN→CHAR変換(DB永続化)
DECLARE v_verified BOOLEAN := TRUE; v_flag CHAR(1); BEGIN v_flag := CASE WHEN v_verified THEN 'Y' ELSE 'N' END; INSERT INTO users(id, verified_flag) VALUES (p_id, v_flag); END;
アンチパターン7選
①テーブル列に対応する変数を明示型で宣言。v_email VARCHAR2(100)でなくv_email users.email%TYPEにすべき。ALTER TABLEで列長変更した時、PL/SQL側の修正が大量発生。
②ループカウンタをNUMBERで宣言。PLS_INTEGERに変えると10倍速。大量データループで顕著な性能差。
③未初期化(NULL)の数値カウンタに加算。v_count NUMBER;のままでv_count := v_count + 1;しても永遠にNULL。必ずDEFAULT 0またはNOT NULL DEFAULT 0で初期化。
④マジックナンバーをコードに直書き。v_tax := v_price * 0.10;のような0.10はCONSTANT化。税率変更で全箇所修正の惨事を防ぐ。
⑤CHAR型を使う。末尾空白パディングで=比較が失敗する事故多発。99%のケースはVARCHAR2で事足りる。
⑥暗黙型変換に頼る。v_num := '1,234';のようなNLS依存はリージョン/環境で動作が変わる。必ずTO_NUMBER('1,234', '9,999')で明示。
⑦パッケージ変数の濫用。セッションライフタイムのグローバル変数は便利だが、並行処理では予期せぬ副作用を生む。値の変更が追跡困難になるので、明確な用途(ユーザーコンテキスト・キャッシュ)以外は引数で渡す方がよい。
よくある質問
:=とDEFAULTは何が違う?DEFAULTの方が宣言時の意図(初期値設定)が明確で可読性が高い。実行コードの代入は:=のみ使えます。慣例としてはDECLARE内の初期値はDEFAULT、BEGIN内の代入は:=と使い分けるのが読みやすい。VARCHAR2(10)とVARCHAR2(10 CHAR)の違いVARCHAR2(10)はデフォルトのNLS_LENGTH_SEMANTICS(通常BYTE)に従い、UTF-8で日本語を10文字入れたいと思うと30バイト必要で格納失敗。VARCHAR2(10 CHAR)なら明確に10文字格納できます。日本語・絵文字を扱うならCHAR単位指定を推奨。PLS_INTEGERとBINARY_INTEGERの違いBINARY_INTEGERはPLS_INTEGERのエイリアス)。どちらを使ってもOKですが、新規コードはPLS_INTEGER推奨。両者ともNUMBERより3〜10倍速いので、整数カウンタ・ループ変数には積極的に使いましょう。CASE WHEN b THEN 'Y' ELSE 'N' ENDで変換します。users.email%TYPE)、%ROWTYPEはテーブル/カーソルの全列構造体(例:users%ROWTYPE→v.id、v.emailでアクセス)。1列だけなら%TYPE、複数列まとめて扱うなら%ROWTYPE。VALUE_ERROR(ORA-06502)例外が発生します。例:v_name VARCHAR2(10); v_name := '1234567890123';はエラー。対策は①サイズを大きく取る、②SUBSTR(val, 1, 10)で切り詰める、③%TYPEでテーブル列と同じサイズ継承、④長文はCLOBにする、のいずれか。SELECT * FROM users WHERE id = idは列vs列の比較で全行マッチ!対策:①変数にプレフィックス(p_id/v_id)、②テーブル別名(users u)で列をu.idと限定、③BEGIN …<<block_name>>ラベルでスコープ明示。g_user := 100;してもセッションBのg_userは別(NULLまたは既定値)。サーバー全体で共有したい場合はテーブルやSYS_CONTEXTを使います。CONSTANT宣言は頭か末尾どちら?変数名 CONSTANT 型 := 値;(型の前)。例:c_max CONSTANT PLS_INTEGER := 100;。他言語のconst int c_max = 100;のような順番とは違うので注意。SUBTYPE positive_int_t IS PLS_INTEGER RANGE 1..100;で宣言した変数に範囲外の値を代入するとVALUE_ERROR例外。型で制約を定義することで「バグを型で防ぐ」設計が可能。NOT NULL制約も同様に効きます。関連記事
- 【PL/SQL】IF文完全ガイド — BOOLEAN変数とIF条件
- 【PL/SQL】例外処理完全ガイド — VALUE_ERROR/INVALID_NUMBER処理
- 【PL/SQL】ループ処理完全ガイド — PLS_INTEGERカウンタで高速化
- 【PL/SQL】カーソル完全ガイド — %ROWTYPEレコード変数
- 【PL/SQL】予約語一覧と使い方 — 変数名に使えないキーワード
- 【PL/SQL】初心者でもわかる基本構文とブロック構造の書き方 — DECLAREセクションの位置
- 【PL/SQL】ストアドプロシージャとファンクションの違いと作り方 — パラメータ宣言
- 【PL/SQL】IN・OUT・IN OUTパラメータの完全ガイド — パラメータ型とNOCOPY
- 【PL/SQL】パッケージを使ったコード管理と再利用性向上 — パッケージ変数と定数
- 【PL/SQL】コレクション(配列・ネスト表)の基本と活用例 — 複合型の詳細
まとめ
- PL/SQLデータ型の4大分類:スカラー/複合/参照/LOB
- 整数演算は
PLS_INTEGERでNUMBERより3〜10倍高速 - 金額・会計は
NUMBER(p, s)(精度保証)、科学計算はBINARY_DOUBLE - 文字列は
VARCHAR2原則、日本語は(n CHAR)で指定 CHARは末尾空白パディングで事故多発、使わない- BOOLEAN型はPL/SQL専用(22c以前はテーブル不可、
CHAR(1)で代用) %TYPE/%ROWTYPEでテーブル変更に自動追従NOT NULL+DEFAULTで未初期化NULL演算バグを物理防止CONSTANTでマジックナンバー排除、パッケージで一元管理- 複合型:
RECORD/TABLE/VARRAY/連想配列 SUBTYPEでドメイン型を定義し可読性UP- 命名規則:
v_/p_/g_/c_/e_/t_ - 型変換は必ず
TO_NUMBER/TO_CHAR/TO_DATEで明示 - アンチパターン:明示型/NUMBER計算/未初期化/マジックナンバー/CHAR型/暗黙変換/パッケージ変数濫用
PL/SQLの変数・定数は単なる「値の入れ物」ではなく、型選択・スコープ・命名規則・初期化・定数化でコードの安全性と性能が大きく変わる設計領域です。本記事の%TYPE・PLS_INTEGER・NOT NULL DEFAULT・CONSTANT・SUBTYPEを組み合わせれば、バグを型で防ぎ、10倍速く、保守性の高いPL/SQLコードが書けます。IF文はIF文完全ガイド、例外は例外処理完全ガイド、ループはループ処理完全ガイド、カーソルはカーソル完全ガイドと組み合わせて活用してください。

