web-dev-qa-db-ja.com

Oracleで10分以内に1,000万のクエリを挿入しますか?

私はファイルローダープログラムに取り組んでいます。

このプログラムの目的は、入力ファイルを取得し、そのデータに対していくつかの変換を行ってから、そのデータをOracleのデータベースにアップロードすることです。

私が直面している問題は、Oracleへの非常に大きな入力データの挿入を最適化する必要があることです。

テーブルにデータをアップロードしています。ABCとしましょう。

Oracleが提供するOCIライブラリをC++プログラムで使用しています。具体的には、マルチスレッド化とOracleへのロードにOCI接続プールを使用しています。 ( http://docs.Oracle.com/cd/B28359_01/appdev.111/b28395/oci09adv.htm

以下は、テーブルABCの作成に使用されたDDLステートメントです–

CREATE TABLE ABC(
   seq_no         NUMBER NOT NULL,
   ssm_id         VARCHAR2(9)  NOT NULL,
   invocation_id  VARCHAR2(100)  NOT NULL,
   analytic_id    VARCHAR2(100) NOT NULL,
   analytic_value NUMBER NOT NULL,
   override       VARCHAR2(1)  DEFAULT  'N'   NOT NULL,
   update_source  VARCHAR2(255) NOT NULL,
   last_chg_user  CHAR(10)  DEFAULT  USER NOT NULL,
   last_chg_date  TIMESTAMP(3) DEFAULT  SYSTIMESTAMP NOT NULL
);

CREATE UNIQUE INDEX ABC_indx ON ABC(seq_no, ssm_id, invocation_id, analytic_id);
/
CREATE SEQUENCE ABC_seq;
/

CREATE OR REPLACE TRIGGER ABC_insert
BEFORE INSERT ON ABC
FOR EACH ROW
BEGIN
SELECT ABC_seq.nextval INTO :new.seq_no FROM DUAL;
END;

現在、次のクエリパターンを使用してデータをデータベースにアップロードしています。 OCI接続プールのさまざまなスレッドを介して500クエリのバッチでデータを送信しています。

使用したSQL挿入クエリのサンプル-

insert into ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value,
override, update_source)
select 'c','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'a','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'b','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'c','g',NULL, 'test', 123 , 'N', 'asdf' from dual

上記のクエリに対するOracleによる実行プラン-

-----------------------------------------------------------------------------
| Id  | Operation                | Name|Rows| Cost (%CPU) | Time     |
-----------------------------------------------------------------------------
|   0 | INSERT STATEMENT         |     | 4  |     8   (0) | 00:00:01 |
|   1 |  LOAD TABLE CONVENTIONAL | ABC |    |             |          |
|   2 |   UNION-ALL              |     |    |             |          |
|   3 |    FAST DUAL             |     | 1  |     2   (0) | 00:00:01 |
|   4 |    FAST DUAL             |     | 1  |     2   (0) | 00:00:01 |
|   5 |    FAST DUAL             |     | 1  |     2   (0) | 00:00:01 |
|   6 |    FAST DUAL             |     | 1  |     2   (0) | 00:00:01 |

100万行をロードするプログラムの実行時間-

Batch Size = 500
Number of threads - Execution Time -
10                  4:19
20                  1:58
30                  1:17
40                  1:34
45                  2:06
50                  1:21
60                  1:24
70                  1:41
80                  1:43
90                  2:17
100                 2:06


Average Run Time = 1:57    (Roughly 2 minutes)

この時間をさらに最適化して短縮する必要があります。私が直面している問題は、アップロード用に1,000万行を配置したときです。

1,000万の平均実行時間は=21分でした。

(私の目標はこの時間を10分未満に短縮することです)

だから私も次の手順を試しました-

[1]seq_noに基づいてテーブルABCを分割しました。使用済み30パーティション100万行でテスト済み-パフォーマンスは非常に悪かった。パーティション化されていないテーブルのほぼ4倍

[2]last_chg_dateに基づくテーブルABCの別のパーティション化。使用済み30パーティション

2.a)100万行でテスト-パフォーマンスはパーティション化されていないテーブルとほぼ同等でした。ほとんど違いがなかったため、考慮されませんでした。

2.b)再度同じことを1,000万行でテストしました。パフォーマンスは、パーティション化されていないテーブルとほぼ同等でした。目立った違いはありません。

以下は、パーティショニングを実現するために使用されたDDLコマンドです-

CREATE TABLESPACE ts1 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts2 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts3 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts4 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts5 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts6 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts7 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts8 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts9 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts10 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts11 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts12 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts13 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts14 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts15 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts16 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts17 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts18 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts19 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts20 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts21 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts22 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts23 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts24 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts25 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts26 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts27 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts28 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts29 DATAFILE AUTOEXTEND ON;
CREATE TABLESPACE ts30 DATAFILE AUTOEXTEND ON;

CREATE TABLE ABC(
   seq_no           NUMBER NOT NULL,
   ssm_id           VARCHAR2(9)  NOT NULL,
   invocation_id    VARCHAR2(100)  NOT NULL,
   calc_id          VARCHAR2(100) NULL,
   analytic_id      VARCHAR2(100) NOT NULL,
   ANALYTIC_VALUE   NUMBER NOT NULL,
   override         VARCHAR2(1)  DEFAULT  'N'   NOT NULL,
   update_source    VARCHAR2(255) NOT NULL,
   last_chg_user    CHAR(10)  DEFAULT  USER NOT NULL,
   last_chg_date    TIMESTAMP(3) DEFAULT  SYSTIMESTAMP NOT NULL
)
PARTITION BY HASH(last_chg_date)
PARTITIONS 30
STORE IN (ts1, ts2, ts3, ts4, ts5, ts6, ts7, ts8, ts9, ts10, ts11, ts12, ts13,
ts14, ts15, ts16, ts17, ts18, ts19, ts20, ts21, ts22, ts23, ts24, ts25, ts26,
ts27, ts28, ts29, ts30);

OCIを使用してスレッド関数(C++で記述)で使用しているコード-

void OracleLoader::bulkInsertThread(std::vector<std::string> const & statements)
{

    try
    {
        INFO("Oracle_LOADER_THREAD","Entered Thread = %1%", m_env);
        string useOraUsr = "some_user";
        string useOraPwd = "some_password";

        int user_name_len   = useOraUsr.length();
        int passwd_name_len = useOraPwd.length();

        text* username((text*)useOraUsr.c_str());
        text* password((text*)useOraPwd.c_str());


        if(! m_env)
        {
            CreateOraEnvAndConnect();
        }
        OCISvcCtx *m_svc = (OCISvcCtx *) 0;
        OCIStmt *m_stm = (OCIStmt *)0;

        checkerr(m_err,OCILogon2(m_env,
                                 m_err,
                                 &m_svc,
                                 (CONST OraText *)username,
                                 user_name_len,
                                 (CONST OraText *)password,
                                 passwd_name_len,
                                 (CONST OraText *)poolName,
                                 poolNameLen,
                                 OCI_CPOOL));

        OCIHandleAlloc(m_env, (dvoid **)&m_stm, OCI_HTYPE_STMT, (size_t)0, (dvoid **)0);

////////// Execution Queries in the format of - /////////////////
//        insert into pm_own.sec_analytics (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value, override, update_source)
//        select 'c','b',NULL, 'test', 123 , 'N', 'asdf' from dual
//        union all select 'a','b',NULL, 'test', 123 , 'N', 'asdf' from dual
//        union all select 'b','b',NULL, 'test', 123 , 'N', 'asdf' from dual
//        union all select 'c','g',NULL, 'test', 123 , 'N', 'asdf' from dual
//////////////////////////////////////////////////////////////////

        size_t startOffset = 0;
        const int batch_size = PCSecAnalyticsContext::instance().getBatchCount();
        while (startOffset < statements.size())
        {
            int remaining = (startOffset + batch_size < statements.size() ) ? batch_size : (statements.size() - startOffset );
            // Break the query vector to meet the batch size
            std::vector<std::string> items(statements.begin() + startOffset,
                                           statements.begin() + startOffset + remaining);

            //! Preparing the Query
            std::string insert_query = "insert into ";
            insert_query += Context::instance().getUpdateTable();
            insert_query += " (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value, override, update_source)\n";

            std::vector<std::string>::const_iterator i3 = items.begin();
            insert_query += *i3 ;

            for( i3 = items.begin() + 1; i3 != items.end(); ++i3)
                insert_query += "union " + *i3 ;
            // Preparing the Statement and Then Executing it in the next step
            text *txtQuery((text *)(insert_query).c_str());
            checkerr(m_err, OCIStmtPrepare (m_stm, m_err, txtQuery, strlen((char *)txtQuery), OCI_NTV_SYNTAX, OCI_DEFAULT));
            checkerr(m_err, OCIStmtExecute (m_svc, m_stm, m_err, (ub4)1, (ub4)0, (OCISnapshot *)0, (OCISnapshot *)0, OCI_DEFAULT ));

            startOffset += batch_size;
        }

        // Here is the commit statement. I am committing at the end of each thread.
        checkerr(m_err, OCITransCommit(m_svc,m_err,(ub4)0));

        checkerr(m_err, OCIHandleFree((dvoid *) m_stm, OCI_HTYPE_STMT));
        checkerr(m_err, OCILogoff(m_svc, m_err));

        INFO("Oracle_LOADER_THREAD","Thread Complete. Leaving Thread.");
    }

    catch(AnException &ex)
    {
        ERROR("Oracle_LOADER_THREAD", "Oracle query failed with : %1%", std::string(ex.what()));
        throw AnException(string("Oracle query failed with : ") + ex.what());
    }
}

投稿への回答中に、INSERT QUERYを最適化するためのいくつかの方法が提案されました。さまざまなINSERTクエリのテスト中に発見した次の理由により、プログラムでQUERY Iを選択して使用しました。私に提案されたSQLクエリの実行時-QUERY I-

insert into ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value,
override, update_source)
select 'c','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'a','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'b','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'c','g',NULL, 'test', 123 , 'N', 'asdf' from dual

OracleによるクエリIの実行プラン-

--------------------------------------------------------------------------
| Id  | Operation                | Name| Rows | Cost (%CPU)   | Time     |
--------------------------------------------------------------------------
|   0 | INSERT STATEMENT         |     |  4   | 8   (0)       | 00:00:01 |
|   1 |  LOAD TABLE CONVENTIONAL | ABC |      |               |          |
|   2 |   UNION-ALL              |     |      |               |          |
|   3 |    FAST DUAL             |     |  1   | 2   (0)       | 00:00:01 |
|   4 |    FAST DUAL             |     |  1   | 2   (0)       | 00:00:01 |
|   5 |    FAST DUAL             |     |  1   | 2   (0)       | 00:00:01 |
|   6 |    FAST DUAL             |     |  1   | 2   (0)       | 00:00:01 |

クエリII-

insert all
into ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value,
override, update_source) values ('c','b',NULL, 'test', 123 , 'N', 'asdf')
into ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value,
override, update_source) values ('c','e',NULL, 'test', 123 , 'N', 'asdf')
into ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value,
override, update_source) values ('c','r',NULL, 'test', 123 , 'N', 'asdf')
into ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value,
override, update_source) values ('c','t',NULL, 'test', 123 , 'N', 'asdf')
select 1 from dual

Oracle for Query IIの実行プラン-

-----------------------------------------------------------------------------
| Id  | Operation           | Name| Rows  | Cost (%CPU)   | Time     |
-----------------------------------------------------------------------------
|   0 | INSERT STATEMENT    |     | 1     |     2   (0)   | 00:00:01 |
|   1 |  MULTI-TABLE INSERT |     |       |               |          |
|   2 |   FAST DUAL         |     | 1     |     2   (0)   | 00:00:01 |
|   3 |   INTO              | ABC |       |               |          |
|   4 |   INTO              | ABC |       |               |          |
|   5 |   INTO              | ABC |       |               |          |
|   6 |   INTO              | ABC |       |               |          |

実験によると、クエリIの方が高速です

ここでは、Oracle SQL Developerの両方でテストしたほか、C++プログラム(FILELOADER)からも挿入クエリを送信しました。

それについてさらに読んだところ、実行プランによって示されるコストは、クエリがそれ自体を処理するために使用するCPUの数であることがわかりました。 これは、Oracleが最初のクエリを処理するためにより多くのCPUを使用することを示しており、そのため、コストは= 8になります。

アプリケーションで同じ挿入パターンを使用しても、パフォーマンスが約1.5倍優れていることがわかりました。

パフォーマンスをさらに向上させる方法についての洞察が必要です。私が試したすべてのことを、私の質問に要約しました。関連するものを見つけたり発見したりした場合は、この質問に追加します。

私の目標は、1000万クエリのアップロード時間を10分未満にすることです。

13
badola

他の人がこれについて言及していることは知っていますが、聞きたくないのですが、 SQL * Loader または 外部テーブル を使用してください。ほぼ同じ幅のテーブルの平均読み込み時間は、10mを少し超える行で12.57 です。これらのユーティリティは、データベースにデータをすばやくロードするように明示的に設計されており、非常に優れています。入力ファイルの形式によっては、追加の時間ペナルティが発生する場合がありますが、かなりの数のオプションがあり、ロードする前にファイルを変更する必要はほとんどありません。

これを実行したくない場合は、ハードウェアをまだアップグレードする必要はありません。これをすばやくロードする際に考えられるすべての障害を取り除く必要があります。それらを列挙するには、以下を削除します。

  1. インデックス
  2. 引き金
  3. シーケンス
  4. パーティション

これらすべてを使用すると、データベースにさらに多くの作業を実行する義務があり、これをトランザクションで実行しているため、データベースを最大限に活用していません。

データを別のテーブル、たとえばABC_LOADにロードします。データが完全にロードされたら、single INSERTステートメントをABCに実行します。

insert into abc
select abc_seq.nextval, a.*
  from abc_load a

これを行うとき(そしてそうでない場合でも)、シーケンスキャッシュサイズが正しいことを確認してください。 引用する

アプリケーションがシーケンスキャッシュ内のシーケンスにアクセスすると、シーケンス番号がすばやく読み取られます。ただし、アプリケーションがキャッシュにないシーケンスにアクセスする場合は、シーケンス番号を使用する前に、シーケンスをディスクからキャッシュに読み取る必要があります。

アプリケーションが同時に多くのシーケンスを使用する場合、シーケンスキャッシュはすべてのシーケンスを保持するのに十分な大きさではない可能性があります。この場合、シーケンス番号へのアクセスにはディスクの読み取りが必要になることがよくあります。すべてのシーケンスにすばやくアクセスするには、アプリケーションで同時に使用されるすべてのシーケンスを保持するのに十分なエントリがキャッシュにあることを確認してください。

つまり、このシーケンスを使用してそれぞれ500レコードを同時に書き込む10個のスレッドがある場合、5,000のキャッシュサイズが必要になります。 ALTER SEQUENCE ドキュメントには、これを変更する方法が記載されています。

alter sequence abc_seq cache 5000

私の提案に従うと、キャッシュサイズは約10.5mになります。

APPENDヒント(Oracle Baseも参照) ;の使用を検討してください。これは、データを配置するスペースを探すのではなく、テーブルの最後に直接データを追加するダイレクトパス挿入を使用するようにOracleに指示します。テーブルにインデックスがある場合はこれを使用できませんが、ABC_LOADで使用できます。

insert /*+ append */ into ABC (SSM_ID, invocation_id , calc_id, ... )
select 'c','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'a','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'b','b',NULL, 'test', 123 , 'N', 'asdf' from dual
union all select 'c','g',NULL, 'test', 123 , 'N', 'asdf' from dual

APPENDヒントを使用する場合; ABCに挿入した後、 [〜#〜] truncate [〜#〜]ABC_LOADを追加します。そうしないと、このテーブルは無期限に大きくなります。それまでにテーブルの使用を終了するので、これは安全なはずです。

使用しているバージョンやエディション、またはOracleについては言及していません。使用できる特別なトリックがいくつかあります。

  • Oracle 12c

    このバージョンは ID列 をサポートします。シーケンスを完全に取り除くことができます。

    CREATE TABLE ABC(
       seq_no         NUMBER GENERATED AS IDENTITY (increment by 5000)
    
  • Oracle 11g r2

    トリガーを保持する場合;シーケンス値を直接割り当てることができます。

    :new.seq_no := ABC_seq.nextval;
    
  • Oracle Enterprise Edition

    Oracle Enterpriseを使用している場合は、 PARALLELヒント を使用してABC_LOADからのINSERTを高速化できます。

    insert /*+ parallel */ into abc
    select abc_seq.nextval, a.*
      from abc_load a
    

    これにより、独自の問題(並列プロセスが多すぎるなど)が発生する可能性があるため、テストしてください。 might小さなバッチ挿入には役立ちますが、どのスレッドが何を処理するかを計算する時間が失われる可能性は低くなります。


tl; dr

データベースに付属のユーティリティを使用してください。

それらを使用できない場合は、挿入を遅くする可能性のあるすべてのものを取り除き、一括して実行してください。これは、データベースが得意とすることだからです。

8
Ben

テキストファイルがある場合は、ダイレクトパスを使用して SQL LOADER を試してください。それは本当に高速で、この種の大量のデータロード用に設計されています。パフォーマンスを向上させることができるこれ options を見てください。

ETLの副次的な利点として、クリアテキストのファイルは10 ^ 7の挿入よりも小さく、監査が容易になります。

何らかの変換を行う必要がある場合は、後でOracleを使用して行うことができます。

2
borjab

データを一括挿入してみてください。そのためには、 OCI * ML を使用できます。それの議論 ここにあります 。注目すべき記事 ここにあります 。または、Oracle SQL Bulk Loader SQLLDR自体を試して、アップロード速度を上げることもできます。これを行うには、データをcsvファイルにシリアル化し、引数としてcsvを渡してSQLLDRを呼び出します。

もう1つの可能な最適化は、トランザクション戦略です。スレッド/接続ごとに1つのトランザクションにすべてのデータを挿入してみてください。

別のアプローチは、 MULTIPLE INSERT :を使用することです。

INSERT ALL
   INTO ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value, 
   override, update_source ) VALUES ('c','b',NULL, 'test', 123 , 'N', 'asdf')
   INTO ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value, 
   override, update_source ) VALUES ('a','b',NULL, 'test', 123 , 'N', 'asdf')
   INTO ABC (SSM_ID, invocation_id , calc_id, analytic_id, analytic_value, 
   override, update_source ) VALUES ('b','b',NULL, 'test', 123 , 'N', 'asdf')

SELECT 1 FROM DUAL;

代わりにinsert .. union all

サンプルデータは相互に独立しているように見えます。つまり、重要な1行を挿入し、挿入後のSQLクエリを使用して4行に拡張します。

また、バッチを挿入する前にすべてのインデックスをオフにします(または、インデックスを削除して、一括で再作成します)。テーブルインデックスは、その時点で実際に使用していない間は挿入パフォーマンスを低下させます(挿入されたすべての行に対してIDを計算し、対応する操作を実行します)。

プリペアドステートメント構文を使用すると、サーバーにすでに解析されたキャッシュステートメントがあるため、アップロードルーチンが高速化されます。

次に、C++コードを最適化します。opsをサイクルから外します。

 //! Preparing the Query
   std::string insert_query = "insert into ";
   insert_query += Context::instance().getUpdateTable();
   insert_query += " (SSM_ID, invocation_id , calc_id, 
        analytic_id, analytic_value, override, update_source)\n";
   while (startOffset < statements.size())
   { ... }
1
xacinay

ちなみに、スレッドだけでなく、物理クライアントの数を増やしようとしましたか?複数のVMまたは複数の物理マシン上のクラウドで実行する。最近、Aerospikeの開発者からのコメントを読みました。多くの人は、クライアントに1秒あたり100万を超えるクエリを実際に送信させるのは簡単ではないことを理解していないために、結果を再現できないと説明しています。場合)。たとえば、ベンチマークでは、4つのクライアントを並行して実行する必要がありました。たぶん、この特定のOracleドライバーは、単一のマシンで1秒あたり7〜8千を超える要求をサポートするのに十分な速度ではありませんか?

0
Maksim Gumerov