Bulk Binds در اوراکل plsql
مقدمه :
موقعی که از دستورات plsql استفاده می کنیم و آنها را به سمت پایگاه داده برای اجرا ارسال می کنیم، پایگاه داده اوراکل آنها را ابتدا در موتور مربوط plsql بررسی و اجرا می کند و قسمت های مربوط به دستورات sql را به موتور sql ارسال می کند تا در آنجا اجرا شوند ودر نهایت فعالیت خود را پایان می دهد.
یکی از مشکلات کارایی، حرکت زیاد بین این دو موتور برای اجرای یک دستور plsql می باشد که اصطلاحا به این حرکت Contex Switch می گویند. هر چقدر بتوان تعداد Contex Switch ها را کم کرد، قطعا دستور plsql با کارایی بهتری اجرا خواهد شد.
استفاده از Bulk Binds ها در راستای کم کردن Contex Switch ها می باشد که در ادامه آنها را بررسی خواهیم کرد.
BULK COLLECT
دستور Bulk Collect Into، خروجی یک کوئری را به Collection متصل می کند. برای اجرا ابتدا جدول زیر را می سازیم.
CREATE TABLE bulk_collect_test AS
SELECT owner,
object_name,
object_id
FROM all_objects;
در ادامه زمان اجرای کوئری با و بدون Bulk Collect Into را در یک ساختار plsql بررسی می کنیم.
SET SERVEROUTPUT ON
DECLARE
TYPE t_bulk_collect_test_tab IS TABLE OF bulk_collect_test%ROWTYPE;
l_tab t_bulk_collect_test_tab := t_bulk_collect_test_tab();
l_start NUMBER;
BEGIN
— Time a regular population.
l_start := DBMS_UTILITY.get_time;
FOR cur_rec IN (SELECT *
FROM bulk_collect_test)
LOOP
l_tab.extend;
l_tab(l_tab.last) := cur_rec;
END LOOP;
DBMS_OUTPUT.put_line(‘Regular (‘ || l_tab.count || ‘ rows): ‘ ||
(DBMS_UTILITY.get_time – l_start));
— Time bulk population.
l_start := DBMS_UTILITY.get_time;
SELECT *
BULK COLLECT INTO l_tab
FROM bulk_collect_test;
DBMS_OUTPUT.put_line(‘Bulk (‘ || l_tab.count || ‘ rows): ‘ ||
(DBMS_UTILITY.get_time – l_start));
END;
/
Regular (42578 rows): 66
Bulk (42578 rows): 4
PL/SQL procedure successfully completed.
SQL>
همانطور که مشاهده می کنید bulk collect into با استفاده از collection ها، کارایی را بهبود بخشیده.
لازم به توضیح می باشد bulk collect برای اجرا احتیاج به collection ها دارد ( در این مقاله می توانید با collection ها بیشتر آشنا شوید) و از نظر ساختار باید مشابه هم باشند.
همانطور که می دانید collection ها در حافظه موقت ایجاد و نگهداری می شوند، پس ایجاد یک collection حجیم با دستور bulk collect می تواند بسیار مخرب باشد به همین دلیل می توان با استفاده از LIMIT حجم واکشی داده ها و قراردادن آن در collection را کنترل کرد. به مثال زیر توجه کنید.
SET SERVEROUTPUT ON
DECLARE
TYPE t_bulk_collect_test_tab IS TABLE OF bulk_collect_test%ROWTYPE;
l_tab t_bulk_collect_test_tab;
CURSOR c_data IS
SELECT *
FROM bulk_collect_test;
BEGIN
OPEN c_data;
LOOP
FETCH c_data
BULK COLLECT INTO l_tab LIMIT 10000;
EXIT WHEN l_tab.count = 0;
— Process contents of collection here.
DBMS_OUTPUT.put_line(l_tab.count || ‘ rows’);
END LOOP;
CLOSE c_data;
END;
/
10000 rows
10000 rows
10000 rows
10000 rows
2578 rows
PL/SQL procedure successfully completed.
SQL>
از نسخه 10gعبارت for loop معادل bulk collect به همراه limit 100 در نظر گرفته می شود . ( کامپایلر خودکار این کار را انجام می دهد)
SET SERVEROUTPUT ON
DECLARE
TYPE t_bulk_collect_test_tab IS TABLE OF bulk_collect_test%ROWTYPE;
l_tab t_bulk_collect_test_tab;
CURSOR c_data IS
SELECT *
FROM bulk_collect_test;
l_start NUMBER;
BEGIN
— Time a regular cursor for loop.
l_start := DBMS_UTILITY.get_time;
FOR cur_rec IN (SELECT *
FROM bulk_collect_test)
LOOP
NULL;
END LOOP;
DBMS_OUTPUT.put_line(‘Regular : ‘ ||
(DBMS_UTILITY.get_time – l_start));
— Time bulk with LIMIT 10.
l_start := DBMS_UTILITY.get_time;
OPEN c_data;
LOOP
FETCH c_data
BULK COLLECT INTO l_tab LIMIT 10;
EXIT WHEN l_tab.count = 0;
END LOOP;
CLOSE c_data;
DBMS_OUTPUT.put_line(‘LIMIT 10 : ‘ ||
(DBMS_UTILITY.get_time – l_start));
— Time bulk with LIMIT 100.
l_start := DBMS_UTILITY.get_time;
OPEN c_data;
LOOP
FETCH c_data
BULK COLLECT INTO l_tab LIMIT 100;
EXIT WHEN l_tab.count = 0;
END LOOP;
CLOSE c_data;
DBMS_OUTPUT.put_line(‘LIMIT 100: ‘ ||
(DBMS_UTILITY.get_time – l_start));
— Time bulk with LIMIT 1000.
l_start := DBMS_UTILITY.get_time;
OPEN c_data;
LOOP
FETCH c_data
BULK COLLECT INTO l_tab LIMIT 1000;
EXIT WHEN l_tab.count = 0;
END LOOP;
CLOSE c_data;
DBMS_OUTPUT.put_line(‘LIMIT 1000: ‘ ||
(DBMS_UTILITY.get_time – l_start));
END;
/
Regular : 18
LIMIT 10 : 80
LIMIT 100: 15
LIMIT 1000: 10
PL/SQL procedure successfully completed.
SQL>
FORALL
از دستور forall برای اعمال دستورات DML بر روی collectionها استفاده می شود که به این ترتیب contex Switch کاهش می یابد. برای انجام آزمایش ابتدا جدول زیر را می سازیم.
CREATE TABLE forall_test (
id NUMBER(10),
code VARCHAR2(10),
description VARCHAR2(50));
ALTER TABLE forall_test ADD (
CONSTRAINT forall_test_pk PRIMARY KEY (id));
ALTER TABLE forall_test ADD (
CONSTRAINT forall_test_uk UNIQUE (code));
حال دستور زیر را که مقاسه insert با و بدون forall می باشد را بررسی می کنیم.
SET SERVEROUTPUT ON
DECLARE
TYPE t_forall_test_tab IS TABLE OF forall_test%ROWTYPE;
l_tab t_forall_test_tab := t_forall_test_tab();
l_start NUMBER;
l_size NUMBER := 10000;
BEGIN
— Populate collection.
FOR i IN 1 .. l_size LOOP
l_tab.extend;
l_tab(l_tab.last).id := i;
l_tab(l_tab.last).code := TO_CHAR(i);
l_tab(l_tab.last).description := ‘Description: ‘ || TO_CHAR(i);
END LOOP;
EXECUTE IMMEDIATE ‘TRUNCATE TABLE forall_test’;
— Time regular inserts.
l_start := DBMS_UTILITY.get_time;
FOR i IN l_tab.first .. l_tab.last LOOP
INSERT INTO forall_test (id, code, description)
VALUES (l_tab(i).id, l_tab(i).code, l_tab(i).description);
END LOOP;
DBMS_OUTPUT.put_line(‘Normal Inserts: ‘ ||
(DBMS_UTILITY.get_time – l_start));
EXECUTE IMMEDIATE ‘TRUNCATE TABLE forall_test’;
— Time bulk inserts.
l_start := DBMS_UTILITY.get_time;
FORALL i IN l_tab.first .. l_tab.last
INSERT INTO forall_test VALUES l_tab(i);
DBMS_OUTPUT.put_line(‘Bulk Inserts : ‘ ||
(DBMS_UTILITY.get_time – l_start));
COMMIT;
END;
/
Normal Inserts: 305
Bulk Inserts : 14
PL/SQL procedure successfully completed.
SQL>
همانطور که مشاهده می کنید با کاهش contex switch، کارایی بهبود می یابد.
به مثال زیر نیز توجه کنید.
SET SERVEROUTPUT ON
DECLARE
TYPE t_id_tab IS TABLE OF forall_test.id%TYPE;
TYPE t_forall_test_tab IS TABLE OF forall_test%ROWTYPE;
l_id_tab t_id_tab := t_id_tab();
l_tab t_forall_test_tab := t_forall_test_tab ();
l_start NUMBER;
l_size NUMBER := 10000;
BEGIN
— Populate collections.
FOR i IN 1 .. l_size LOOP
l_id_tab.extend;
l_tab.extend;
l_id_tab(l_id_tab.last) := i;
l_tab(l_tab.last).id := i;
l_tab(l_tab.last).code := TO_CHAR(i);
l_tab(l_tab.last).description := ‘Description: ‘ || TO_CHAR(i);
END LOOP;
— Time regular updates.
l_start := DBMS_UTILITY.get_time;
FOR i IN l_tab.first .. l_tab.last LOOP
UPDATE forall_test
SET ROW = l_tab(i)
WHERE id = l_tab(i).id;
END LOOP;
DBMS_OUTPUT.put_line(‘Normal Updates : ‘ ||
(DBMS_UTILITY.get_time – l_start));
l_start := DBMS_UTILITY.get_time;
— Time bulk updates.
FORALL i IN l_tab.first .. l_tab.last
UPDATE forall_test
SET ROW = l_tab(i)
WHERE id = l_id_tab(i);
DBMS_OUTPUT.put_line(‘Bulk Updates : ‘ ||
(DBMS_UTILITY.get_time – l_start));
COMMIT;
END;
/
Normal Updates : 235
Bulk Updates : 20
PL/SQL procedure successfully completed.
SQL>
SQL%BULK_ROWCOUNT
SQL%BULK_ROWCOUNT ویژگی که تعداد رکوردهایی که در هر مرتبه دچار تغییر می شوند را در خود نگهداری می کند.
به مثال زیر توجه کنید.
CREATE TABLE bulk_rowcount_test AS
SELECT *
FROM all_users;
SET SERVEROUTPUT ON
DECLARE
TYPE t_array_tab IS TABLE OF VARCHAR2(30);
l_array t_array_tab := t_array_tab(‘SCOTT’, ‘SYS’,
‘SYSTEM’, ‘DBSNMP’, ‘BANANA’);
BEGIN
— Perform bulk delete operation.
FORALL i IN l_array.first .. l_array.last
DELETE FROM bulk_rowcount_test
WHERE username = l_array(i);
— Report affected rows.
FOR i IN l_array.first .. l_array.last LOOP
DBMS_OUTPUT.put_line(‘Element: ‘ || RPAD(l_array(i), 15, ‘ ‘) ||
‘ Rows affected: ‘ || SQL%BULK_ROWCOUNT(i));
END LOOP;
END;
/
Element: SCOTT Rows affected: 1
Element: SYS Rows affected: 1
Element: SYSTEM Rows affected: 1
Element: DBSNMP Rows affected: 1
Element: BANANA Rows affected: 0
PL/SQL procedure successfully completed.
SQL>
استفاده از SAVE EXCEPTIONS و SQL%BULK_EXCEPTION
در صورت استفاده از forall، هنگام ایجاد خطا در هریک از دستورات DML، اگر handler برای بر خورد با خطا وجود نداشته باشد همه عملیات rollback خواهد شد و اگر handler وجود داشته، عملیات تا قبل از بروز خطا، حفظ خواهد شد و اما با وقوع خطا، از ادامه عملیات جلوگیری خواهد شد که هر دو سناریو مناسب نمی باشند.
می توان از دستور SAVE EXCEPTIONS استفاده کرد که در مواجه با خطا آن را نگهداری کند و عملیات ادامه یابد و بعداز اتمام عملیات، با SQL%BULK_EXCEPTION به خطاهای رخ داده دسترسی پیدا کرد و تصمیم گیری کرد. این مورد با اجرای یک مثال کاملا شفاف خواهد شد. برای اجرا مثال ابتدا جدول زیر را خواهیم ساخت .
CREATE TABLE exception_test (
id NUMBER(10) NOT NULL
);
در ادامه کد زیر را اجرا خواهیم کرد. در این مثال سعی می شود در هنگام ورود 100 رکورد در این جدول 2 رکورد که مقدار آنها null می باشد را وارد این جدول کرد که با توجه به محدودیتی که برای جدول در نظر گرفته شده با توقع ایجاد خطا را داشته باشیم
SET SERVEROUTPUT ON
DECLARE
TYPE t_tab IS TABLE OF exception_test%ROWTYPE;
l_tab t_tab := t_tab();
l_error_count NUMBER;
ex_dml_errors EXCEPTION;
PRAGMA EXCEPTION_INIT(ex_dml_errors, -24381);
BEGIN
— Fill the collection.
FOR i IN 1 .. 100 LOOP
l_tab.extend;
l_tab(l_tab.last).id := i;
END LOOP;
— Cause a failure.
l_tab(50).id := NULL;
l_tab(51).id := NULL;
EXECUTE IMMEDIATE ‘TRUNCATE TABLE exception_test’;
— Perform a bulk operation.
BEGIN
FORALL i IN l_tab.first .. l_tab.last SAVE EXCEPTIONS
INSERT INTO exception_test
VALUES l_tab(i);
EXCEPTION
WHEN ex_dml_errors THEN
l_error_count := SQL%BULK_EXCEPTIONS.count;
DBMS_OUTPUT.put_line(‘Number of failures: ‘ || l_error_count);
FOR i IN 1 .. l_error_count LOOP
DBMS_OUTPUT.put_line(‘Error: ‘ || i ||
‘ Array Index: ‘ || SQL%BULK_EXCEPTIONS(i).error_index ||
‘ Message: ‘ || SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE));
END LOOP;
END;
END;
/
Number of failures: 2
Error: 1 Array Index: 50 Message: ORA-01400: cannot insert NULL into ()
Error: 2 Array Index: 51 Message: ORA-01400: cannot insert NULL into ()
PL/SQL procedure successfully completed.
SQL>
SELECT COUNT(*)
FROM exception_test;
COUNT(*)
———-
98
1 row selected.
SQL>