JavaScriptが無効になっています。
この状態では一部の情報が表示されず、すべての機能を利用することができません。
大容量の CSV 出力を10倍高速にするには…
SQL*Plus から SPOOL を使用して CSV を出力するよりも10倍くらい高速できそうな方法を試行してみる。
PL/SQL において以下の手法を使って CSV 出力処理を高速化する。
PL/SQL 上でマルチタスクの CSV 出力処理を行なう
PL/SQL においてマルチタスクを実現するために CSV 作成・出力処理を複数のチャンク(塊り)に分け、それぞれにジョブ・プロセスを生成して並列実行処理を行なう。
PL/SQL をマシン語に変換する(Native Compile)
Oracle 11g から面倒な設定や別途必要だった C コンパイラも要らずに初期化パラメータで PL/SQL コードをネイティブ・コードにして実行することができる。(※1)
今回の UNLOAD 処理は基本的に SQL の実行とレコードのフェッチ処理しかないのでファイル出力系統(UTL_FILE)のネイティブ化による高速化くらいにしかメリットは無さそう。
(※1) 組み込み UTL_FILE パッケージを Native 化するには DB全体レベルの再コンパイル作業(アップグレード)が必要。
ダイレクト・パス・リードを行なう
パラレル・クエリーオプションがあれば、パラレルクエリーを行なうことでバッファ・キャッシュを経由しないアクセスが可能となる。
この方法で処理をパラレル化すると後続タスクにおいてキャッシュの恩恵がなくなる。この場合、チャンクにはパーティショニングを使ったタスク分割が効果的になると思われる。 SELECT ... FROM table_name PARTITION partition_name
スタンダードでも使用できるように、ダイレクト・パス・リードを制御するヒント句が欲しいところ。
CSV を高速に生成するために必要なプログラム
PARALLEL_EXECUTE パッケージ・モジュール
ジョブを使用してマルチタスク処理させるためのフロント・モジュール
このモジュールから UNLOAD のプロシージャを複数個、並列化して呼び出す。
仕様と制限
物理 ROWID を使用してデータの格納ブロックによってチャンク分割を行なっている。
論理 ROWID を使用する 索引構成表 、外部表 、および、ビュー などには使用できないため、別の分割方法を組み込んでカスタマイズする必要がある。NTILE 分析関数などは簡単なバケット分割方法であるがパフォーマンスを上げるひと工夫が必要だろう。
BIGFILE 表領域 に対してもソースの変更作業が必要となる(※2)
DBMS_SCHEDULER 組み込みパッケージを使用しているため、Oracle 10g 以上の環境が必要。
ファイルの結合処理は含まれていない:ファイルを1つに結合したい場合には、呼び出しシェルで行なう、SQL*Plus HOST コマンド を実行する、または、DBMS_SCHEDULER から OS のコマンドを実行し結合する方法などがある。
(※2) BIGFILE 表領域 の場合には DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) ⇒ DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID, 'BIGFILE') に変更する。
並列実行がうまく動作しない場合には 120行 あたりに
debug 用の並列化させない&エラー出力する実行コードが含まれるのでデバッグに使ってください。
-- for debug (シリアルで実行される&エラー発生時に例外が発生する&JOBオートドロップはされない)
-- DBMS_SCHEDULER.RUN_JOB(job_names);
-- DROP_TASK(job_names);
-- for release(パラレルで実行される&エラー発生時してもキャッチできない)
DBMS_SCHEDULER.ENABLE(job_names);
-- 起動したジョブの終了待ち&出力件数取得(タイムアウト6時間)
FOR i in 1..parallel_level LOOP
status := DBMS_PIPE.RECEIVE_MESSAGE(pipe_name, 21600);
DBMS_PIPE.PURGE(pipe_name);
略
UNLOAD パッケージ・モジュール
CSV のデータを生成と書き出しを行なうライブラリ
CSV を生成するための標準機能は用意されていない。パッケージの詳細については 汎用 CSV 出力プロシージャ を参照
実行に必要な権限と作業
ファイルを出力するための ディレクトリ・オブジェクトの作成(例:DATA_UNLOAD_DIR)
DBMS_PIPE 組み込みパッケージを実行する権限の付与(親プロセスと生成プロセスとの同期に使用)
JOB を作成する CREATE JOB システム権限の付与
-- コマンドを実行するユーザー (rivus)
SQL> conn rivus/passwd
SQL> CREATE DIRECTORY DATA_UNLOAD_DIR AS 'D:\CSV' ;
-- └─ DBのデータファイルと異なる HDD がよい
SQL> conn / as sysdba
SQL> GRANT EXECUTE ON DBMS_PIPE TO rivus
SQL> GRANT CREATE JOB TO rivus;
GRANT EXECUTE ON DBMS_PIPE TO username するときに他のセッションで DBMS_PIPE を使用し続けているものがいるとハングアップしているように見えることがあるらしいので要注意。
テーブル名 employees を 並列度4で CSV 出力する例
employees テーブル上の 1000万レコードを CSV 出力する例
ファイルはディレクトリ・オブジェクト 'DATA_UNLOAD_DIR' 以下に出力される。
SQL> alter session set nls_date_format = 'yyyy-mm-dd hh24:mi:ss' ;
SQL> alter session set nls_timestamp_format = 'yyyy-mm-dd hh24:mi:ssxff3' ;
SQL> set serveroutput on
SQL> execute parallel_execute.user_task('employees' , 'DATA_UNLOAD_DIR' , 4);
-- ↑ ↑ ↑
-- テーブル名 ディレクトリOBJ名 並列度
…
export_employees_1.csv=2499883
export_employees_2.csv=2499983
export_employees_4.csv=2500027
export_employees_3.csv=2500107
PL/SQLプロシージャが正常に完了しました。
経過: 00:02:21.40
SQL>
代表的なテーブルでの1千万レコードの所要時間比較
Windows XP + Oracle 11g R2 (32bit / DBCA 汎用:パーティショニング未使用)
CPU: Corei7(Nehalem 世代 4コア) / Mem: 4GB (DB:512MB) / HDD: 7500 rpm / SSD: 250 MB/s (read)
sqlplus + spool にて employees 1000万件の出力 約 21分
parallel_execute.user_task による 4並列にて employees 1000万件の出力 約 2分30秒
(※) SQL*Plus + spool の処理と比べて約10倍になっているが、サーバーの CPU のスペックおよびデータベース・データファイルのドライブ、出力先のドライブのスペックによって相当変動する。
ドライブによる比較検証(未実施 '-' )
データファイルのデバイスを SDD、HDD(SATA)、出力デバイスを RAM ディスク、HDD(データファイルの HDD とは違う)の組み合わせにおいての実験。
※ 試験環境が家庭向けデスクトップPCなので UNIX 系で RAID構成の DBサーバ専用機では異なる結果が出る可能性があります。
タスク 並列度 データファイル・メディア to CSV 出力先メディア ファイルサイズ SSD to RAM HDD to RAM SSD to HDD HDD to HDD SQL*Plus(ローカル接続) └ linesize 32767 ×1 > 6時間 > 6時間 > 6時間 > 6時間 933MB SQL*Plus └ linesize 1000 ×1 19分50秒 21分00秒 19分24秒 19分28秒 SQL*Plus └ linesize 200 ×1 10分30秒 - - - SQL*Plus └ linesize 200 , arraysize 100 ×1 9分43秒 - - 11分43秒 UNLOAD ×1 9分27秒 9分29秒 9分40秒 11分05秒 933MB UNLOAD ×2 5分50秒 5分52秒 5分43秒 9分11秒 466MB×2 UNLOAD ×4 2分33秒 2分30秒 7分38秒 10分03秒 233MB×4 UNLOAD ×8 2分44秒 2分45秒 10分54秒 11分06秒 115MB×2 116MB×2 117MB×4 UNLOAD (NATIVE コンパイル) └ DB 全体も NATIVE で再コンパイル ×1 9分12秒 - - 9分41秒 933MB UNLOAD (同上) ×2 5分40秒 - - 7分34秒 466MB×2 UNLOAD (同上) ×4 2分29秒 - - 9分37秒 233MB×4 UNLOAD (同上) ×8 2分42秒 - - 10分26秒 115MB×2 116MB×2 117MB×4 UNLOAD (NATIVE コンパイル) └ さらにダイレクト・パス・リード(※3) ×2 - - - 6分03秒 466MB×2 UNLOAD (同上) ×4 1分46秒 1分45秒 - - 233MB×4
(※3) 副次的に「ダイレクト・パス・リード」を行なわせるために強制的に「パラレル・クエリー」モードにする。ソースコード 75行目の ALTER SESSION FORCE PARALLEL QUERY PARALLEL 2; を有効にする。(要:エンタープライズ+パラレルクエリー・オプション)
検証結果
SQL*Plus はバージョン 11g R2 でも依然として1レコードのサイズが大きなデータを(TRIM 付きで)取り扱う場合において、効率の良いとは言えないアプリケーションであることに変わりがない。
SQL*Plus と SPOOL で効率的に CSV 出力するにはテーブル単位に最大レコード長を調べ、個別にチューニングしなければならない。
SQL*Plus のレコードのフェッチに関しては(かなりの昔から比べて)効率は改善されているようで ARRAYSIZE によるチューニング手法はあまり効果がない。
入・出力デバイスが HDD となった場合にそれぞれ単一ドライブのせいか性能の頭打ちが激しい。(SCSI の場合には改善の可能性があるかもしれない)
HDD に複製するコストを含めても一度 RAM ディスクに書き出すと、飛躍的にパフォーマンスが向上する。
高速な RAID コントローラと高速なストレージ群をもった製品を導入することも、かなりパフォーマンスを向上すると期待できる。
HDD 書き込みに対しては同期回数を減らすことができそうな RAW によるファイル出力に変更すると改善できるかもしれない。
employees テーブルは HR サンプルスキーマのもの
データは制約をはずしてサンプルスキーマのデータの複製の繰り返し。
CREATE TABLE RIVUS.EMPLOYEES
(
EMPLOYEE_ID NUMBER(6,0),
FIRST_NAME VARCHAR2(20),
LAST_NAME VARCHAR2(25) NOT NULL,
EMAIL VARCHAR2(25) NOT NULL,
PHONE_NUMBER VARCHAR2(20),
HIRE_DATE DATE NOT NULL,
JOB_ID VARCHAR2(10) NOT NULL,
SALARY NUMBER(8,2),
COMMISSION_PCT NUMBER(2,2),
MANAGER_ID NUMBER(6,0),
DEPARTMENT_ID NUMBER(4,0)
)
PARALLEL_EXECUTE パッケージ
ソース抜粋:パッケージ宣言部のみなのでコピーしても動きません。内容が古い場合もあります。
CREATE OR
REPLACE PACKAGE PARALLEL_EXECUTE
IS
/*********************************************************************/
-- ファンクション名 USER_TASK
-- パラメータ :(I ) export_table CSV出力するテーブル
-- :(I ) dir_name ディレクトリ・オブジェクト名
-- :(I ) parallel_level 並列度
-- 戻り値 :なし
-- Copyright :rivus.jp
/*********************************************************************/
-- constatnt
--
--
PROCEDURE USER_TASK(
export_table IN VARCHAR2,
dir_name IN VARCHAR2 := 'DATA_PUMP_DIR' ,
parallel_level IN POSITIVEN := 4
);
PROCEDURE USER_TASK_CHILD(
child_no IN NUMBER,
parallel_level IN NUMBER,
table_name IN VARCHAR2,
dir_name IN VARCHAR2,
pipe_name IN VARCHAR2 := NULL
);
END;
/
関連事項