插入大记录时的性能问题

Posted

技术标签:

【中文标题】插入大记录时的性能问题【英文标题】:performance issue when inserting large records 【发布时间】:2020-05-10 19:00:23 【问题描述】:

我正在将字符串解析为逗号分隔并将它们插入到全局表中。插入大约 5k 条记录时性能很好,如果插入记录大约 40k+ 则性能很差。全局表只有一列。我认为使用 bulk fetch 和 forall 会提高性能,但到目前为止还不是这样。如何在插入查询下方重写或任何其他可以实现插入大记录的方式?帮助将不胜感激。我通过自己运行插入查询进行了测试,如果数据量很大,则需要很长时间来处理。

//large string
emp_refno in CLOB;

CREATE OR replace PROCEDURE employee( emp_refno IN CLOB ) AS 
    c_limit PLS_INTEGER := 1000;

      CURSOR token_cur IS
        WITH inputs(str) AS
             ( SELECT to_clob(emp_refno)
                    FROM   dual ),
        prep(s,n,token,st_pos,end_pos ) AS
           (
             SELECT ','|| str || ',',-1,NULL,NULL,1
             FROM   inputs
             UNION ALL
             SELECT s, n + 1,substr(s, st_pos, end_pos - st_pos),
                    end_pos + 1,instr(s, ',', 1, n + 3)
             FROM   prep
             WHERE  end_pos != 0
      )
    SELECT token
    FROM   prep
    WHERE  n > 0;

    TYPE token_t
    IS
      TABLE OF CLOB;
      rec_token_t TOKEN_T;
BEGIN
   OPEN token_cur;
   LOOP
     FETCH token_cur bulk collect
     INTO  rec_token_t limit c_limit;

     IF rec_token_t.count > 0 THEN
          forall rec IN rec_token_t.first ..rec_token_t.last
          INSERT INTO globaltemp_emp 
          VALUES  (  rec_token_t(rec) );
          COMMIT;
      END IF;
      EXIT
      WHEN rec_token_t.count = 0;
   END LOOP;
   OPEN p_resultset FOR
      SELECT e.empname,
             e.empaddress,
             f.department
      FROM   employee e
      join   department f
      ON     e.emp_id = t.emp_id
      AND    e.emp_refno IN
             (
                    SELECT emp_refno
                    FROM   globaltemp_emp) //USING gtt IN subquery
END;

【问题讨论】:

尝试在循环结束后移动提交,而不是在循环内。 @Kumar 我尝试在循环后移动提交,但性能没有提高。被这个性能问题困扰 这串 EMP REFNO 来自哪里?有什么东西把它们串联起来了,从那里接近它可能会更好。 @APC emp_refno 来自 java。我将它们解析为逗号分隔的字符串到控制器并将它们发送到 proc。像 Map listemp = new Hashmap(): listemp.put("emp_refno", delemitemp_refno");任何建议在这里可以做什么 在其他任何事情之前,您应该先确定select 是慢还是insert。更改过程以执行相同的操作,除了insert,并检查所需的运行时间。 【参考方案1】:

我已经调整了一个性能更好的函数。对于 90k 记录,它会在 13 秒内返回。还将 c_limit 降低到 250

您可以调整以下内容

  CREATE OR replace FUNCTION pipe_clob ( p_clob       IN CLOB,
                                          p_max_lengthb IN INTEGER DEFAULT 4000,
                                          p_rec_delim   IN VARCHAR2 DEFAULT '
    ' )
      RETURN sys.odcivarchar2list pipelined authid current_user AS
      /*
    Break CLOB into VARCHAR2 sized bites.
    Reduce p_max_lengthb if you need to expand the VARCHAR2
    in later processing.
    Last record delimiter in each bite is not returned,
    but if it is a newline and the output is spooled
    the newline will come back in the spooled output.
    Note: this cannot work if the CLOB contains more than
    <p_max_lengthb> consecutive bytes without a record delimiter.
    */
      l_amount           INTEGER;
      l_offset           INTEGER;
      l_buffer           VARCHAR2(32767 byte);
      l_out              VARCHAR2(32767 byte);
      l_buff_lengthb     INTEGER;
      l_occurence        INTEGER;
      l_rec_delim_length INTEGER := length(p_rec_delim);
      l_max_length       INTEGER;
      l_prev_length      INTEGER;
      BEGIN
        IF p_max_lengthb > 4000 THEN
          raise_application_error(-20001, 'Maximum record length (p_max_lengthb) cannot be greater than 4000.');
        ELSIF p_max_lengthb < 10 THEN
          raise_application_error(-20002, 'Maximum record length (p_max_lengthb) cannot be less than 10.');
        END IF;
        IF p_rec_delim IS NULL THEN
          raise_application_error(-20003, 'Record delimiter (p_rec_delim) cannot be null.');
        END IF;
        /* This version is limited to 4000 byte output, so I can afford to ask for 4001
    in case the record is exactly 4000 bytes long.
    */
        l_max_length:=dbms_lob.instr(p_clob,p_rec_delim,1,1)-1;
        l_prev_length:=0;
        l_amount := l_max_length + l_rec_delim_length;
        l_offset := 1;
        WHILE (l_amount = l_max_length + l_rec_delim_length
        AND
        l_amount > 0)
        LOOP
          BEGIN
            dbms_lob.READ ( p_clob, l_amount, l_offset, l_buffer );
          EXCEPTION
          WHEN no_data_found THEN
            l_amount := 0;
          END;
          IF l_amount = 0 THEN
            EXIT;
          ELSIF lengthb(l_buffer) <= l_max_length THEN
            pipe ROW(rtrim(l_buffer, p_rec_delim));
            EXIT;
          END IF;
          l_buff_lengthb := l_max_length + l_rec_delim_length;
          l_occurence := 0;
          WHILE l_buff_lengthb > l_max_length
          LOOP
            l_occurence := l_occurence                                      + 1;
            l_buff_lengthb := instrb(l_buffer,p_rec_delim, -1, l_occurence) - 1;
          END LOOP;
          IF l_buff_lengthb < 0 THEN
            IF l_amount = l_max_length + l_rec_delim_length THEN
              raise_application_error( -20004, 'Input clob at offset '
              ||l_offset
              ||' for lengthb '
              ||l_max_length
              ||' has no record delimiter' );
            END IF;
          END IF;
          l_out := substrb(l_buffer, 1, l_buff_lengthb);
          pipe ROW(l_out);
          l_prev_length:=dbms_lob.instr(p_clob,p_rec_delim,l_offset,1)-1;--san temp
          l_offset := l_offset + nvl(length(l_out),0) + l_rec_delim_length;
          l_max_length:=dbms_lob.instr(p_clob,p_rec_delim,l_offset,1)-1;--san temp
          l_max_length:=l_max_length-l_prev_length;
          l_amount := l_max_length  + l_rec_delim_length;
        END LOOP;
        RETURN;
      END;

然后在你的过程中的光标中使用如下所示

           CURSOR token_cur IS

            select  * from table (pipe_clob(emp_refno||',',10,','));

【讨论】:

这与您上次建议我的查询相同 - 此查询非常完美。我实现了您上面建议的内容,对于小数据非常有效,但对于大数据,性能非常慢。您是否认为在 WITH CTE 子句中删除逗号分隔的字符串可能会导致问题。感谢您的努力 我已经对限制进行了更改,如果条件有所更改,请尝试并告诉我 是的,我在上面更新了查询,但仍然很慢,有 50000 条 emp_ref 记录 我们可以把它分成两部分,一个用于插入,另一个用于选择查询以检查问题出在哪里,现在我将删除选择查询以检查瓶颈 我单独做了一些测试,当数据很大时插入查询需要时间。【参考方案2】:

三个快速建议:

    对大约 1000 条(或分批)记录执行提交,而不是为每条记录执行。 将 Ref 光标替换为存在。 索引 globaltemp_emp.emp_refno 如果它还没有。

另外建议为每个 DML 操作运行解释计划以检查任何奇怪的行为。

【讨论】:

的角全局表被索引。如何对大约 1000 条记录或分批执行提交?你有什么例子吗? @user10806781 是默认 GTT(提交时删除行)还是 GTT 提交时保留行?。 @Kumar 提交时是反常行 有点老派,添加一个计数器并增加相同的值,并在达到 1000 时提交,然后重置计数器。【参考方案3】:

用户上传文本文件,我将该文本文件解析为逗号分隔的字符串并将其传递给 Oracle DB。

您正在做一堆工作来将该文件转换为字符串,然后再做一堆工作将该字符串转换为表格。正如许多人在我之前观察到的那样,最好的表现来自于不做我们不必做的工作。

在这种情况下,这意味着您应该将文件的内容直接加载到数据库中。我们可以使用外部表来做到这一点。这是一种允许我们使用 SQL 从服务器上的文件中查询数据的机制。它看起来像这样:

create table emp_refno_load
   (emp_refno            varchar2(24))
    organization external
      (type oracle_loader
       default directory file_upload_dir
       access parameters
         (records delimited by newline
          fields (employee_number      char(24)
         )
       )
    location ('some_file.txt')
   );

然后您可以丢弃您的存储过程和临时表,并将您的查询重写为如下内容:

SELECT e.empname,
       e.empaddress,
       f.department
FROM   emp_refno_load  l
       join employee   e ON l.emp_refno = e.emp_refno
       join department f ON e.emp_id    = f.emp_id

外部表的一个障碍是它们需要访问操作系统目录(在我上面的示例中为file_upload_dir),并且一些数据库安全策略对此很奇怪。然而,性能优势和方法的简单性应该会成为主流。 Find out more.

外部表无疑是最具表现力的方法(直到您遇到数百万条道路,然后您需要 SQL*Loader )。

【讨论】:

我无法创建加载器或外部表。我能使用的最好的全局临时表

以上是关于插入大记录时的性能问题的主要内容,如果未能解决你的问题,请参考以下文章

插入和不存在的插入之间的性能差异

加入两个大表时的性能问题

更新/插入子文档时的 mongodb 性能

通过 HTTPS 发送大文件时的性能影响 [重复]

插入时的雪花微分区

测试记录EF插入查询性能