【PL/SQL】パーティションテーブル運用を自動化する方法|月次追加・EXCHANGE・DBMS_STATSまで解説

【PL/SQL】パーティションテーブルとPL/SQLの組み合わせ活用例 PL/SQL

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する、古いパーティションを切り離す、統計を収集する、といった処理です。

設計で決めることパーティションキー、範囲、粒度、索引、保存期間、ロード方式。
PL/SQLで自動化することDDL実行、事前確認、運用ログ、統計収集、ジョブ化、失敗通知。
SQLで守ることパーティションプルーニングが効く条件、制約、索引、ロードSQL。
運用で守ること暗黙COMMIT、権限、ロック時間、グローバル索引、切り戻し手順。

まずInterval Partitioningを検討する

月次や日次の範囲パーティションを毎回手動追加する前に、Interval Partitioningで自動作成できないか検討します。単純な日付範囲で、将来パーティションを同じ間隔で増やすだけなら、PL/SQLで ALTER TABLE ADD PARTITION を自動化するより、Interval Partitioningのほうがシンプルです。

interval-partition-example.sql
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 PARTITIONDROP PARTITIONEXCHANGE PARTITION などはDDLです。DDLは実行前後に暗黙COMMITが発生するため、通常のDMLと同じ感覚でトランザクション制御できません。パーティション運用をPL/SQL化する場合、DDL前に運用ログを残し、DDL後に結果を更新する設計にします。

partition-operation-log.sql
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自体は戻せません。そのため、事前検証、実行ログ、再実行時の存在チェックが重要になります。

月次パーティション追加のテンプレート

次は、売上ログ表に月次パーティションを追加する例です。テーブル名やパーティション名をユーザー入力から直接連結せず、固定対象またはホワイトリストで扱います。ここでは対象表を固定し、パーティション名と境界日をプログラムで生成します。

add-monthly-partition.sql
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;
/
動的SQLの注意: DDLではオブジェクト名をバインド変数にできません。テーブル名やパーティション名を外部入力で受ける場合は、ホワイトリスト化、命名規則チェック、DBMS_ASSERT などで防御します。日付値も文字列連結する場合は、NLS依存を避けるため DATE 'YYYY-MM-DD' 形式に寄せます。

パーティション存在確認SQL

パーティション運用では、存在確認、境界値、統計状態をすぐ見られるSQLを用意しておくと便利です。運用スクリプトの前後で同じ確認SQLを使うと、変更前後の差分を追いやすくなります。

check-partitions.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_PARTITIONSALL_TAB_PARTITIONS を使うこともあります。権限に応じて確認ビューを選んでください。

パーティションプルーニングを前提にSQLを書く

パーティションテーブルの性能は、パーティションキーに素直な条件を書けるかで大きく変わります。たとえば sale_date で月次パーティションを切っているなら、検索条件も sale_date >= ... AND sale_date < ... の形にします。

partition-pruning-query.sql
-- パーティションプルーニングしやすい条件
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-partition.sql
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 で入れ替える方式が有効です。データを行単位で移動するのではなく、セグメントを交換するため高速です。ただし、列定義、制約、索引、統計、検証オプションを事前にそろえる必要があります。

exchange-partition.sql
-- ステージング表に対象月のデータをロード済みとする
ALTER TABLE sales_log
EXCHANGE PARTITION P202605
WITH TABLE sales_stage_202605
INCLUDING INDEXES
WITHOUT VALIDATION
UPDATE GLOBAL INDEXES;
EXCHANGEの注意: WITHOUT VALIDATION は高速ですが、ステージング表のデータが本当に対象パーティション範囲に収まっていることを事前に検証する前提です。不安がある場合は検証SQLや制約で守り、グローバル索引がある場合は UPDATE GLOBAL INDEXES の影響も確認してください。

EXCHANGE前の検証SQL

EXCHANGE前には、ステージング表の件数、日付範囲、NULL、重複、パーティション範囲外データを確認します。この検証をPL/SQLプロシージャに組み込み、失敗時はEXCHANGEしないようにします。

validate-stage-before-exchange.sql
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 PARTITIONTRUNCATE PARTITION は取り返しがつきにくいため、事前確認とバックアップ方針が必要です。

drop-old-partition.sql
-- 例: 古い月次パーティションを削除する
ALTER TABLE sales_log
DROP PARTITION P202401
UPDATE GLOBAL INDEXES;

削除前に、対象パーティション、件数、保存期限、アーカイブ済みかどうかをログに残します。グローバル索引がある場合はメンテナンス時間が伸びることがあるため、事前検証が必要です。

DBMS_STATSで統計を収集する

ロードやEXCHANGE後は、対象パーティションの統計を収集します。統計が古いと、パーティションプルーニングや結合順序が期待通りにならず、急に遅くなることがあります。パーティション単位の統計、表全体のグローバル統計、索引統計をどう扱うかを運用手順に含めます。

gather-partition-stats.sql
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、統計、ログ更新を段階化します。以下は全体の骨格です。

monthly-partition-maintenance-procedure.sql
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 で定期実行し、失敗時の通知・再実行・履歴確認まで含めて設計します。

scheduler-monthly-partition-job.sql
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 を使います。

load-with-error-logging.sql
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_ERRLOGDBMS_ERRLOGの使い方FORALL SAVE EXCEPTIONSFORALL SAVE EXCEPTIONSで失敗行をログ化 に詳しくまとめています。

グローバル索引の影響を確認する

パーティション操作では、ローカル索引とグローバル索引で影響が変わります。DROP PARTITIONEXCHANGE PARTITION の後にグローバル索引が無効になる、または更新に時間がかかる場合があります。どの索引がローカルかグローバルかを事前に確認します。

check-partitioned-indexes.sql
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;

よくある落とし穴

DDLをDMLのつもりで扱うALTER TABLE系は暗黙COMMITがあります。ROLLBACK前提の設計にしないでください。
動的SQLをそのまま連結する外部入力のテーブル名・パーティション名をそのまま連結しない。固定名・ホワイトリスト・検証を使います。
Intervalで足りるのに自動化する単純な月次追加ならInterval Partitioningのほうが保守しやすい場合があります。
EXCHANGE前検証がない対象月外データ、重複、制約違反を確認してから交換します。
統計収集を忘れるロード後の統計が古いと、実行計画が悪化することがあります。
グローバル索引を見ないDROP/EXCHANGE後の索引状態とメンテナンス時間を事前に確認します。

よくある質問

Q. 月次パーティションは必ずPL/SQLで追加すべきですか?
A. いいえ。単純な日付範囲ならInterval Partitioningで自動作成するほうが簡単な場合があります。
Q. ALTER TABLEをPL/SQL内で実行してもROLLBACKできますか?
A. 基本的にできません。DDLには暗黙COMMITがあります。事前確認と運用ログ、再実行設計が重要です。
Q. EXCHANGE PARTITIONは安全ですか?
A. 高速ですが、列定義、制約、索引、対象範囲の検証が必要です。WITHOUT VALIDATIONを使うなら特に事前検証が重要です。
Q. 統計は表全体を毎回集めるべきですか?
A. 常に表全体とは限りません。ロード対象パーティション、グローバル統計、索引統計の必要性をデータ量と実行計画で判断します。
Q. パーティション拡張名を使えば必ず速くなりますか?
A. 目的のパーティションが明確なロードでは有効ですが、通常の検索ではパーティションキー条件でプルーニングさせるほうが自然です。

まとめ

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