在Oracle下10分钟内插入1000万个查询?

时间:2014-07-14 10:10:16

标签: sql database oracle oci bulk-load

我正在处理文件加载程序。

该程序的目的是获取输入文件,对其数据进行一些转换,然后将数据上传到Oracle数据库中。

我面临的问题是我需要优化在Oracle上插入非常大的输入数据。

我正在将数据上传到表中,比方说ABC。

我在我的C ++程序中使用Oracle提供的OCI库。 具体来说,我使用OCI连接池进行多线程并加载到ORACLE。 (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)

我需要进一步优化和缩短这个时间。我面临的问题是我上传了1000万行。

1000万的平均运行时间= = 21分钟

(我的目标是将此时间缩短至10分钟以下)

所以我也尝试了以下步骤 -

[1] 是否基于 seq_no 对表ABC进行了分区。 使用 30个分区。 测试 100万行 - 性能非常差。几乎未分区表的4倍。

[2] 在 last_chg_date 的基础上对表ABC进行另一次分区。 使用 30个分区

2.a)测试了100万行 - 性能几乎等于未分区的表格。差异很小,因此没有考虑。

2.b)再次测试相同的1000万行。性能几乎与未分区表相同。没有明显的区别。

以下是用于实现分区的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);

我在线程函数中使用的代码(用C ++编写),使用OCI -

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 的方法。 我在我的程序中选择并使用 QUERY I ,原因如下我在测试各种INSERT查询时发现的原因。 在运行向我建议的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 |

QUERY 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针对查询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 |       |               |          |

根据实验,查询我更快

这里我在Oracle SQL Developer上进行了测试,并且我也通过我的C ++程序(FILELOADER)发送了插入查询。

在进一步阅读它时,我发现执行计划显示的成本是查询用于处理自身的CPU数量。 这表明Oracle将使用更多的CPU来处理第一个查询,这就是为什么它的成本将继续为= 8。

即使通过我的应用程序使用相同的插入模式,我发现它的性能几乎提高了1.5倍。

我需要深入了解如何进一步提高性能......? 我尝试过的所有事情,都是在我的问题中总结出来的。 如果我发现或发现任何相关内容,我将添加此问题。

我的目标是在10分钟内带来 1000万次查询的上传时间

4 个答案:

答案 0 :(得分:7)

我知道其他人已经提到了这一点,但您不想听到它,而是使用SQL*Loaderexternal tables。我大约相同宽度的表的平均加载时间为12.57 ,行数超过10米。这些实用程序已明确设计为快速将数据加载到数据库中并且非常擅长。根据输入文件的格式,这可能会产生一些额外的时间处罚,但是有很多选项,我很少在加载之前更改文件。

如果您不愿意这样做,那么您还不必升级硬件;你需要消除所有可能阻碍快速加载的障碍。要枚举它们,请删除:

  1. 索引
  2. 触发器
  3. 序列
  4. 分区
  5. 通过所有这些,您需要让数据库执行更多工作,并且因为您在事务上执行此操作,所以您并未充分利用数据库。

    将数据加载到单独的表中,例如ABC_LOAD。数据完全加载后,在ABC中执行单个 INSERT语句。

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

    当你这样做时(即使你没有)确保序列缓存大小是正确的; to quote

      

    当应用程序访问序列缓存中的序列时,   序列号快速读取。但是,如果应用程序访问   如果序列不在缓存中,则必须读取序列   在使用序列号之前从磁盘到缓存。

         

    如果您的应用程序同时使用多个序列,那么您的   序列缓存可能不够大,无法容纳所有序列。在   在这种情况下,访问序列号可能通常需要磁盘读取。   要快速访问所有序列,请确保缓存足够   用于保存您的所有序列同时使用的条目   应用

    这意味着如果你有10个线程同时使用这个序列写500条记录,那么你需要一个5000的缓存大小。 ALTER SEQUENCE文档说明了如何更改此内容:

    alter sequence abc_seq cache 5000
    

    如果你按照我的建议,我将缓存大小调整到大约10.5米。

    使用APPEND hint (see also 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_LOAD后我添加TRUNCATE ABC,否则此表将无限增长。这应该是安全的,因为到那时你将完成使用该表。

    您不会提及您正在使用的版本或版本或Oracle。你可以使用一些额外的小技巧:

    • Oracle 12c

      此版本支持identity columns;你可以完全摆脱序列。

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

      如果你保持扳机;您可以直接指定序列值。

      :new.seq_no := ABC_seq.nextval;
      
    • Oracle企业版

      如果您正在使用Oracle Enterprise,则可以使用PARALLEL hintABC_LOAD加速INSERT:

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

      这可能导致它自身的问题(太多的并行进程等),所以测试。它可能帮助较小的批量插入,但它不太可能,因为你将失去计算什么线程应该处理什么的时间。


    TL;博士

    使用数据库附带的实用程序。

    如果您无法使用它们,那么请删除所有可能会降低插入速度并大量执行此操作的内容,以及导致数据库擅长的功能。

答案 1 :(得分:2)

如果您有文本文件,则应尝试使用直接路径SQL LOADER。它非常快,专为这种大规模数据负载而设计。看一下可以提高性能的options

作为ETL的第二个优势,明文文件比10 ^ 7插入文件更小,更容易审核。

如果您需要进行一些转换,您可以使用oracle进行转换。

答案 2 :(得分:1)

您应该尝试批量插入数据。为此,您可以使用OCI*ML。对它的讨论is here。值得注意的文章is here。 或者您可以尝试使用Oracle SQL批量加载程序SQLLDR来提高上传速度。为此,将数据序列化为csv文件并调用SQLLDR作为参数传递csv。

另一种可能的优化是交易策略。尝试在每个线程/连接的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())
   { ... }

答案 3 :(得分:0)

顺便问一下,您是否尝试增加物理客户端数量,而不仅仅是线程?通过在多个VM或多个物理计算机上运行云。我最近读过Aerospike开发人员的评论,他们解释说许多人无法重现他们的结果只是因为他们不理解它并不容易使客户端实际每秒发送那么多查询(他们的每秒大于1M)案件)。例如,对于他们的基准测试,他们必须并行运行4个客户端。也许这个特殊的oracle驱动程序速度不够快,无法在单台机器上支持每秒7-8千多个请求?

相关问题