【问题标题】:INSERT of 10 million queries under 10 minutes in Oracle?在 Oracle 中 10 分钟内插入 1000 万个查询?
【发布时间】:2014-09-04 05:27:56
【问题描述】:

我正在开发一个文件加载程序。

这个程序的目的是获取一个输入文件,对其数据进行一些转换,然后将数据上传到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 |

查询二 -

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 |       |               |          |

根据实验,查询 I 更快

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

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

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

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

我的目标是将1000 万次查询的上传时间控制在 10 分钟以内

【问题讨论】:

  • 您的硬盘写入速度有多快?它能够达到多少 IOPS?您可能会达到这些限制之一
  • 为每个分区创建一个表空间没有任何好处——尤其是当它们都在同一个硬盘上时。
  • UNIQUE 索引是无用的,因为 seq_no 已经是唯一的。我会先测试原始导入速度,没有唯一索引,没有序列,检查哪个部分最慢。
  • 另外说明:prepared statements 通常更快,您应该将它们与数组处理结合起来,以减少向 DBMS 发送的请求。
  • 再想一想:尝试将所有数据存储到一个文件或一系列文件中,然后使用 SQL Loader 加载它们。结果加载时间将概述它的速度。因此,更容易理解您与最佳指标的差距。

标签: sql database oracle oracle-call-interface bulk-load


【解决方案1】:

您应该尝试批量插入数据。为此,您可以使用OCI*ML。对它的讨论is here。值得注意的文章is here。 或者您可以尝试使用 Oracle SQL Bulk Loader 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++ 代码: 将操作移出循环:

 //! 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())
   { ... }

【讨论】:

  • 另外,还有包DBMS_DATAPUMPexpdp/impdp使用这个API来执行它的任务。
  • @xacinay - 我已经使用我在问题中提到的 SQL 查询将所有数据合并到一个事务中。我想避免使用 SQLLDR,因为它会使我的 CPP 程序失去控制。我更喜欢可以在我的 C++ 应用程序中完成的东西,比如 OCI 库。
  • 好吧,不清楚您实际插入的每个事务有多少记录。如果 500rcs/transaction - 金额非常小。尝试将其增加到 20000rcs/transaction。
  • @xacinay 好吧,当我发送 1000 万行时,每个线程大约 33334 行。然后线程根据用户输入将其分解为 500 或 1000 个批次。我也测试了不同数量的批量大小,最高可达 5000,但性能变化并不显着。
  • 修改了我的答案(参见 OCI ML 参考)
【解决方案2】:

如果你有一个文本文件,你应该尝试使用直接路径的SQL LOADER。它非常快,专为这种海量数据加载而设计。看看这个可以提高性能的options

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

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

【讨论】:

    【解决方案3】:

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

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

    1. 索引
    2. 触发器
    3. 序列
    4. 分区

    通过所有这些,您迫使数据库执行更多工作,并且由于您是在事务性地执行此操作,因此您没有充分利用数据库。

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

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

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

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

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

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

    alter sequence abc_seq cache 5000
    

    如果你听从我的建议,我会将缓存大小增加到 10.5m 左右。

    考虑使用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 之后,我会添加TRUNCATE ABC_LOAD,否则此表将无限增长。这应该是安全的,因为届时您将使用完该表。

    您没有提及您使用的是什么版本或版本或 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 hint 加速从 ABC_LOAD 插入:

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

      这可能会导致它自己的问题(太多并行进程等),所以测试一下。它可能对小批量插入有所帮助,但不太可能,因为您会浪费时间计算哪个线程应该处理什么。


    tl;博士

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

    如果你不能使用它们,那么就去掉所有可能减慢插入速度的东西,然后批量执行,因为这是数据库擅长的。

    【讨论】:

      【解决方案4】:

      顺便说一句,您是否尝试增加物理客户端的数量,而不仅仅是线程?通过在多台虚拟机或多台物理机器上的云中运行。我最近从 Aerospike 开发人员那里读到我认为的 cmets,他们解释说,许多人无法重现他们的结果只是因为他们不明白让客户端实际上每秒发送那么多查询(在他们的案子)。例如,对于他们的基准测试,他们必须并行运行 4 个客户端。也许这个特定的 oracle 驱动程序不够快,无法在单台机器上支持每秒超过 7-8 千个请求?

      【讨论】:

      • 嗨@Maskim,我没有时间测试你建议的方法。最后,我最终使用了与 Oracle 捆绑在一起的 sqlldr 程序。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-02-08
      • 2016-12-01
      • 2019-10-28
      • 2020-12-31
      • 2020-02-29
      • 2014-01-26
      • 1970-01-01
      相关资源
      最近更新 更多