|
SQL and PL/SQL |
||
| ID# | Question | Answer |
|
1 |
How can you create a function that will return the median of a group of numbers? |
Here are 2 examples: Note: The first query works in all releases (7.1 and up) The second query is a little more efficient but works in releases 8.1.5 and up only
Example 1:
CREATE
TABLE t AS SELECT user_id FROM all_users SELECT AVG(DISTINCT datum) FROM (SELECT cp1.datum FROM number_data cp1, number_data CP2 GROUP BY cp1.datum HAVING SUM(decode(cp1.datum, cp2.datum, 1, 0)) >= abs(SUM(sign(cp1.datum - cp2.datum)))) /
Example 2: SELECT AVG( user_id ) FROM ( SELECT user_id, ROWNUM r FROM ( SELECT user_id FROM t ORDER BY user_id ) ), ( SELECT count(*) cnt FROM t ) WHERE ( cnt/2 = trunc(cnt/2) AND ( r = cnt/2 OR r = cnt/2+1 ) ) OR ( cnt/2<> trunc(cnt/2) AND ( r = ceil(cnt/2) ) ) /
|
|
2 |
Why doesn't the following SELECT statement work?
select * from emp where hiredate = to_date('30/01/2001','dd/mm/rrrr');
|
The dates are probably getting stored with the time. If you place a function on the date you will get a full table scan. If your database is large enough you don't like that you have a few alternatives: 1) Place a redundant column in the table that is a trunc of the hiredate column. (This is what most of us used to do for this kind of problem) 2) If you are using Oracle 8.1.5(?? I think) or later you can make an index on trunc(hiredate) so you can compare using it and use that index. 3) If you can't change the database, you can use select * from emp where hiredate between to_date('30/01/2001','dd/mm/rrrr') and to_date('31/01/2001','dd/mm/rrrr') This will use the index. You probably want to take a second off the second date. |
|
3 |
Please suggest an alternative to the following
query problem:
The query has to return the rows from a master table which are not present in the detail table. It is not like an outer join; the IN operator is selecting the compliment of what is needed. The NOT IN operator should work for my condition but it is retrieving all the rows and it takes a lot of time. The query is as follows: select code from code_master where code not in (select code from code_detail where codedate between to_date('14-JUN-2001 03:04:05','dd-MON-yyyy hh24:mi:ss') and to_date('14-JUN-2001 04:04:05','dd-MON-yyyy hh24:mi:ss')); The codedate datatype is date and it has data for every second. |
Actually it is an outer join. Try the following:
select m.code from code_master m, (select code from code_detail where codedate between to_date('14-JUN-2001 03:04:05','dd-MON-yyyy hh24:mi:ss') and to_date('14-JUN-2001 04:04:05','dd-MON-yyyy hh24:mi:ss')) d where m.code = d.code(+) and d.code is null
|
|
4 |
How can I
speed up a conversion routine in PL/SQL? I need to get a flat file of data from a system, which can then be convert ed and stored on the new database. It is currently taking a couple of seconds per entry. Since there are a few thousand entries, we need to speed this up a bit. |
This
is not a trivial question. You can't assume that dropping indexes
will solve the problem. At 2 seconds per entry, this is probably not
an index problem. It does not take 2 seconds to update an index.
It might take 2 seconds to update 50 of them, but this would mean that the
system is too slow even in an OLTP mode and you have more serious
problems. I
would look closely at the migration script and put timing points into
it to find out where the bottleneck is. It is just as likely that
the problem is due to a poorly tuned query looking up a reference value as
it is due to anything regarding indexes. There
are many tuning documents available to look at for help as well as any
book on tuning in general. For
migration tuning you have a few more tools available to you. 1)
Selective shutting off of indexes, triggers, constraints. 2)
Use direct load SQL loader for 1,000,000 records The
problem can even be due to something pretty silly. Going down memory
lane, in my first migration project 5 years ago, I was building a history
table. I had a call to SYSDATE in a loop that was executing
thousands of times. I pulled the call to SYSDATE out and calculated
SYSDATE once at the beginning of the loop and the performance problem was
solved. Before
you start shutting off indexes at random, try to figure out exactly where
in your code the delay is.
|
| 5 |
Is it possible to use 'EXECUTE IMMEDIATE' for
a function with its name (with parameters) is stored in a variable?
I tried to invoke a Procedure (with parameters, by using clause USING ...) and it worked well but did not work with a function.
|
Try:
declare t varchar2(30) := 'FLACK'; x varchar2(30); begin execute immediate 'begin :a := LOWER(:b); end;' USING OUT x, IN t; DBMS_OUTPUT.PUT_LINE('x is '||x); end;
|
|
6 |
How can you use 'dynamic' ref cursors in Oracle Reports 3.0 / 6i? |
Thanks to Zlatko Sirotic in Croatia for this tip: From Oracle 8.1.5 database, we can use 'dynamic' ref cursors. With 'dynamic' ref cursor we can avoid the use of lexical parameters in Reports 3.0 / 6i. With 'static' ref cursor this is not possible in all cases. For example, if we need dynamic WHERE, we practically can't use 'static' ref cursor. 1. Example for 'static' ref cursor (dynamic ORDER BY) 1.1 Stored package CREATE OR REPLACE PACKAGE report_static IS TYPE type_ref_cur_sta IS REF CURSOR RETURN dept%ROWTYPE; FUNCTION func_sta (p_order_by VARCHAR2) RETURN type_ref_cur_sta; END; / CREATE OR REPLACE PACKAGE BODY report_static IS FUNCTION func_sta (p_order_by VARCHAR2) RETURN type_ref_cur_sta IS l_ref_cur_sta type_ref_cur_sta; BEGIN IF p_order_by = 'dname' THEN OPEN l_ref_cur_sta FOR SELECT * FROM dept ORDER BY dname; ELSE OPEN l_ref_cur_sta FOR SELECT * FROM dept ORDER BY deptno; END IF; RETURN l_ref_cur_sta; END;
END; /
1.2 Query PL/SQL in Reports function QR_1RefCurQuery return report_static.type_ref_cur_sta is begin return report_static.func_sta (:p_order_by); end; 2. Example for 'dynamic' ref cursor (dynamic WHERE) 2.1 Stored package
CREATE OR REPLACE PACKAGE report_dynamic IS TYPE type_ref_cur_sta IS REF CURSOR RETURN dept%ROWTYPE; -- for Report Layout only TYPE type_ref_cur_dyn IS REF CURSOR; FUNCTION func_dyn (p_where VARCHAR2) RETURN type_ref_cur_dyn; END; /
CREATE OR REPLACE PACKAGE BODY report_dynamic IS
FUNCTION func_dyn (p_where VARCHAR2) RETURN type_ref_cur_dyn IS l_ref_cur_dyn type_ref_cur_dyn; BEGIN OPEN l_ref_cur_dyn FOR 'SELECT * FROM dept WHERE ' || NVL (p_where, '1 = 1'); RETURN l_ref_cur_dyn; END;
END; /
2.2 Query PL/SQL in Reports function QR_1RefCurQuery return report_dynamic.type_ref_cur_sta is begin return report_dynamic.func_dyn (:p_where); end;
Note that Oracle Reports 3.0 / 6i needs 'static' ref cursor type for building Report Layout. So, in the package specification we must have both ref cursor types, static for Report Layout and dynamic for ref cursor query.
|
|
7 |
Using :OLD and :NEW values in dynamic SQL in database triggers |
Also thanks to Zlatko Sirotic for this tip: --------------------------------------------------------------
We often want to create and execute 'generic' dynamic statements in database triggers containing :NEW, referring to new values of current records, or :OLD, referring to old values of current records. In execution time, however, :OLD and :NEW are flagged as bad bind variables.
One workaround is to use AFTER STATEMENT triggers in the following manner:
1. Create a database package with a PL/SQL table for storing ROWID values 2. Clear the PL/SQL table in the BEFORE STATEMENT trigger 3. Populate the PL/SQL table with ROWID values in the BEFORE (or AFTER) ROW trigger 4. Read the rows in the AFTER STATEMENT trigger with dynamic PL/SQL, using ROWIDs from the PL/SQL table and using dynamically selected column names with this statement: SELECT column_name FROM user_tab_columns WHERE UPPER (table_name) = UPPER (p_table);
In this way, we can read :NEW values of columns.
Using AUTONOMOUS_TRANSACTION, we can read :OLD values as well, with one (big) restriction! Because an autonomous transaction does not see any changes made by the main transaction, this method does not work very well if we have 2 (or more) updates on the same row in the same transaction. For example, if we have DEPT row with DEPTNO = 10 and LOC = 'AAA', then after first update (in a transaction): UPDATE dept SET loc = 'BBB' WHERE deptno = '10'; we have: OLD VALUES: 10--DEPT10--AAA NEW VALUES: 10--DEPT10--BBB but after second update (in the same transaction): UPDATE dept SET loc = 'CCC' WHERE deptno = '10' we have: OLD VALUES: 10--DEPT10--AAA (!!! not BBB) NEW VALUES: 10--DEPT10--CCC ..."
Example (scott.dept table):
1. DATABASE TRIGGERS (FOR UPDATE)
CREATE OR REPLACE TRIGGER bus_dept BEFORE UPDATE ON dept BEGIN plsql_table.clear; END; /
CREATE OR REPLACE TRIGGER bur_dept BEFORE UPDATE ON dept FOR EACH ROW BEGIN plsql_table.populate_with_rowid (:OLD.ROWID); END; /
CREATE OR REPLACE TRIGGER aus_dept AFTER UPDATE ON dept DECLARE v_current_rowid ROWID; BEGIN dynamic_new_old.set_table_name ('dept'); dynamic_new_old.create_column_names;
WHILE plsql_table.rowid_exists LOOP v_current_rowid := plsql_table.current_rowid;
DBMS_OUTPUT.PUT_LINE ('OLD VALUES:'); dynamic_new_old.display_old_values (v_current_rowid);
DBMS_OUTPUT.PUT_LINE ('NEW VALUES:'); dynamic_new_old.display_new_values (v_current_rowid);
DBMS_OUTPUT.PUT_LINE ('*****'); END LOOP; END; /
2. PACKAGES CREATE OR REPLACE PACKAGE plsql_table IS PROCEDURE clear; PROCEDURE populate_with_rowid (p_rowid ROWID); FUNCTION rowid_exists RETURN BOOLEAN; FUNCTION current_rowid RETURN ROWID; END; /
CREATE OR REPLACE PACKAGE BODY plsql_table IS TYPE type_plsql_table IS TABLE OF ROWID INDEX BY BINARY_INTEGER; m_plsql_table type_plsql_table;
m_rec_number BINARY_INTEGER;
PROCEDURE clear IS BEGIN m_rec_number := 0; END;
PROCEDURE populate_with_rowid (p_rowid ROWID) IS BEGIN m_rec_number := m_rec_number + 1; m_plsql_table (m_rec_number) := p_rowid; END;
FUNCTION rowid_exists RETURN BOOLEAN IS BEGIN RETURN (m_rec_number > 0); END;
FUNCTION current_rowid RETURN ROWID IS v_rowid ROWID; BEGIN v_rowid := m_plsql_table (m_rec_number); m_rec_number := m_rec_number - 1; RETURN v_rowid; END; END; /
CREATE OR REPLACE PACKAGE dynamic_new_old IS PROCEDURE set_table_name (p_table VARCHAR2); PROCEDURE create_column_names; PROCEDURE display_old_values (p_rowid ROWID); PROCEDURE display_new_values (p_rowid ROWID); END; /
CREATE OR REPLACE PACKAGE BODY dynamic_new_old IS m_table VARCHAR2 (30); m_columns VARCHAR2 (32000);
PROCEDURE set_table_name (p_table VARCHAR2) IS BEGIN m_table := UPPER (p_table); END;
PROCEDURE create_column_names IS v_first_column BOOLEAN; BEGIN v_first_column := TRUE; FOR rec IN (SELECT column_name FROM user_tab_columns WHERE table_name = m_table) LOOP IF v_first_column THEN v_first_column := FALSE; m_columns := 'v_record.' || rec.column_name; ELSE m_columns := m_columns || '||' || '''--''' || '|| v_record.' || rec.column_name; END IF; END LOOP; END;
PROCEDURE display_values (p_rowid ROWID) IS v_statement VARCHAR2 (32000); BEGIN v_statement := ' DECLARE ' || ' v_record ' || m_table || '%ROWTYPE;' || ' BEGIN' || ' SELECT * INTO v_record' || ' FROM ' || m_table || ' WHERE ROWID = :p_rowid;' || ' DBMS_OUTPUT.PUT_LINE (' || m_columns || ');' || ' END;';
EXECUTE IMMEDIATE v_statement USING p_rowid; END;
PROCEDURE display_old_values (p_rowid ROWID) IS PRAGMA AUTONOMOUS_TRANSACTION; BEGIN display_values (p_rowid); END;
PROCEDURE display_new_values (p_rowid ROWID) IS BEGIN display_values (p_rowid); END; END; /
If you need :OLD values only, you can write a simpler solution (without statement triggers and 'plsql_table' package), using 'dynamic_new_old' package and AUTONOMOUS_TRANSACTION in the BEFORE (or AFTER) ROW trigger (but, don't forget previously noted restriction). |
|
8 |
How can you transfer binary files in and out of an Oracle database? |
This can be done using a Java stored script as follows:
// CMLob_Java_Proc_Src.txt ----------------------------------------------------- The PL/SQL file is shown here:
-- cm_lob_sql_faq.sql |
|
9 |
M. K. Shrivastava asked:
I want to optimize some of our long running queries. Even after going through the documentation, it is not clear to me that which query hint (FULL, INDEX, STAR, RULE etc.) should be used in a particular query. Could you please write me some Hints/Thumb rules on this topic?
The basics don't even involve hints. As the Cost Based Optimizer (CBO) has become smarter over time, the need for hints continues to decrease. We use them rarely now.
|
Here is my top 10 list of tuning tips:
1. Query tuning is a dark art. Sometimes no matter what you do, it just doesn't make any sense. I was on a project where the following happened: I had a query that had run well everywhere just fine (sub second), at a client site it took almost a minute.
Select 'None', null from dual Union all Select ...
was the structure of the query.
When I removed the "Select 'None', null from dual Union all" and ran only "Select ...", it ran fine.
I have always believed that UNION ALL simply ran the 2 queries in sequence with no additional overhead. So much for conventional tuning wisdom.
2. Learn to read an ExplainPlan, TK Prof output. All nice tools (SQL Navigator, Toad) include ExplainPlan as part of the product so it is less of a hassle. This will tell you where full table scans are causing problems. Many Oracle tuning books cover this topic and you can also read the Help files.
3. The main trivial thing to do is make sure you have indexes on all FK columns in the joins.
4. Beware of WHERE clauses with functions. If you put an index on a date and then saye 'where trunk(mydate) = trunk(sysdate)' you will not use the index,.
Instead you can write mydate between trunk(sysdate) and trunk(sysdate) + .9999999 and it will use the index. Alternatively, you can put an index directly on the function trunk(mydate).
5. One of the things we have started using often is replacing queries with multi-table joins with nested SELECT from SELECT commands. It makes the queries look a little odd, but for things that really needed to be fast, this tends to give the best performance.
6. For learning to use hints, I would suggest Rich Niemiec's book Oracle Performance Tuning Tips and Techniques (Oracle Press, 1999) . It includes useful discussions about many hints.
7. Don't believe the conventional wisdom. When the query still doesn't run fast, try everything. You may find something that works that doesn't even make sense.
8. Before you start, analyze the schema; otherwise the CBO will make stupid decisions.
9. Don't use multi-table views in joins. The CBO tends to say "What the heck, I am confused. I will just full table scan EVERYTHING!"
10. You don't have to get really complicated. Hints that force different join methods never really matter. 99% of all queries can be made to work acceptably with relatively simple stuff. It is only when you are trying to eek out every last millisecond that you need to resort to the subtle stuff.
|
|
10 |
Thanks to Zlatko Sirotic from Croatia for this tip: How can you avoid a "vicious circle" (a closed loop) in a multi-tree recursive structure (any object may have any number of children and, at most, one parent) or in a single-tree recursive structure (exactly one object would have no parent, and all other objects would have exactly one parent)?
For example - To forbid that in an EMP table one worker is a supervisor of another and that the other is subordinate to the first one. It is better to validate this in the database rather than a client application. Unfortunately, this is not so easy to do.
To illustrate: Add some records in a DEPT table:
DELETE emp / INSERT INTO emp (empno, ename, mgr) VALUES (1, 'EMP 1', NULL) / INSERT INTO emp (empno, ename, mgr) VALUES (2, 'EMP 2', 1) / INSERT INTO emp (empno, ename, mgr) VALUES (3, 'EMP 3', 1) / INSERT INTO emp (empno, ename, mgr) VALUES (4, 'EMP 4', 2) / INSERT INTO emp (empno, ename, mgr) VALUES (5, 'EMP 5', 2) / INSERT INTO emp (empno, ename, mgr) VALUES (6, 'EMP 6', 3) / INSERT INTO emp (empno, ename, mgr) VALUES (7, 'EMP 7', 3) /
Then write the first version of the package:
CREATE OR REPLACE PACKAGE emp_closed_loop IS PROCEDURE clear_plsql_tab;
PROCEDURE write_plsql_tab ( p_empno emp.empno%TYPE, p_mgr emp.mgr%TYPE);
PROCEDURE test; END emp_closed_loop; /
CREATE OR REPLACE PACKAGE BODY emp_closed_loop IS TYPE rec_t IS RECORD ( empno emp.empno%TYPE, mgr emp.mgr%TYPE);
TYPE plsql_tab_t IS TABLE OF rec_t INDEX BY BINARY_INTEGER;
m_plsql_tab plsql_tab_t; m_rows BINARY_INTEGER;
PROCEDURE clear_plsql_tab IS BEGIN m_rows := 0; END;
PROCEDURE write_plsql_tab ( p_empno emp.empno%TYPE, p_mgr emp.mgr%TYPE) IS BEGIN m_rows := m_rows + 1; m_plsql_tab (m_rows).empno := p_empno; m_plsql_tab (m_rows).mgr := p_mgr; END;
PROCEDURE test IS l_mgr emp.mgr%TYPE; l_empno emp.empno%TYPE; BEGIN FOR i IN 1..m_rows LOOP l_empno := m_plsql_tab (i).empno; l_mgr := m_plsql_tab (i).mgr;
WHILE l_mgr IS NOT NULL LOOP SELECT mgr INTO l_mgr FROM emp WHERE empno = l_mgr;
IF l_mgr = l_empno THEN RAISE_APPLICATION_ERROR (-20003, 'Closed loop!'); END IF; END LOOP; END LOOP; END; END emp_closed_loop; /
We can write the package which uses CONNECT BY clause (but to continue we would use the first version that would be upgraded):
CREATE OR REPLACE PACKAGE BODY emp_closed_loop IS TYPE rec_t IS RECORD ( empno emp.empno%TYPE, mgr emp.mgr%TYPE);
TYPE plsql_tab_t IS TABLE OF rec_t INDEX BY BINARY_INTEGER;
m_plsql_tab plsql_tab_t; m_rows BINARY_INTEGER;
PROCEDURE clear_plsql_tab IS BEGIN m_rows := 0; END;
PROCEDURE write_plsql_tab ( p_empno emp.empno%TYPE, p_mgr emp.mgr%TYPE) IS BEGIN m_rows := m_rows + 1; m_plsql_tab (m_rows).empno := p_empno; m_plsql_tab (m_rows).mgr := p_mgr; END;
PROCEDURE test IS closed_loop EXCEPTION; -- ORA-01436: CONNECT BY loop in user data PRAGMA EXCEPTION_INIT (closed_loop, -1436);
l_mgr emp.mgr%TYPE; l_empno emp.empno%TYPE; l_dummy NUMBER; BEGIN FOR i IN 1..m_rows LOOP l_empno := m_plsql_tab (i).empno; l_mgr := m_plsql_tab (i).mgr;
SELECT COUNT (*) INTO l_dummy FROM emp WHERE empno = l_empno START WITH empno = l_mgr CONNECT BY PRIOR mgr = empno; END LOOP; EXCEPTION WHEN closed_loop THEN RAISE_APPLICATION_ERROR (-20003, 'Closed loop!'); END; END emp_closed_loop; /
Database triggers:
CREATE OR REPLACE TRIGGER bir_emp BEFORE INSERT ON emp FOR EACH ROW BEGIN IF :NEW.empno = :NEW.mgr THEN RAISE_APPLICATION_ERROR (-20002, 'Employee can''t be managed by itself!'); END IF; END; /
CREATE OR REPLACE TRIGGER bus_emp BEFORE UPDATE ON emp BEGIN emp_closed_loop.clear_plsql_tab; END; /
CREATE OR REPLACE TRIGGER bur_emp BEFORE UPDATE ON emp FOR EACH ROW BEGIN IF :NEW.empno <> :OLD.empno THEN RAISE_APPLICATION_ERROR (-20001, 'NEW EMPNO <> OLD EMPNO'); END IF;
IF :NEW.empno = :NEW.mgr THEN RAISE_APPLICATION_ERROR (-20002, 'Employee can''t be managed by itself!'); END IF;
IF :NEW.mgr IS NOT NULL AND :NEW.mgr <> NVL (:OLD.mgr, 0) THEN emp_closed_loop.write_plsql_tab ( p_empno => :OLD.empno, p_mgr => :NEW.mgr); END IF; END; /
CREATE OR REPLACE TRIGGER aus_emp AFTER UPDATE ON emp BEGIN emp_closed_loop.test; END; /
If we try to make a next UPDATE we get an error (and this is OK):
UPDATE emp SET mgr = 4 WHERE empno = 1 / ERROR at line 1: ORA-20003: Closed loop! ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 40 ORA-06512: at "SCOTT.AUS_EMP", line 2 ORA-04088: error during execution of trigger 'SCOTT.AUS_EMP'
It seems that this solution works well. Unfortunately, it works only in a single user mode. In a multi-user mode an error is possible as followed:
-- SESSION 1 UPDATE emp SET mgr = 3 WHERE empno = 2 / -- SESSION 2 UPDATE emp SET mgr = 2 WHERE empno = 3 /
Consequently, the loop occurrence was permitted by the database.
We must not forget to execute a ROLLBACK: -- SESSION 1 ROLLBACK / -- SESSION 2 ROLLBACK /
It is obvious that we have to find another solution for the multi-user mode. The main idea is, while checking the circularity, also check to see if the current record is locked. If it is locked, we might assume that the rules are violated. But how to check to see whether or not the record is locked? If we use SELECT FOR UPDATE, the record locked by us will be locked until the end of the transaction and others will be unable to work. This solution is completely impractical. Therefore we might try to make the lock action an autonomous transaction in order to unlock the record after checking:
CREATE OR REPLACE PACKAGE BODY emp_closed_loop IS TYPE rec_t IS RECORD ( empno emp.empno%TYPE, mgr emp.mgr%TYPE);
TYPE plsql_tab_t IS TABLE OF rec_t INDEX BY BINARY_INTEGER;
m_plsql_tab plsql_tab_t; m_rows BINARY_INTEGER;
PROCEDURE clear_plsql_tab IS BEGIN m_rows := 0; END;
PROCEDURE write_plsql_tab ( p_empno emp.empno%TYPE, p_mgr emp.mgr%TYPE) IS BEGIN m_rows := m_rows + 1; m_plsql_tab (m_rows).empno := p_empno; m_plsql_tab (m_rows).mgr := p_mgr; END;
PROCEDURE test_lock (p_mgr emp.mgr%TYPE);
PROCEDURE test IS l_mgr emp.mgr%TYPE; l_empno emp.empno%TYPE; BEGIN FOR i IN 1..m_rows LOOP l_empno := m_plsql_tab (i).empno; l_mgr := m_plsql_tab (i).mgr;
WHILE l_mgr IS NOT NULL LOOP test_lock (l_mgr);
SELECT mgr INTO l_mgr FROM emp WHERE empno = l_mgr;
IF l_mgr = l_empno THEN RAISE_APPLICATION_ERROR (-20003, 'Closed loop!'); END IF; END LOOP; END LOOP; END;
PROCEDURE test_lock (p_mgr emp.mgr%TYPE) IS PRAGMA AUTONOMOUS_TRANSACTION; l_dummy NUMBER; BEGIN SELECT 1 INTO l_dummy FROM emp WHERE empno = p_mgr FOR UPDATE NOWAIT;
ROLLBACK; EXCEPTION WHEN OTHERS THEN -- ORA-00054: resource busy and acquire with NOWAIT specified IF SQLCODE = -54 THEN RAISE_APPLICATION_ERROR (-20004, 'Maybe closed loop!'); ELSE RAISE; END IF; END; END emp_closed_loop; /
Now it works well:
-- SESSION 1 UPDATE emp SET mgr = 3 WHERE empno = 2 / -- SESSION 2 UPDATE emp SET mgr = 2 WHERE empno = 3 / ERROR at line 1: ORA-20004: Maybe closed loop! ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 64 ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 37 ORA-06512: at "SCOTT.AUS_EMP", line 2 ORA-04088: error during execution of trigger 'SCOTT.AUS_EMP'
-- SESSION 1 ROLLBACK /
Unfortunately this doesn't work well, because the autonomous transaction (because of its autonomous character) finds the locked record (locked by the main transaction) and "thinks" that the rule is violated (and it isn't):
-- two UPDATEs in the same session UPDATE emp SET mgr = 2 WHERE empno = 6 / UPDATE emp SET mgr = 6 WHERE empno = 7 / ERROR at line 1: ORA-20004: Maybe closed loop! ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 64 ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 37 ORA-06512: at "SCOTT.AUS_EMP", line 2 ORA-04088: error during execution of trigger 'SCOTT.AUS_EMP'
ROLLBACK /
The solution is to use the PL/SQL tip "Simulating ROLLBACK TO SAVEPOINT Behavior in a Database Trigger" (www.dulcian.com/ Conference Papers and Slide Presentations/PL/SQL):
CREATE DATABASE LINK local_db_link CONNECT TO scott IDENTIFIED BY tiger using 'local_alias' -- alias for your local database /
CREATE OR REPLACE PACKAGE emp_closed_loop IS PROCEDURE clear_plsql_tab;
PROCEDURE write_plsql_tab ( p_empno emp.empno%TYPE, p_mgr emp.mgr%TYPE);
PROCEDURE test; PROCEDURE test_lock (p_mgr emp.mgr%TYPE); END emp_closed_loop; /
CREATE OR REPLACE PACKAGE BODY emp_closed_loop IS TYPE rec_t IS RECORD ( empno emp.empno%TYPE, mgr emp.mgr%TYPE);
TYPE plsql_tab_t IS TABLE OF rec_t INDEX BY BINARY_INTEGER;
m_plsql_tab plsql_tab_t; m_rows BINARY_INTEGER;
PROCEDURE clear_plsql_tab IS BEGIN m_rows := 0; END;
PROCEDURE write_plsql_tab ( p_empno emp.empno%TYPE, p_mgr emp.mgr%TYPE) IS BEGIN m_rows := m_rows + 1; m_plsql_tab (m_rows).empno := p_empno; m_plsql_tab (m_rows).mgr := p_mgr; END;
PROCEDURE test IS l_mgr emp.mgr%TYPE; l_empno emp.empno%TYPE; BEGIN FOR i IN 1..m_rows LOOP l_empno := m_plsql_tab (i).empno; l_mgr := m_plsql_tab (i).mgr;
WHILE l_mgr IS NOT NULL LOOP BEGIN emp_closed_loop.test_lock@local_db_link (l_mgr); EXCEPTION WHEN OTHERS THEN IF SQLCODE = -20999 THEN NULL; ELSE RAISE; END IF; END;
SELECT mgr INTO l_mgr FROM emp WHERE empno = l_mgr;
IF l_mgr = l_empno THEN RAISE_APPLICATION_ERROR (-20003, 'Closed loop!'); END IF; END LOOP; END LOOP; END;
PROCEDURE test_lock (p_mgr emp.mgr%TYPE) IS l_dummy NUMBER; BEGIN SELECT 1 INTO l_dummy FROM emp WHERE empno = p_mgr FOR UPDATE NOWAIT;
RAISE_APPLICATION_ERROR (-20999, 'This is for unlocking record'); EXCEPTION WHEN OTHERS THEN -- ORA-00054: resource busy and acquire with NOWAIT specified IF SQLCODE = -54 THEN RAISE_APPLICATION_ERROR (-20004, 'Maybe closed loop!'); ELSE RAISE; END IF; END; END emp_closed_loop; /
This solution could notify a rule violation even when this rule is not really violated. But that is inevitable since one session doesn't know precisely what the other does:
-- SESSION 1 UPDATE emp SET mgr = 6 WHERE empno = 2 /
-- SESSION 2 UPDATE emp SET mgr = 5 WHERE empno = 7 / ERROR at line 1: ORA-20004: Maybe closed loop! ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 70 ORA-06512: at "SCOTT.EMP_CLOSED_LOOP", line 42 ORA-06512: at "SCOTT.AUS_EMP", line 2 ORA-04088: error during execution of trigger 'SCOTT.AUS_EMP'
|
|
|
11 |
Tom Berthoff asked: Why is "select *" a bad thing?
|
Depends on where it is.
If you are doing INSERT INTO AS SELECT * FROM, if the table changes you are in big trouble.
However, if you are using it in a cursor and you are using all the columns in the table, then it certainly can be used to make cleaner code that is easier to maintain when table structures change.
I also read Steven Feuerstein’s paper of top 10 stupid things, and maybe I am really stupid but I am not opposed to some of them.
For example, I will use an exception to bypass a routine. It is not really an exception so I am using an exception to handle code logic but it is sort of an exception (I am checking initial conditions on a routine).
I just wrote a paper on this for a presentation and I found that a number of the "rules" were breakable. |
|
Kurt asked: Isn't an exception to bypass a routine as problematic as using GOTO - a bit more restricted (can jump only to the Exception code section, though anonymous blocks can modify that to an extent) but still not as structured as it could be?
|
Consider the following example:
Function f_getsomething (i_id number) return varchar2 is e_noId exception; v_out_tx varchar2(200); Begin if i_id is null then raise e_noId; end if;
... Big complex routine to figure out what to return that should be bypassed if the ID is null. ...
Return v_out_tx; Exception when others then return 'no id'; End;
-- I can put a big IF THEN around the whole routine. I can make a bypass Boolean that my exception populates, but that is just as bad. I could break it into 2 routines. All of those would be less clear than using the above technique.
We need to be careful about setting best practice rules that then become set in stone when there may be very valid times to ignore them. Others, like the ever popular "WHEN OTHERS THEN NULL" seem to be a valid acid test for incompetence.
The goal is to create bug free code that performs adequately. Once we have that, we should try to maximize: 1) readability 2) maintainability 3) stability
The "rules" we come up with are supposed to reflect the best practices we have discovered in order to achieve the above goals. If we can write code that performs better on the criteria listed above, then the rules are wrong (or at least have exceptions).
In the example above, I came up with this technique in response to building some functions that had several input parameters and the code was a few hundred lines long. Raising exceptions to support the initial condition checks made for a very clean routine.
|
|
| Oracle Designer FAQs |
| Oracle Developer FAQs |
| Oracle RDBMS FAQs |
| Data Modeling FAQs |
| JDeveloper FAQs |
| BRIM® FAQs |
| Business Rules FAQs |
| Miscellaneous FAQs |
Oracle Consulting Services | Application Development | Custom Training | Data Migration | Data Warehousing
About Dulcian | Papers & Presentations | Related Links
Publications | Employment Opportunities | Products | Home | Contact Us
©2004 Dulcian, Inc.