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

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

大規模データを扱うOracleでは、パーティションテーブルを前提にPL/SQLを設計すると性能と運用性が一気に上がります。読み取りはパーティションプルーニングで最小I/Oに、書き込みはパーティション単位のDMLやEXCHANGEで一括・高速に、保守は「スライディングウィンドウ」で停止なく入れ替えるのが定石です。ここでは月次パーティションを例に、実務で使えるPL/SQLの活用パターンをまとめます。

前提のテーブル設計(範囲+月次パーティション)

まずは売上表を月単位に範囲パーティション化します。ローカルインデックスを基本にするとパーティション操作時の再作成コストを抑えられます。

-- 例:売上テーブルを月次パーティション化(2025-01~)
CREATE TABLE sales (
  id        NUMBER        NOT NULL,
  sale_dt   DATE          NOT NULL,
  cust_id   NUMBER        NOT NULL,
  amount    NUMBER(12,2)  NOT NULL
)
PARTITION BY RANGE (sale_dt) (
  PARTITION p202501 VALUES LESS THAN (DATE '2025-02-01'),
  PARTITION p202502 VALUES LESS THAN (DATE '2025-03-01')
  -- 以降は運用で追加
);

-- ローカルインデックス(各パーティションに分割される)
CREATE INDEX sales_l_idx1 ON sales(cust_id, sale_dt) LOCAL;

PL/SQLで次月パーティションを自動追加する

月初のエラーを防ぐには、翌月分のパーティションを先行作成しておきます。スケジューラから呼び出す小さなプロシージャにしておくと運用が楽になります。

CREATE OR REPLACE PROCEDURE add_next_month_partition IS
  v_first DATE := TRUNC(ADD_MONTHS(SYSDATE, 1), 'MM');
  v_name  VARCHAR2(30) := 'p' || TO_CHAR(v_first, 'YYYYMM');
  v_sql   VARCHAR2(4000);
BEGIN
  v_sql := 'ALTER TABLE sales ADD PARTITION '||v_name||
           ' VALUES LESS THAN (DATE '''||
           TO_CHAR(ADD_MONTHS(v_first, 1), 'YYYY-MM-DD')||''')';
  EXECUTE IMMEDIATE v_sql;
END;
/

パーティション拡張名を使った高速DML

対象が明確なときはパーティション名をDMLに明示すると、プルーニングが確実になり無駄なインデックス維持も抑制できます。

-- 月内データの一括投入(APPENDと併用)
INSERT /*+ APPEND */ INTO sales PARTITION(p202501) (id, sale_dt, cust_id, amount)
SELECT id, sale_dt, cust_id, amount FROM sales_stg WHERE sale_dt >= DATE '2025-01-01' AND sale_dt < DATE '2025-02-01';

-- パーティション限定UPDATE/DELETE
UPDATE  sales PARTITION(p202501) SET amount = amount * 1.02 WHERE cust_id = :target;
DELETE FROM sales PARTITION(p202501) WHERE sale_dt < DATE '2025-01-15';

FORALLと組み合わせたチャンク処理

行ごと前処理が必要なケースでは、コレクションへ取り込みつつパーティション限定でFORALLを投げると安定します。

DECLARE
  TYPE t_row IS RECORD(id NUMBER, sale_dt DATE, cust_id NUMBER, amount NUMBER);
  TYPE t_tab IS TABLE OF t_row;
  v_rows t_tab;
BEGIN
  SELECT id, sale_dt, cust_id, amount
    BULK COLLECT INTO v_rows
    FROM sales_stg
   WHERE sale_dt >= DATE '2025-01-01' AND sale_dt < DATE '2025-02-01';

  FORALL i IN 1..v_rows.COUNT SAVE EXCEPTIONS
    INSERT INTO sales PARTITION(p202501)
      (id, sale_dt, cust_id, amount)
    VALUES
      (v_rows(i).id, v_rows(i).sale_dt, v_rows(i).cust_id, v_rows(i).amount);

  COMMIT;
EXCEPTION
  WHEN OTHERS THEN
    -- 失敗行はSQL%BULK_EXCEPTIONSで拾い上げてログへ
    ROLLBACK;
    RAISE;
END;
/

読み取りの高速化:パーティションプルーニングを前提に書く

PL/SQL内のSELECTも、日付境界をリテラルまたはバインドで明示すればプルーニングが効きます。関数ラップで列値を変換してしまうとプルーニングが効かないことがあるため、境界計算は外で済ませます。

DECLARE
  v_from DATE := DATE '2025-01-01';
  v_to   DATE := DATE '2025-02-01';
  v_sum  NUMBER;
BEGIN
  SELECT SUM(amount)
    INTO v_sum
    FROM sales
   WHERE sale_dt >= v_from AND sale_dt < v_to; -- 列を関数で包まない
  DBMS_OUTPUT.PUT_LINE('sum='||v_sum);
END;
/

スライディングウィンドウ:EXCHANGEで無停止アーカイブ

古いパーティションをアーカイブ表へ素早く退避するには、EXCHANGE PARTITIONでデータ移動なしに入れ替え、片方をDROP/TRUNCATEします。トリガや制約整合のために構造を合わせておきます。

-- アーカイブ用表(同じ列定義・制約)を用意
CREATE TABLE sales_arc AS SELECT * FROM sales WHERE 1=0;

-- パーティションと表の入れ替え(インデックスや制約は要件に応じて調整)
ALTER TABLE sales EXCHANGE PARTITION p202501 WITH TABLE sales_arc WITHOUT VALIDATION;

-- 退避が済んだらパーティションをDROP、またはTRUNCATE
ALTER TABLE sales DROP PARTITION p202501 UPDATE GLOBAL INDEXES;

EXCHANGEは実データの移動を伴わないため高速で、メンテナンス時間を最小化できます。グローバルインデックスを併用している場合はUPDATE GLOBAL INDEXES句を忘れないようにします。

パーティション情報を読み取りながら動的処理する

期間の自動検出や一括保守はディクショナリを読むのが実務的です。USER_TAB_PARTITIONSから境界を取り出し、ループで処理を流せます。

DECLARE
  v_sql VARCHAR2(4000);
BEGIN
  FOR p IN (
    SELECT partition_name
      FROM user_tab_partitions
     WHERE table_name = 'SALES'
       AND high_value < TO_CHAR(DATE '2025-03-01', 'SYYYY-MM-DD', 'NLS_CALENDAR=GREGORIAN')
  ) LOOP
    v_sql := 'ALTER TABLE sales TRUNCATE PARTITION '||p.partition_name||' UPDATE GLOBAL INDEXES';
    EXECUTE IMMEDIATE v_sql;
  END LOOP;
END;
/

DBMS_STATSでパーティション統計を適切に収集する

統計はパーティション単位で鮮度を保つと実行計画が安定します。新規追加や大量ロード後に対象だけを収集すると短時間で終わります。

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS(
    ownname          => USER,
    tabname          => 'SALES',
    partname         => 'P202501',
    estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
    method_opt       => 'FOR ALL COLUMNS SIZE AUTO',
    degree           => DBMS_STATS.AUTO_DEGREE
  );
END;
/

エラーログとの併用で堅牢なロードにする

一括投入時はDBMS_ERRLOGを併用して壊れた行だけ脇に逃がします。パーティション拡張名と合わせると障害切り分けが容易です。

BEGIN
  DBMS_ERRLOG.CREATE_ERROR_LOG('SALES'); -- 初回のみ
  INSERT /*+ APPEND */ INTO sales PARTITION(p202502)
    SELECT id, sale_dt, cust_id, amount FROM sales_stg WHERE load_id = :id
    LOG ERRORS INTO err$_sales REJECT LIMIT UNLIMITED;
  COMMIT;
END;
/

よくある落とし穴と回避策

ローカルインデックス前提の保守にグローバルインデックスを混在させると、DROP/TRUNCATE PARTITION時に再構築で止まりがちです。可能ならローカルへ寄せ、どうしてもグローバルが必要ならUPDATE GLOBAL INDEXES付き操作か、保守時間帯にリビルドを前提化します。パーティションキー列に関数を掛けるとプルーニングが効かなくなるため、境界値はリテラルまたはバインドで素直に書きます。DMLでパーティション名を明示する場合、誤った名前指定はORA-14701系のエラーになるので、運用名は規約化しディクショナリから検証するのが安全です。

まとめ

パーティションテーブルは、読む・書く・保守するを「パーティション単位」に落とし込むことで真価を発揮します。PL/SQLではパーティション拡張名のDML、FORALLとの併用、EXCHANGEを核に据え、追加や削除を自動化する小さなユーティリティを用意すると運用が安定します。統計はパーティション単位で更新し、エラーログでロードを堅牢化しつつ、スライディングウィンドウで古いデータを無停止に入れ替える。この一連の型を押さえることで、巨大テーブルでも予測可能な性能と短時間の保守を両立できます。