【PL/SQL】変数・定数完全ガイド|データ型体系・%TYPE/%ROWTYPE・PLS_INTEGER高速化・CONSTANT・SUBTYPE・命名規則

【PL/SQL】変数・定数の使い方 PL/SQL

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種(NUMBERPLS_INTEGERBINARY_INTEGERBINARY_DOUBLE)の性能比較
  • 文字列型(VARCHAR2CHARCLOBNVARCHAR2)の使い分け
  • 日付型(DATETIMESTAMPINTERVAL)の違い
  • BOOLEAN型(PL/SQL限定、テーブルには使えない)
  • %TYPE%ROWTYPEでテーブル変更に自動追従
  • NOT NULL制約・DEFAULT初期値で未初期化NULL事故を防ぐ
  • CONSTANT定数とマジックナンバー排除
  • 複合型:RECORDTABLEVARRAY/連想配列
  • SUBTYPEで独自型を定義して可読性UP
  • 変数スコープ:ブロック/サブプログラム/パッケージ変数でセッション永続
  • 命名規則(v_p_g_c_l_
  • 実務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_INTEGERNUMBERより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)
  • 文字列→原則VARCHAR2CHARは使わない)
  • 大容量テキスト/バイナリCLOBBLOB

数値型4種の性能と使い分け

精度/範囲 速度 用途
NUMBER(p,s) 10進38桁/無限小数可 △ 標準 金額/会計/小数計算(精度最重要)
PLS_INTEGER -2,147,483,648〜2,147,483,647(32bit整数) NUMBERより3〜10倍速 ループカウンタ/整数演算
BINARY_INTEGER PLS_INTEGERと同等(エイリアス扱い) レガシー互換(PLS_INTEGER推奨)
BINARY_FLOAT IEEE 754 単精度(32bit) 科学計算(精度より速度)
BINARY_DOUBLE IEEE 754 倍精度(64bit) 統計/機械学習/浮動小数計算
SIMPLE_INTEGER PLS_INTEGERと同範囲・NULL不可 ◎◎ 最速(オーバーフロー検査省略) ホットループのカウンタ
NUMBER vs PLS_INTEGER 性能実例
-- ❌ 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はオーバーフロー時にラップアラウンド(最大値→最小値)する仕様で例外が出ないため、確実に範囲内と分かっている場合のみ使用してください。

文字列型の使い分け

最大長(PL/SQL) 特徴
VARCHAR2(n) 32767バイト(PL/SQL)/4000バイト(SQL・MAX_STRING_SIZE=STANDARD 第一選択。可変長、末尾空白を保持
CHAR(n) 32767バイト 固定長、末尾空白パディング(事故多発)
NVARCHAR2(n) 32767バイト Unicode固定の可変長文字列
CLOB 最大128TB 大容量テキスト(ブログ本文・JSON等)
LONG 2GB 非推奨(レガシー)。CLOBで置換すべき
VARCHAR2 vs CHAR の典型事故
-- ❌ 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';    -- マッチする
VARCHAR2のサイズ単位指定
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限定の落とし穴

BOOLEAN基本
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対応(ただしまだ普及途上)。

BOOLEAN ↔ CHAR/NUMBER 変換
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型:大容量テキスト/バイナリ

LOB型4種
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.APPENDWRITEAPPEND:追記
  • 詳細は【PL/SQL】予約語一覧とあわせてOracle公式DBMS_LOBリファレンスを参照

%TYPE/%ROWTYPE でテーブル変更に自動追従

%TYPE:単一列の型を継承

%TYPEで保守性UP
-- ❌ 明示型宣言(テーブル変更時に全変数を書き換え必要)
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で一括継承
-- テーブル%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初期値

未初期化NULLを防ぐ
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)でマジックナンバーを排除

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種

TABLE / VARRAY / 連想配列
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_

プレフィックス 対象
v_ ローカル変数(Variable) v_user_idv_total_amount
p_ サブプログラムのParameter p_user_idp_status
l_ ローカル(Local)。v_の代替 l_bufferl_count
g_ パッケージ変数(Global) g_session_idg_debug
c_ 定数(Constant) c_tax_ratec_max_retry
e_ 例外(Exception) e_not_founde_invalid
t_ 型定義(TYPE) t_idst_user_rec
cur_ カーソル(CURSOR) cur_userscur_orders

命名規則の効果:①コード中の識別子がパラメータ(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_NUMBERTO_CHARTO_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')で明示。

⑦パッケージ変数の濫用。セッションライフタイムのグローバル変数は便利だが、並行処理では予期せぬ副作用を生む。値の変更が追跡困難になるので、明確な用途(ユーザーコンテキスト・キャッシュ)以外は引数で渡す方がよい。

よくある質問

Q:=DEFAULTは何が違う?
A機能的には同じ(初期値の代入)。DEFAULTの方が宣言時の意図(初期値設定)が明確で可読性が高い。実行コードの代入は:=のみ使えます。慣例としてはDECLARE内の初期値はDEFAULTBEGIN内の代入は:=と使い分けるのが読みやすい。
QVARCHAR2(10)VARCHAR2(10 CHAR)の違い
ABYTE単位かCHAR単位かVARCHAR2(10)はデフォルトのNLS_LENGTH_SEMANTICS(通常BYTE)に従い、UTF-8で日本語を10文字入れたいと思うと30バイト必要で格納失敗。VARCHAR2(10 CHAR)なら明確に10文字格納できます。日本語・絵文字を扱うならCHAR単位指定を推奨。
QPLS_INTEGERBINARY_INTEGERの違い
A実質同じ(Oracle 11g以降はBINARY_INTEGERPLS_INTEGERのエイリアス)。どちらを使ってもOKですが、新規コードはPLS_INTEGER推奨。両者ともNUMBERより3〜10倍速いので、整数カウンタ・ループ変数には積極的に使いましょう。
QBOOLEAN型をテーブル列に使える?
AOracle 22c以前は不可(PL/SQL変数のみ使用可)。23c以降のSQL BOOLEAN型サポートで可能になりましたが、まだ多くの本番環境が21c以前のため、CHAR(1) ‘Y’/’N’ または NUMBER(1) 0/1で代用するのが実務の定石です。PL/SQL側でBOOLEAN変数を使い、DB格納時にCASE WHEN b THEN 'Y' ELSE 'N' ENDで変換します。
Q定数は値を変えられないの?
Aはい、一度初期化した定数(CONSTANT)は実行中に変更できません。変えようとするとPLS-00363コンパイルエラー。ビジネスルール値(税率・閾値)のように「セッション中は変わらない」値に使うと、コードの意図が明確になり不意な変更を防げます。
Q%TYPEと%ROWTYPEの使い分け
A%TYPE単一列の型継承(例:users.email%TYPE)、%ROWTYPEテーブル/カーソルの全列構造体(例:users%ROWTYPEv.idv.emailでアクセス)。1列だけなら%TYPE、複数列まとめて扱うなら%ROWTYPE
Q変数の上限文字数を超えたら?
AVALUE_ERROR(ORA-06502)例外が発生します。例:v_name VARCHAR2(10); v_name := '1234567890123';はエラー。対策は①サイズを大きく取る、②SUBSTR(val, 1, 10)で切り詰める、③%TYPEでテーブル列と同じサイズ継承、④長文はCLOBにする、のいずれか。
Q変数と同じ名前の列があってクエリが変になる
A名前衝突が起きると、列とPL/SQL変数のどちらを参照するかで意図しない挙動になります。例:SELECT * FROM users WHERE id = idは列vs列の比較で全行マッチ!対策:①変数にプレフィックスp_idv_id)、②テーブル別名(users u)で列をu.idと限定、③BEGIN …<<block_name>>ラベルでスコープ明示。
Qパッケージ変数は他セッションから見える?
A見えません。パッケージ変数はセッションごとに独立した領域を持つため、セッションAでg_user := 100;してもセッションBのg_userは別(NULLまたは既定値)。サーバー全体で共有したい場合はテーブルSYS_CONTEXTを使います。
QCONSTANT宣言は頭か末尾どちら?
APL/SQLの正しい構文は変数名 CONSTANT 型 := 値;(型の前)。例:c_max CONSTANT PLS_INTEGER := 100;。他言語のconst int c_max = 100;のような順番とは違うので注意。
QSUBTYPEの範囲制約は実行時に効く?
A効きます。SUBTYPE positive_int_t IS PLS_INTEGER RANGE 1..100;で宣言した変数に範囲外の値を代入するとVALUE_ERROR例外。型で制約を定義することで「バグを型で防ぐ」設計が可能。NOT NULL制約も同様に効きます。

関連記事

まとめ

  • PL/SQLデータ型の4大分類:スカラー/複合/参照/LOB
  • 整数演算はPLS_INTEGERNUMBERより3〜10倍高速
  • 金額・会計はNUMBER(p, s)(精度保証)、科学計算はBINARY_DOUBLE
  • 文字列はVARCHAR2原則、日本語は(n CHAR)で指定
  • CHARは末尾空白パディングで事故多発、使わない
  • BOOLEAN型はPL/SQL専用(22c以前はテーブル不可、CHAR(1)で代用)
  • %TYPE%ROWTYPEでテーブル変更に自動追従
  • NOT NULLDEFAULT未初期化NULL演算バグを物理防止
  • CONSTANTでマジックナンバー排除、パッケージで一元管理
  • 複合型:RECORDTABLEVARRAY/連想配列
  • SUBTYPEでドメイン型を定義し可読性UP
  • 命名規則:v_p_g_c_e_t_
  • 型変換は必ずTO_NUMBERTO_CHARTO_DATEで明示
  • アンチパターン:明示型/NUMBER計算/未初期化/マジックナンバー/CHAR型/暗黙変換/パッケージ変数濫用

PL/SQLの変数・定数は単なる「値の入れ物」ではなく、型選択・スコープ・命名規則・初期化・定数化でコードの安全性と性能が大きく変わる設計領域です。本記事の%TYPEPLS_INTEGERNOT NULL DEFAULTCONSTANTSUBTYPEを組み合わせれば、バグを型で防ぎ、10倍速く、保守性の高いPL/SQLコードが書けます。IF文はIF文完全ガイド、例外は例外処理完全ガイド、ループはループ処理完全ガイド、カーソルはカーソル完全ガイドと組み合わせて活用してください。