Oracleのパーティションテーブルは、大量データを月別・日別・範囲別に分割し、検索、ロード、アーカイブ、削除を扱いやすくする仕組みです。PL/SQLはパーティションそのものの機能ではありませんが、月次パーティション追加、ロード、EXCHANGE、統計収集、古いパーティション整理などの運用を自動化できます。
ただし、パーティション操作の多くはDDLです。DDLには暗黙COMMITがあり、権限・索引・統計・ロック・再実行性まで考えずに自動化すると、本番運用で事故になりやすいです。この記事では、PL/SQLでパーティション運用を安全に自動化するための実務テンプレートを整理します。動的SQLの基本は Oracle EXECUTE IMMEDIATE完全ガイド、DBMS_STATS全体は DBMS_STATSによる統計情報の手動収集・管理完全ガイド も参考にしてください。
- PL/SQLで自動化する範囲と、SQL/DDL側で設計する範囲
- Interval Partitioningを先に検討する判断基準
- 月次パーティション追加の安全な動的SQL
- DDLの暗黙COMMITと運用ログ
- EXCHANGE PARTITIONによる高速ロード・アーカイブ
- DBMS_STATSでのパーティション統計収集
- DBMS_SCHEDULERによる定期実行
PL/SQLで自動化する範囲
パーティション設計はSQL/DDLと物理設計の領域です。PL/SQLは、その運用作業を手順化し、漏れなく実行するために使います。たとえば、月末に次月パーティションを追加する、ロード用ステージング表を本番パーティションへEXCHANGEする、古いパーティションを切り離す、統計を収集する、といった処理です。
まずInterval Partitioningを検討する
月次や日次の範囲パーティションを毎回手動追加する前に、Interval Partitioningで自動作成できないか検討します。単純な日付範囲で、将来パーティションを同じ間隔で増やすだけなら、PL/SQLで ALTER TABLE ADD PARTITION を自動化するより、Interval Partitioningのほうがシンプルです。
CREATE TABLE sales_log ( sale_id NUMBER, sale_date DATE NOT NULL, amount NUMBER ) PARTITION BY RANGE (sale_date) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH')) ( PARTITION p202601 VALUES LESS THAN (DATE '2026-02-01') );
一方、パーティション名を厳密に管理したい、EXCHANGE運用と合わせたい、月ごとに表領域や圧縮設定を変えたい、古いパーティションの切り離しまで同じ手順で扱いたい場合は、PL/SQLによる運用自動化が役立ちます。
DDLの暗黙COMMITに注意する
ALTER TABLE ADD PARTITION、DROP PARTITION、EXCHANGE PARTITION などはDDLです。DDLは実行前後に暗黙COMMITが発生するため、通常のDMLと同じ感覚でトランザクション制御できません。パーティション運用をPL/SQL化する場合、DDL前に運用ログを残し、DDL後に結果を更新する設計にします。
CREATE TABLE partition_maintenance_log ( log_id NUMBER GENERATED BY DEFAULT AS IDENTITY, operation VARCHAR2(50) NOT NULL, table_name VARCHAR2(128) NOT NULL, partition_name VARCHAR2(128), status VARCHAR2(30) NOT NULL, message VARCHAR2(4000), started_at TIMESTAMP DEFAULT SYSTIMESTAMP, finished_at TIMESTAMP, CONSTRAINT pk_partition_maintenance_log PRIMARY KEY (log_id) );
DDLが成功したあとでPL/SQL側が失敗しても、DDL自体は戻せません。そのため、事前検証、実行ログ、再実行時の存在チェックが重要になります。
月次パーティション追加のテンプレート
次は、売上ログ表に月次パーティションを追加する例です。テーブル名やパーティション名をユーザー入力から直接連結せず、固定対象またはホワイトリストで扱います。ここでは対象表を固定し、パーティション名と境界日をプログラムで生成します。
CREATE OR REPLACE PROCEDURE add_sales_partition(
p_month DATE
) IS
l_month_start DATE := TRUNC(p_month, 'MM');
l_next_month DATE := ADD_MONTHS(TRUNC(p_month, 'MM'), 1);
l_part_name VARCHAR2(30);
l_sql VARCHAR2(4000);
l_exists NUMBER;
BEGIN
l_part_name := 'P' || TO_CHAR(l_month_start, 'YYYYMM');
SELECT COUNT(*)
INTO l_exists
FROM user_tab_partitions
WHERE table_name = 'SALES_LOG'
AND partition_name = l_part_name;
IF l_exists > 0 THEN
RETURN;
END IF;
l_sql :=
'ALTER TABLE sales_log ADD PARTITION ' || l_part_name ||
' VALUES LESS THAN (DATE ''' || TO_CHAR(l_next_month, 'YYYY-MM-DD') || ''')';
INSERT INTO partition_maintenance_log(operation, table_name, partition_name, status, message)
VALUES ('ADD_PARTITION', 'SALES_LOG', l_part_name, 'STARTED', l_sql);
EXECUTE IMMEDIATE l_sql;
UPDATE partition_maintenance_log
SET status = 'DONE',
finished_at = SYSTIMESTAMP
WHERE operation = 'ADD_PARTITION'
AND table_name = 'SALES_LOG'
AND partition_name = l_part_name
AND status = 'STARTED';
END;
/
DBMS_ASSERT などで防御します。日付値も文字列連結する場合は、NLS依存を避けるため DATE 'YYYY-MM-DD' 形式に寄せます。パーティション存在確認SQL
パーティション運用では、存在確認、境界値、統計状態をすぐ見られるSQLを用意しておくと便利です。運用スクリプトの前後で同じ確認SQLを使うと、変更前後の差分を追いやすくなります。
SELECT table_name, partition_name, high_value, num_rows, last_analyzed FROM user_tab_partitions WHERE table_name = 'SALES_LOG' ORDER BY partition_position;
本番運用では、DBA_TAB_PARTITIONS や ALL_TAB_PARTITIONS を使うこともあります。権限に応じて確認ビューを選んでください。
パーティションプルーニングを前提にSQLを書く
パーティションテーブルの性能は、パーティションキーに素直な条件を書けるかで大きく変わります。たとえば sale_date で月次パーティションを切っているなら、検索条件も sale_date >= ... AND sale_date < ... の形にします。
-- パーティションプルーニングしやすい条件 SELECT SUM(amount) FROM sales_log WHERE sale_date >= DATE '2026-05-01' AND sale_date < DATE '2026-06-01'; -- 避けたい例: パーティションキーに関数をかける SELECT SUM(amount) FROM sales_log WHERE TO_CHAR(sale_date, 'YYYYMM') = '202605';
PL/SQLから検索条件を組み立てる場合も、パーティションキーに関数をかけない、暗黙変換を避ける、日付範囲を明示する、という基本を守ります。大量処理でBULK COLLECTやFORALLを使う場合は BULK COLLECT / FORALL完全ガイド も参考になります。
パーティション指定INSERTを使う場面
ロード対象の月が明確な場合、INSERT INTO table PARTITION(partition_name) で投入先パーティションを指定できます。ただし、指定を誤るとパーティション範囲外エラーになります。通常はパーティションキーで自然に振り分け、必要な場面だけ拡張名を使うのが安全です。
INSERT INTO sales_log PARTITION (P202605) ( sale_id, sale_date, amount ) SELECT sale_id, sale_date, amount FROM sales_stage WHERE sale_date >= DATE '2026-05-01' AND sale_date < DATE '2026-06-01';
INSERT INTO ... SELECT の基本や大量投入の注意は INSERT INTO…SELECT完全ガイド にまとめています。
EXCHANGE PARTITIONで高速ロードする
大量データを本番パーティションへ入れる場合、ステージング表へ先にロードしてから EXCHANGE PARTITION で入れ替える方式が有効です。データを行単位で移動するのではなく、セグメントを交換するため高速です。ただし、列定義、制約、索引、統計、検証オプションを事前にそろえる必要があります。
-- ステージング表に対象月のデータをロード済みとする ALTER TABLE sales_log EXCHANGE PARTITION P202605 WITH TABLE sales_stage_202605 INCLUDING INDEXES WITHOUT VALIDATION UPDATE GLOBAL INDEXES;
WITHOUT VALIDATION は高速ですが、ステージング表のデータが本当に対象パーティション範囲に収まっていることを事前に検証する前提です。不安がある場合は検証SQLや制約で守り、グローバル索引がある場合は UPDATE GLOBAL INDEXES の影響も確認してください。EXCHANGE前の検証SQL
EXCHANGE前には、ステージング表の件数、日付範囲、NULL、重複、パーティション範囲外データを確認します。この検証をPL/SQLプロシージャに組み込み、失敗時はEXCHANGEしないようにします。
SELECT COUNT(*) AS row_count, MIN(sale_date) AS min_sale_date, MAX(sale_date) AS max_sale_date FROM sales_stage_202605; -- 対象月外のデータが混ざっていないか SELECT COUNT(*) AS out_of_range_count FROM sales_stage_202605 WHERE sale_date < DATE '2026-05-01' OR sale_date >= DATE '2026-06-01'; -- 業務キー重複の確認例 SELECT sale_id, COUNT(*) FROM sales_stage_202605 GROUP BY sale_id HAVING COUNT(*) > 1;
古いパーティションを切り離す
保存期間を過ぎたデータは、DELETEで1行ずつ消すより、パーティション単位で切り離すほうが高速です。ただし、DROP PARTITION や TRUNCATE PARTITION は取り返しがつきにくいため、事前確認とバックアップ方針が必要です。
-- 例: 古い月次パーティションを削除する ALTER TABLE sales_log DROP PARTITION P202401 UPDATE GLOBAL INDEXES;
削除前に、対象パーティション、件数、保存期限、アーカイブ済みかどうかをログに残します。グローバル索引がある場合はメンテナンス時間が伸びることがあるため、事前検証が必要です。
DBMS_STATSで統計を収集する
ロードやEXCHANGE後は、対象パーティションの統計を収集します。統計が古いと、パーティションプルーニングや結合順序が期待通りにならず、急に遅くなることがあります。パーティション単位の統計、表全体のグローバル統計、索引統計をどう扱うかを運用手順に含めます。
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => USER,
tabname => 'SALES_LOG',
partname => 'P202605',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO',
granularity => 'AUTO',
cascade => TRUE
);
END;
/
列の相関が強く、通常の統計だけでは実行計画が安定しない場合は、拡張統計を検討します。詳しくは Oracle拡張統計完全ガイド が近い内容です。
パーティション運用プロシージャの骨格
月次ロードを自動化する場合、1つの巨大プロシージャにすべて詰め込むより、確認、ロード、EXCHANGE、統計、ログ更新を段階化します。以下は全体の骨格です。
CREATE OR REPLACE PROCEDURE run_monthly_partition_load(
p_target_month DATE
) IS
l_part_name VARCHAR2(30) := 'P' || TO_CHAR(TRUNC(p_target_month, 'MM'), 'YYYYMM');
BEGIN
-- 1. 次月パーティションの存在確認・必要なら追加
add_sales_partition(p_target_month);
-- 2. ステージング表を検証
validate_sales_stage(p_target_month);
-- 3. EXCHANGE PARTITION
exchange_sales_partition(p_target_month);
-- 4. 対象パーティション統計を収集
DBMS_STATS.GATHER_TABLE_STATS(
ownname => USER,
tabname => 'SALES_LOG',
partname => l_part_name,
granularity => 'AUTO',
cascade => TRUE
);
-- 5. 運用ログ更新
INSERT INTO partition_maintenance_log(operation, table_name, partition_name, status, message)
VALUES ('MONTHLY_LOAD', 'SALES_LOG', l_part_name, 'DONE', 'monthly load completed');
END;
/
本番では、各ステップの開始・終了・失敗をログ化し、どこまで完了したかを再実行時に判断できるようにします。ジョブ履歴管理は ジョブ実行履歴の集中管理と異常検知 も参考になります。
DBMS_SCHEDULERで定期実行する
パーティション追加や統計収集を手動運用にすると、月初や月末に漏れやすくなります。DBMS_SCHEDULER で定期実行し、失敗時の通知・再実行・履歴確認まで含めて設計します。
BEGIN
DBMS_SCHEDULER.CREATE_JOB(
job_name => 'JOB_ADD_NEXT_MONTH_PARTITION',
job_type => 'PLSQL_BLOCK',
job_action => q'[
BEGIN
add_sales_partition(ADD_MONTHS(TRUNC(SYSDATE, 'MM'), 1));
END;
]',
start_date => SYSTIMESTAMP,
repeat_interval => 'FREQ=MONTHLY;BYMONTHDAY=20;BYHOUR=2;BYMINUTE=0;BYSECOND=0',
enabled => TRUE,
comments => 'Create next month partition for SALES_LOG'
);
END;
/
DBMS_SCHEDULERの基本は DBMS_SCHEDULER完全ガイド、CHAINやリトライまで含めた運用は DBMS_SCHEDULERでジョブ管理を極める を確認してください。
エラーログと再実行性
大量ロードでは、ロード失敗行を別に保存し、正常行だけ進める設計が必要な場合があります。SQL単体で失敗行を集めるなら DBMS_ERRLOG、PL/SQL配列と突き合わせるなら FORALL SAVE EXCEPTIONS を使います。
INSERT INTO sales_stage_202605(sale_id, sale_date, amount)
SELECT sale_id, sale_date, amount
FROM sales_source
WHERE sale_date >= DATE '2026-05-01'
AND sale_date < DATE '2026-06-01'
LOG ERRORS INTO err$_sales_stage_202605('LOAD_202605')
REJECT LIMIT UNLIMITED;
DBMS_ERRLOG は DBMS_ERRLOGの使い方、FORALL SAVE EXCEPTIONS は FORALL SAVE EXCEPTIONSで失敗行をログ化 に詳しくまとめています。
グローバル索引の影響を確認する
パーティション操作では、ローカル索引とグローバル索引で影響が変わります。DROP PARTITION や EXCHANGE PARTITION の後にグローバル索引が無効になる、または更新に時間がかかる場合があります。どの索引がローカルかグローバルかを事前に確認します。
SELECT index_name, partitioned, status FROM user_indexes WHERE table_name = 'SALES_LOG' ORDER BY index_name; SELECT index_name, partition_name, status FROM user_ind_partitions WHERE index_name IN ( SELECT index_name FROM user_indexes WHERE table_name = 'SALES_LOG' ) ORDER BY index_name, partition_position;
よくある落とし穴
よくある質問
まとめ
PL/SQLは、Oracleのパーティションテーブル運用を自動化する強力な手段です。月次パーティション追加、EXCHANGE PARTITION、古いパーティション整理、統計収集、DBMS_SCHEDULERによる定期実行まで、手順化すれば運用漏れを減らせます。
一方で、パーティション操作の多くはDDLであり、暗黙COMMIT、権限、動的SQL、グローバル索引、統計、再実行性に注意が必要です。単純な月次追加ならInterval Partitioningを検討し、PL/SQLで自動化する場合は、事前確認、運用ログ、EXCHANGE前検証、DBMS_STATSまで含めた実務手順として設計しましょう。
参考: Oracle Partitioning Overview / Oracle Database PL/SQL Packages and Types Reference – DBMS_STATS / Oracle Database VLDB and Partitioning Guide

