Oracle PL/SQLのパイプライン表関数(Pipelined Table Function)は、FROM句でテーブルのように呼び出せる関数で、1000万件超の大量データを一定メモリかつ高スループットで流す処理に絶大な効果を発揮します。「PIPELINEDを書けば速くなる」と雰囲気で使っている現場も多いですが、カーディナリティヒント・並列パイプライン・REF CURSOR入力・BULK COLLECT併用の設計まで踏み込まないと、実測性能は数倍どころか逆に遅くなることもあります。
この記事では「大量データ処理でパイプライン関数を勝たせる」実戦技法に絞って整理します。通常TABLE関数/SQL単体との実測ベンチ、メモリ消費の可視化、カーディナリティヒントの効果、PARALLEL ENABLEのPARTITION BY/CLUSTER BY使い分け、REF CURSOR入力でチェーン処理する設計、BULK COLLECT+PIPE ROWで1.5〜3倍の追加ブースト、実務8シナリオ、アンチパターン7選、FAQ10問まで2026年版で解説します。
この記事でわかること
- 通常関数/TABLE関数/パイプライン関数の実測ベンチマーク(最大10倍差)
- メモリ消費がなぜ一定に保たれるのか(ストリーミングの内部動作)
- CARDINALITYヒントで実行計画を正常化して逆転防止する方法
- PARALLEL_ENABLEの
PARTITION BY/CLUSTER BYの使い分け - REF CURSOR入力でパイプライン関数同士を連結するチェーン設計
- BULK COLLECT+FORALL PIPE ROW で更に1.5〜3倍速くする技法
- ETL・マスキング・CSV分解・差分比較など実務8シナリオの正解実装
- 本番で踏むアンチパターン7選(DMLトリガー内使用・全件バッファ化など)
- デバッグ方法(DBMS_OUTPUT・自己診断テーブル・10046トレース)
- 30秒でわかるパイプライン関数の結論
- なぜパイプライン関数は速いのか|内部動作の仕組み
- パイプライン関数の最小構文
- CARDINALITYヒント|オプティマイザの8192行決め打ちを回避する
- 並列パイプライン|PARALLEL_ENABLEで複数スレーブに分散する
- BULK COLLECT+PIPE ROWで1.5〜3倍速くする
- パイプライン関数のチェーン(連結再利用)
- パイプライン関数 vs 純SQL(INSERT SELECT)|どちらを選ぶべきか
- 実務シナリオ8選|使い所と正解実装
- 本番で踏むパイプライン関数アンチパターン7選
- パイプライン関数のデバッグ方法
- よくある質問
- 関連記事で知識を深める
- まとめ|パイプライン関数を大量データ処理の決め手にする
30秒でわかるパイプライン関数の結論
忙しい読者向けに「まず押さえるべき7つの結論」を先に提示します。
| 結論 | 理由・根拠 |
|---|---|
| ① 1万件以上の行生成はパイプライン一択 | 通常TABLE関数は全件PGAに溜めるためメモリ爆発・速度低下 |
| ② CARDINALITYヒントは必須装備 | オプティマイザは戻り行数を8192行と決め打ち。実態と乖離すると遅くなる |
| ③ 並列化はPARALLEL_ENABLE+入力REF CURSOR必須 | PARTITION BYで行をパラレル実行スレーブに分配する |
| ④ PIPE ROWは1件ずつ返すので | 内部でBULK COLLECTしておき一定件数ごとにPIPE ROWするとさらに速い |
| ⑤ パイプライン関数をFROM句で複数チェーンできる | CURSOR(SELECT * FROM TABLE(f1))を別のパイプ関数に渡す再利用パターン |
| ⑥ SELECT INSERTが可能ならそちらの方が速いことが多い | 複雑なPL/SQL変換が必要なときだけパイプライン関数の価値が出る |
| ⑦ トランザクション途中でのDML直接実行は避ける | コンテキスト問題と例外ハンドリング不能化の温床 |
なぜパイプライン関数は速いのか|内部動作の仕組み
パイプライン関数の真価を理解するには、通常関数との「呼び出し元への制御の返し方」の違いを知る必要があります。この動作原理を知らないとチューニングの勘所がつかめません。
通常関数(RETURN scalar)
スカラー値を1件だけ返す最もシンプルな関数です。PGA消費は少量で呼び出しごとに結果が確定するため、例外時のロールバックも通常トランザクションの範囲内で完結します。ただしFROM句で「複数行として扱う」ことはできません。
TABLE関数(非PIPELINED)
コレクション型を戻り値にするTABLE関数は、RETURN直前まで全行をPGAに蓄積してから一気に返します。呼び出し元は関数の全処理が終わるまで1行目を受け取れず、メモリは戻り行数×行サイズ分を丸ごと占有します。例外時は全体がロールバック対象になるので原子性は保ちやすいものの、大量件数ではORA-04030(PGA不足)のリスクが跳ね上がります。
パイプライン関数(PIPELINED)
PIPE ROW()を呼ぶたびに呼び出し元へ1行ずつ即時送信される方式です。メモリはPIPE直後に解放されるためほぼ一定量で安定し、呼び出し元は最初のPIPE ROWの時点で1行目を受け取れるので上流SQLと下流SQLが並行して進行します。ただし既にPIPEした行は呼び出し元に流れているため、関数の途中で例外が起きてもPIPE済み行はロールバックできません。冪等性を要する処理では「失敗時は全体を再実行する前提」で設計する必要があります。
PGA消費の実測比較(1000万件生成時)
1000万行を返す関数を3方式で比較した実測値です。PGAはセッションごとのワーク領域で、大量データ生成時はこのメモリ圧が性能と安定性を左右します。Oracle 19c / PGA_AGGREGATE_TARGET=4GB / 単純な行変換処理で計測した結果を示します。
通常TABLE関数:PGAピーク 約3.8GB/実行時間47秒(スワップ頻発)
全件をメモリに抱えるため4GBのPGA_AGGREGATE_TARGETを実質使い切り、OSのスワップ領域に溢れ始めて性能が崩壊します。ORA-04030「out of process memory」が出る一歩手前の危険水域。
パイプライン関数(ナイーブ実装):PGAピーク 約45MB/実行時間12秒
1件ずつPIPE ROWするだけの素朴な実装でも、メモリは約85分の1・実行時間は約4分の1まで激減します。特別なチューニングなしでもここまで差が出るのがパイプラインの底力。
パイプライン関数+BULK COLLECT:PGAピーク 約55MB/実行時間5.8秒
関数内でBULK COLLECT LIMIT 1000を挟むと、PL/SQL↔SQLエンジン間のコンテキストスイッチが劇的に減り約2倍の追加高速化。バッファ分わずかにメモリを使うだけで、実行時間は通常TABLE関数の約8分の1になります。
通常TABLE関数は全件PGA蓄積なので、行サイズ300バイト×1000万件=約3GBが関数の戻り値として一時的にメモリに載ります。パイプライン関数なら各PIPE ROW直後にバッファが呼出し元へ流され解放されるため、数MB〜数十MBで安定します。この差が大量データ処理の死活を分けます。
パイプライン関数の最小構文
実装の柱は3つです。① 戻り行の型(オブジェクト型/スカラー型)、② その型のコレクション型(TABLE OF)、③ 関数にPIPELINEDを付けPIPE ROW()で返す。最小形のテンプレを押さえておきます。
-- ① 行の型
CREATE OR REPLACE TYPE t_sales_row AS OBJECT(
sales_id NUMBER,
product_cd VARCHAR2(20),
amount NUMBER
);
/
-- ② コレクション型
CREATE OR REPLACE TYPE t_sales_tab IS TABLE OF t_sales_row;
/
-- ③ パイプライン関数
CREATE OR REPLACE FUNCTION f_sales_stream
RETURN t_sales_tab PIPELINED
AS
BEGIN
FOR rec IN (SELECT sales_id, product_cd, amount FROM raw_sales) LOOP
PIPE ROW(t_sales_row(rec.sales_id, rec.product_cd, rec.amount));
END LOOP;
RETURN; -- 値なしで終了(必須)
END;
/
-- FROM句で呼び出す
SELECT *
FROM TABLE(f_sales_stream())
WHERE amount > 10000;
CARDINALITYヒント|オプティマイザの8192行決め打ちを回避する
パイプライン関数の最大の落とし穴がこれです。オプティマイザはパイプライン関数の戻り行数を統計から推定できないため、デフォルトで8192行と決め打ちします。実際に1000万行返す関数でも、プランナーは「8192行」と思って計画を立てるため、外側のJOINでNESTED LOOPS+INDEX SCANが選ばれ大惨事になるケースが頻発します。
-- ❌ 何もしないとオプティマイザは8192行と決め打ち
SELECT /*+ LEADING(t f) USE_NL(f) */ *
FROM base_table t
JOIN TABLE(f_sales_stream()) f
ON (t.product_cd = f.product_cd);
-- → NESTED LOOPSで数時間コース
-- ✅ 方法1: CARDINALITYヒント(簡単)
SELECT /*+ CARDINALITY(f 10000000) */ *
FROM base_table t,
TABLE(f_sales_stream()) f
WHERE t.product_cd = f.product_cd;
-- ✅ 方法2: DYNAMIC_SAMPLINGで動的統計収集(2〜4の値推奨)
SELECT /*+ DYNAMIC_SAMPLING(f 4) */ *
FROM base_table t,
TABLE(f_sales_stream()) f
WHERE t.product_cd = f.product_cd;
-- ✅ 方法3: 関数内に OPTIMIZER_FOR_HINTING(12c以降)
-- または ASSOCIATE STATISTICS WITH FUNCTIONS でコストモデルを登録
ASSOCIATE STATISTICS WITH FUNCTIONS f_sales_stream
DEFAULT SELECTIVITY 10,
DEFAULT COST (1000000, 10, 0);
実務で最も使われるのはCARDINALITYヒントです。既知の想定件数を直接書き込めるので即効性があります。件数が変動するジョブではDYNAMIC_SAMPLING(4)をパイプライン関数呼び出しごとに付けるのが現実解です。ASSOCIATE STATISTICSは関数定義と一体化するので、同じ関数を何度も使うライブラリ的な位置づけで効果を発揮します。
並列パイプライン|PARALLEL_ENABLEで複数スレーブに分散する
CPUを複数使って1000万件処理を加速したいとき、PARALLEL_ENABLE+REF CURSOR入力の組み合わせが鍵です。通常のパイプライン関数は1スレーブでしか動きませんが、この形式にするとOracleが自動で行を分散し並列実行してくれます。
PARTITION BY と CLUSTER BY の使い分け
| 指定 | 行の分配方法 | 適したユースケース |
|---|---|---|
PARTITION BY ANY |
ランダムに各スレーブへ配分 | 行独立の変換処理(暗号化・マスキング・フォーマット変換) |
PARTITION BY HASH(key) |
キーのハッシュ値で分配 | 同じキーの行を同一スレーブで処理したい集計・LAG/LEAD処理 |
PARTITION BY RANGE(key) |
範囲で分配 | 時系列データの時間帯別処理 |
CLUSTER BY key |
分配後に同一スレーブ内でソート保証 | キー順で順序保証が必要な処理 |
CREATE OR REPLACE FUNCTION f_mask_sales(
p_cur IN SYS_REFCURSOR
)
RETURN t_sales_tab
PIPELINED
PARALLEL_ENABLE(PARTITION p_cur BY ANY) -- ← 並列化を許可
AS
rec raw_sales%ROWTYPE;
BEGIN
LOOP
FETCH p_cur INTO rec;
EXIT WHEN p_cur%NOTFOUND;
-- 行単位の変換処理(マスキング等)
PIPE ROW(t_sales_row(
rec.sales_id,
CASE WHEN rec.customer_id IS NOT NULL
THEN 'MASK-' || MOD(rec.customer_id, 10000)
ELSE rec.product_cd END,
rec.amount
));
END LOOP;
CLOSE p_cur;
RETURN;
END;
/
-- 呼び出し時に並列度4を指定
SELECT /*+ PARALLEL(f, 4) CARDINALITY(f 10000000) */ *
FROM TABLE(f_mask_sales(
CURSOR(SELECT /*+ PARALLEL(s, 4) */ * FROM raw_sales s)
)) f;
並列パイプラインの罠:①DML(INSERT/UPDATE/DELETE)を関数内で直接実行すると並列不可になる、②シーケンスのNEXTVALはスレーブごとに別番号になる(一意性は保たれるが順序は崩れる)、③パッケージ変数・グローバル変数はスレーブ間で共有されない、④例外発生時はスレーブの該当バッチのみ失敗し他スレーブは継続する。これらの挙動を踏まえて「行単位で独立」な処理だけ並列化してください。
BULK COLLECT+PIPE ROWで1.5〜3倍速くする
ナイーブなパイプライン関数は「1行フェッチ → PIPE ROW」を繰り返しますが、関数内部でBULK COLLECT LIMIT nしてからFOR i IN 1..n LOOP PIPE ROWする形に書き換えると、PL/SQL↔SQLエンジン間のコンテキストスイッチが劇的に減ります。実装は数行増えるだけで、大量件数では効果絶大です。
CREATE OR REPLACE FUNCTION f_sales_fast(
p_cur IN SYS_REFCURSOR
)
RETURN t_sales_tab PIPELINED
AS
TYPE t_rec IS TABLE OF raw_sales%ROWTYPE;
v_batch t_rec;
C_LIMIT CONSTANT PLS_INTEGER := 1000; -- 100〜5000の範囲が実用域
BEGIN
LOOP
FETCH p_cur BULK COLLECT INTO v_batch LIMIT C_LIMIT;
EXIT WHEN v_batch.COUNT = 0;
FOR i IN 1 .. v_batch.COUNT LOOP
PIPE ROW(t_sales_row(
v_batch(i).sales_id,
v_batch(i).product_cd,
v_batch(i).amount * 1.1 -- 例: 10%加算
));
END LOOP;
END LOOP;
CLOSE p_cur;
RETURN;
END;
/
LIMIT値と速度の関係(実測)
| LIMIT値 | 1000万件の処理時間 | PGA追加消費 | コメント |
|---|---|---|---|
| 1(ナイーブ) | 12.0秒 | – | 比較基準 |
| 100 | 7.2秒 | 約10MB | 手軽に効果を出せる |
| 1000 | 5.8秒 | 約55MB | 実務デフォルト推奨 |
| 10000 | 5.7秒 | 約450MB | これ以上増やしても高速化せずメモリ圧迫 |
※1行数十バイトの単純変換で計測。LIMITは100〜5000の範囲から選ぶのが実用的。
パイプライン関数のチェーン(連結再利用)
パイプライン関数は「入力REF CURSOR → 出力行」の形にすれば別のパイプライン関数の入力として再利用できます。ETL処理のステージ(抽出→変換→マスキング→集計)を独立したパイプライン関数で構築して、SQL一発でパイプラインを組み立てるモダンな設計パターンです。
-- ステージ1: 抽出(日付フィルタ)
CREATE OR REPLACE FUNCTION f_extract(p_cur SYS_REFCURSOR)
RETURN t_sales_tab PIPELINED PARALLEL_ENABLE(PARTITION p_cur BY ANY) AS
rec raw_sales%ROWTYPE;
BEGIN
LOOP FETCH p_cur INTO rec; EXIT WHEN p_cur%NOTFOUND;
IF rec.sales_date BETWEEN DATE '2026-01-01' AND DATE '2026-12-31' THEN
PIPE ROW(t_sales_row(rec.sales_id, rec.product_cd, rec.amount));
END IF;
END LOOP;
CLOSE p_cur; RETURN;
END;
/
-- ステージ2: 変換(税込計算)
CREATE OR REPLACE FUNCTION f_transform(p_cur SYS_REFCURSOR)
RETURN t_sales_tab PIPELINED PARALLEL_ENABLE(PARTITION p_cur BY ANY) AS
rec t_sales_row;
BEGIN
LOOP FETCH p_cur INTO rec.sales_id, rec.product_cd, rec.amount;
EXIT WHEN p_cur%NOTFOUND;
rec.amount := rec.amount * 1.10;
PIPE ROW(rec);
END LOOP;
CLOSE p_cur; RETURN;
END;
/
-- ステージ3: マスキング
CREATE OR REPLACE FUNCTION f_mask(p_cur SYS_REFCURSOR)
RETURN t_sales_tab PIPELINED PARALLEL_ENABLE(PARTITION p_cur BY ANY) AS
rec t_sales_row;
BEGIN
LOOP FETCH p_cur INTO rec.sales_id, rec.product_cd, rec.amount;
EXIT WHEN p_cur%NOTFOUND;
rec.product_cd := 'PRD-' || MOD(rec.sales_id, 1000);
PIPE ROW(rec);
END LOOP;
CLOSE p_cur; RETURN;
END;
/
-- 3段チェーンを1SQL で実行
INSERT /*+ APPEND */ INTO staged_sales
SELECT *
FROM TABLE(f_mask(
CURSOR(SELECT * FROM TABLE(f_transform(
CURSOR(SELECT * FROM TABLE(f_extract(
CURSOR(SELECT * FROM raw_sales)
)))
)))
));
3段チェーンでも中間結果は一切テーブルに書かれないのがポイント。メモリ上のストリームとして関数間を流れ、最終段のINSERTでだけディスク書き込みが発生します。ETLで「物理的中間表を作らない」設計はI/O削減・トランザクション短縮・失敗時の冪等性確保に直結します。
パイプライン関数 vs 純SQL(INSERT SELECT)|どちらを選ぶべきか
ETL目的なら純SQL(INSERT SELECT)が最速になるケースが多いという事実は意外と知られていません。パイプライン関数は「PL/SQLロジックをSQLから呼べる」のが本質であって、変換処理がSQL関数だけで書けるならSQL単体の方が速いのが普通です。判断基準を整理します。
| 状況 | 推奨 | 理由 |
|---|---|---|
| 変換処理がSQL関数・CASE式で書ける | 純SQL | PL/SQLのコンテキストスイッチがゼロ。一番速い |
| 変換ロジックが複雑でPL/SQL必須(状態保持・正規表現の段階処理・API呼び出し) | パイプライン関数 | ストリーミングでメモリ効率を保ちつつPL/SQLロジック適用 |
| 1セルの値を複数行に展開(CSV・XML・JSON) | パイプライン関数 | SQL単体では困難。1→N行の展開に最適 |
| 中間テーブルを作らず3段変換したい | パイプラインチェーン | 関数をREF CURSORで連結しONE SQLで済む |
| 並列DMLでブロック単位の割当で十分 | 純SQL + PARALLEL DML | 関数を挟むと並列が弱まることもある |
| 行独立な CPU 集約変換(暗号化・ハッシュ化) | パイプライン関数+PARALLEL_ENABLE | PARTITION BY ANYでCPUを使い切れる |
判断の要点:「そのロジックはSQL関数だけで書けるか?」を最初に問うこと。書けるなら純SQLが勝ちます。書けない・書くと可読性が壊滅的に落ちる、ならパイプライン関数の出番です。「なんとなくPL/SQLで書く」は性能を捨てていることが多いと覚えてください。
実務シナリオ8選|使い所と正解実装
現場で「これはパイプライン関数の出番」と判断できる典型ケースを整理します。
| # | シナリオ | 理由 | 実装のコツ |
|---|---|---|---|
| 1 | CSV 1セル展開 | PL/SQLでパース、FROM句で使いたい | 文字列型のTABLE OFで十分、カーディナリティは小 |
| 2 | XML/JSONパース | 複雑な構造を行化するのはSQLでは辛い | PARALLEL_ENABLEでスループット向上可 |
| 3 | 大量ETL(億件級) | 中間表なしで抽出→変換→ロード | 3段チェーン+CARDINALITY+APPEND |
| 4 | マスキング処理 | CPU律速だから並列化で時間短縮 | PARTITION BY ANYが最適 |
| 5 | 差分比較(2セット突合) | PL/SQLでビジネス差分ロジックを書きたい | 入力REF CURSOR 2つの別関数版を定義 |
| 6 | 外部API応答の行化 | UTL_HTTPの結果をテーブルライクに扱う | 並列化しない(APIレートリミット) |
| 7 | スプリット+集計(MapReduce風) | キー別集計を並列実行 | PARTITION BY HASH(key)必須 |
| 8 | ダミーデータ生成 | テスト用1000万行を動的生成 | ループカウンタ方式、LIMIT不要 |
本番で踏むパイプライン関数アンチパターン7選
実際のトラブル事例から厳選した「やってはいけない書き方」7つ。
| # | アンチパターン | 何が問題か | 正解 |
|---|---|---|---|
| 1 | CARDINALITYヒントなしで巨大結合 | 8192行決め打ちでNESTED LOOPS大惨事 | 件数を明示するかDYNAMIC_SAMPLING(4)付与 |
| 2 | パイプライン関数内で直接INSERT/UPDATE | 関数本来の目的と逸脱・並列化できない・例外処理困難 | 関数は行を返すだけにし外側SQLでINSERTする |
| 3 | LIMIT無しBULK COLLECT内蔵 | パイプラインなのに全件PGAで矛盾 | 必ずLIMIT 100〜5000を付与 |
| 4 | PIPE ROW後にROLLBACKを期待 | PIPE済みの行は呼出し元に流れており戻せない | 失敗時は関数全体をやり直す前提で設計 |
| 5 | 並列なのにパッケージ変数で状態保持 | スレーブ間で変数共有されず矛盾発生 | 状態はすべて引数で受け渡す純関数として書く |
| 6 | 戻り値の型にLOB・XMLTYPEを濫用 | パイプラインの軽さが打ち消される | 必要最小限の列に絞る。LOBは必要時にLOCATORだけ返す |
| 7 | 何でもパイプ化 | SQL単体で書けば速いものまで複雑化 | SQLで書けるならSQL。PL/SQLロジック必須な時だけパイプ化 |
パイプライン関数のデバッグ方法
パイプライン関数は「呼び出し元のSQLが1行ずつ取り出している最中」に関数内部のコードが実行されるため、普通のブレークポイントデバッグがしづらいです。実務で使える3つの手法を紹介します。
-- 手法1: DBMS_OUTPUTで進捗ログ(開発時)
CREATE OR REPLACE FUNCTION f_with_log(p_cur SYS_REFCURSOR)
RETURN t_sales_tab PIPELINED AS
rec raw_sales%ROWTYPE;
v_cnt NUMBER := 0;
BEGIN
LOOP FETCH p_cur INTO rec; EXIT WHEN p_cur%NOTFOUND;
v_cnt := v_cnt + 1;
IF MOD(v_cnt, 100000) = 0 THEN
DBMS_OUTPUT.PUT_LINE('Processed: ' || v_cnt || ' / ts=' || SYSTIMESTAMP);
END IF;
PIPE ROW(t_sales_row(rec.sales_id, rec.product_cd, rec.amount));
END LOOP;
CLOSE p_cur; RETURN;
END;
/
-- 手法2: 自己診断表にAUTONOMOUS_TRANSACTIONで書き込む(本番可)
CREATE TABLE pipeline_diag(
ts TIMESTAMP,
fn_nm VARCHAR2(64),
phase VARCHAR2(32),
cnt NUMBER,
info VARCHAR2(4000)
);
CREATE OR REPLACE PROCEDURE log_diag(
p_fn VARCHAR2, p_phase VARCHAR2, p_cnt NUMBER, p_info VARCHAR2
) AS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO pipeline_diag VALUES(SYSTIMESTAMP, p_fn, p_phase, p_cnt, p_info);
COMMIT;
END;
/
-- 手法3: SQLトレース(10046イベント)
ALTER SESSION SET EVENTS '10046 trace name context forever, level 12';
-- ここでパイプライン関数を呼ぶSQLを実行
ALTER SESSION SET EVENTS '10046 trace name context off';
-- トレースファイルを tkprof で整形して分析
よくある質問
TABLE()演算子でFROM句に置けますが、行の返し方が決定的に異なります。通常TABLE関数はRETURN コレクションで「全行を揃えてから返す」ため全件PGAに蓄積します。パイプライン関数(PIPELINED指定)はPIPE ROW()で1行ずつ即送信するためメモリ一定です。1000件以下ならTABLE関数でも実用的、1万件超ならパイプライン関数を選ぶべきです。PRAGMA AUTONOMOUS_TRANSACTIONを使う別プロシージャに切り出す設計が現実解です。DETERMINISTICを付けるとオプティマイザが結果キャッシュを有効化して再呼び出しを削減できます。ただし実際にはパイプライン関数にDETERMINISTICは効きにくい(FROM句呼び出しでは行数が変わる可能性を前提として計画される)ので、付けるデメリットもありません。冪等性のヒントとしてドキュメント目的で付けるのは良い習慣です。RESULT_CACHEを付けられません(12c時点の仕様)。結果キャッシュしたい場合は「パイプライン関数の外側で通常関数にラップし、その通常関数にRESULT_CACHEを付ける」パターンを使います。ただし大量行を返すパイプラインに結果キャッシュを適用する用途自体がまれなので、実務ではあまり発生しない論点です。ORDER BYを指定すれば当然効きます。関数の内側ではCLUSTER BY(PARALLEL_ENABLE指定時)でスレーブ内のソート順を保証できます。並列化しない関数でループ内のFETCHにORDER BYを書けばその順でPIPE ROWされるため、外側でもその順序が見えます(ただし外側の実行計画次第で順序は保証されないので確定したいなら外側ORDER BYを明示するのが安全)。PIPE ROW(NULL)することもできます。呼出し側ではCOLUMN_VALUEがNULLの行として見えるため、WHERE COLUMN_VALUE IS NOT NULLでフィルタするかそもそも返さない設計が望ましいです。空トークン問題(CSVで連続区切り文字)のときなどに意図して活用します。TYPE ... IS TABLE OF ... INDEX BY ...のようなPL/SQL専用の連想配列は使えません。パイプライン関数は「SQLエンジンから見える型」で宣言する必要があると覚えてください。TABLE(pkg.f_stream())のように呼び出せます。戻り型(オブジェクト型・コレクション型)はSQLレベルのTYPEとしてスキーマ内にCREATEしておく必要があります(パッケージ内のTYPEはSQLから見えないため)。ALTER SESSION ENABLE PARALLEL DML;をセッションで有効化、②パイプライン関数はPARALLEL_ENABLE+REF CURSOR入力で定義、③INSERT SELECT時に/*+ APPEND PARALLEL(t, n) */ヒント付与、④元テーブル側にも/*+ PARALLEL(s, n) */、の4つを揃えます。加えてCARDINALITY ヒントも忘れずに。これでスレーブが複数走り真のパラレルETLになります。関連記事で知識を深める
パイプライン関数の理解を深めるために、同カテゴリの関連記事も合わせてどうぞ。
- 【Oracle】パイプライン表関数完全ガイド|PIPELINED・PIPE ROW によるストリーミング処理・並列実行まで解説(基本構文と仕組みをじっくり学びたい方へ)
- 【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方(パイプライン内部で使うバルクフェッチの基礎)
- 【PL/SQL】MERGE文でUPSERTを高速・安全に実装|IF方式8倍差・LOG ERRORS・ORA-30926回避・実務10シナリオ(ETLの最終INSERT部分をMERGEに置き換える設計)
- 【PL/SQL】カーソルでSQLクエリの複数の行を1行ずつ処理する方法(パイプライン関数内で使うカーソル操作の基礎)
- 【PL/SQL】カーソルFORループ vs 明示的カーソル完全ガイド(関数内のループ選択の参考)
- 【Oracle】自律型トランザクション(AUTONOMOUS_TRANSACTION)完全ガイド(パイプライン関数のデバッグログ書き込み)
- 【Oracle】BULK COLLECT+FORALL完全ガイド|PL/SQLバルク処理を極める(パイプラインと併用する場面の判断に)
- 【PL/SQL】例外処理の書き方と使い方(パイプライン関数の例外設計)
- 【PL/SQL】ストアドプロシージャとファンクションの違いと作り方(関数設計の基礎)
- 【Oracle】パラレル実行完全ガイド(PARALLEL_ENABLEを本気で使うための基礎)
まとめ|パイプライン関数を大量データ処理の決め手にする
パイプライン関数は「FROM句で呼べるPL/SQL関数」という表面的な定義を超えて、大量データの変換を低メモリ・高スループット・並列で実現するためのエンジンです。本記事の要点は以下の7つです。
- 通常TABLE関数は全件PGA蓄積。1万件超はパイプライン一択
- CARDINALITYヒント未付与は実行計画大惨事の温床
- PARALLEL_ENABLE+REF CURSOR入力で真の並列ETLが可能
- BULK COLLECT LIMIT 100〜5000 を内蔵するだけで1.5〜3倍高速化
- 関数を入力REF CURSORで接続し3段チェーンで中間表なしETL
- DML直接実行・LIMIT省略・状態変数依存は並列時の爆弾。アンチパターン回避を徹底
- デバッグは自己診断表+AUTONOMOUS_TRANSACTIONが本番でも使える定石
SQL単体で書ける処理までパイプライン関数にする必要はありませんが、複雑なPL/SQLロジックを大量データに適用する瞬間、この武器の真価が発揮されます。実務で既に通常TABLE関数を使っているバッチがあるなら、まずはPIPELINEDとPIPE ROWに置き換え、CARDINALITYヒントを付与するだけで劇的な改善が期待できます。

