sql - 在 Oracle 中 10 分钟内插入 1000 万个查询?

标签 sql database oracle oracle-call-interface 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 查询时 - 查询 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 分钟以内

最佳答案

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

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

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

有了所有这些,您就迫使数据库执行更多工作,并且因为您是在事务上执行此操作,所以您没有充分利用数据库的潜力。

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

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

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

When an application accesses a sequence in the sequence cache, the sequence numbers are read quickly. However, if an application accesses a sequence that is not in the cache, then the sequence must be read from disk to the cache before the sequence numbers are used.

If your applications use many sequences concurrently, then your sequence cache might not be large enough to hold all the sequences. In this case, access to sequence numbers might often require disk reads. For fast access to all sequences, be sure your cache has enough entries to hold all the sequences used concurrently by your applications.

这意味着如果您有 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 提示;我会添加 TRUNCATE ABC_LOAD 在您插入 ABC 之后,否则该表将无限增长。这应该是安全的,因为届时您将完成对表格的使用。

您没有提及您使用的是什么版本或版本或 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 Enterprise,则可以通过使用 PARALLEL hint 来加快 ABC_LOAD 的 INSERT :

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

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


tl;dr

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

如果您不能使用它们,则摆脱所有可能减慢插入速度并批量执行的操作,因为这是数据库擅长的。

关于sql - 在 Oracle 中 10 分钟内插入 1000 万个查询?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24734131/

相关文章:

sql - VB.NET 中的准备语句

sql - 将 SQL 二进制内容转换为文件

mysql - 无法创建 mysql 表 : foreign key constraint issue

php - Doctrine 一对多关系返回 Doctrine_Record 而不是 Doctrine_Collection

sql - Oracle 加入 SQL 探索

mysql - 在具有 160M+ 行的 MySQL InnoDB 表上,选择查询非常慢

ruby-on-rails - Rails 3.1 数据库查询

oracle - 如何使用 Oracle SQL Developer 打印实体关系 (ER) 图

mysql - 如何对数据库中的字母数值使用自动递增

mysql - 关系表的最佳方式