插入大记录时的性能问题
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。像 Mapselect
是慢还是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 )。
【讨论】:
我无法创建加载器或外部表。我能使用的最好的全局临时表以上是关于插入大记录时的性能问题的主要内容,如果未能解决你的问题,请参考以下文章