Presentation is loading. Please wait.

Presentation is loading. Please wait.

Copyright All material contained herein is owned by Daniel Stober, the author of this presentation. This presentation and the queries, examples, and original.

Similar presentations


Presentation on theme: "Copyright All material contained herein is owned by Daniel Stober, the author of this presentation. This presentation and the queries, examples, and original."— Presentation transcript:

1 Copyright All material contained herein is owned by Daniel Stober, the author of this presentation. This presentation and the queries, examples, and original sample data may be shared with others for educational purposes only in the following circumstances: That the person or organization sharing the information is not compensated, or If shared in a circumstance with compensation, that the author has granted written permission to use this work Using examples from this presentation, in whole or in part, in another work without proper source attribution constitutes plagiarism. Use of this work implies acceptance of these terms

2 Six Bugs you can put into Your PLSQL Programs
Dan Stober Intermountain Healthcare September 14, 2011

3 Dan Stober Data Architect – Intermountain Healthcare
Attended California State Univ., Fresno Working in Oracle databases since 2001 Frequent presenter at local and national user group conferences Oracle Open World twice Private Instructor for Trutek Teaching PLSQL Oracle Certified SQL Expert Board of Trustees – Utah Oracle Users Group Edit newsletter Write SQL Tip column

4 Intermountain Healthcare
23 hospitals in Utah and Idaho Non-profit integrated health care system 750 Employed physicians 32,000 employees The largest non-government employer in Utah One of the largest and most complete clinical data warehouses in the world!

5 Session Norms Questions? I learn something from every session I do!
Interrupt Me! I learn something from every session I do! Set the record straight! Cell phones? OK!

6 Bugs? These are: How did I learn these? My goal? Not bugs in PLSQL
Errors in the code introduced by programmers who missed some of the nuances in the language Examples of PLSQL behaving exactly as documented How did I learn these? I’ve made every one of these mistakes! My goal? Make you aware So you can avoid them

7 Bug Agenda FOR LOOP variables No_data_found Parameter defaults
Function returned without value Declaration section exceptions More FOR LOOP issues Implicit COMMIT

8 Bug #1 FOR LOOP Variables

9 What will be the result? This variable i is the same as this i
DECLARE i NUMBER := 10; BEGIN dbms_output.put_line ( 'i=' || i ) ; FOR i IN 1..5 LOOP END LOOP; END; This variable i is the same as this i But, this i is a different variable i=10 i=1 i=2 i=3 i=4 i=5 PL/SQL procedure successfully completed.

10 The FOR LOOP handles all of these
Components of a LOOP Duties / Functions Declaration Initialization Iteration Exit condition DECLARE i NUMBER; BEGIN i := 1; LOOP dbms_output.put_line ( i ) ; i := i + 1; EXIT WHEN i > 5; END LOOP; END; Declaration Initialization Iteration Exit Condition The FOR LOOP handles all of these BEGIN FOR i IN 1..5 LOOP dbms_output.put_line ( i ) ; END LOOP; END;

11 Components of a Cursor Loop
DECLARE CURSOR emps IS SELECT ename FROM scott.emp; rec emps%TYPE; BEGIN OPEN emps; LOOP FETCH emps INTO rec; EXIT WHEN emps%NOTFOUND; dbms_output.put_line ( rec.ename ) END LOOP; CLOSE emps; END; Declaration Open / Close Record Fetch Iteration Exit Condition Open / Close

12 Components of a Cursor FOR Loop
Declaration BEGIN FOR rec IN ( SELECT ename FROM scott.emp ) LOOP dbms_output.put_line ( rec.ename ) END LOOP; END; Open / Close Record Fetch Iteration Exit Condition

13 How does this manifest as a bug?
Scope of LOOP variables DECLARE CURSOR emps IS SELECT empno, ename FROM scott.emp; rec emps%TYPE; BEGIN FOR rec IN emps LOOP INSERT INTO myemps VALUES rec; END LOOP; EXCEPTION WHEN OTHERS THEN dbms_output.put_line ( SQLERRM || '>' || rec.empno ); END; It creates the temptation to reference the loop variable outside of the loop, with the belief that the variable will retain the value that it held inside of the loop.

14 CURSOR FOR variables The same principle applies in a CURSOR FOR LOOP
DECLARE CURSOR emps_cur is ( SELECT empno, ename, sal FROM emp ORDER BY empno); rec emps_cur%TYPE; BEGIN FOR rec IN emps_cur LOOP process_record ( rec.empno ); END LOOP; IF rec.sal > 1000 THEN ... etc etc etc The programmer probably intended to read the value from the last record processed. But this is not the same variable

15 One solution Bug #1: Never declare FOR LOOP variables On solution:
Create a separate variable With a different name With scope outside of the loop DECLARE CURSOR emps IS SELECT empno, ename FROM scott.emp; v_empno NUMBER; BEGIN FOR rec IN emps LOOP v_empno := rec.empno; INSERT INTO myemps VALUES rec; END LOOP; EXCEPTION WHEN OTHERS THEN dbms_output.put_line ( SQLERRM || '>' || v_empno ); END; Assign the value from the cursor record to a variable with a scope which exceeds the loop boundary.

16 Bug #2 No_data_found

17 No_data_found Built-in PLSQL exception designed to be used with implicit cursors Implicit cursor: Query structure using SELECT xxx INTO xxx One and only one record is expected If more than one record: Too_many_rows If no records: No_data_found Bug: An aggregate function always returns one row Unless used with GROUP BY

18 Some handlers are not needed
Group functions always return one row SQL> SELECT COUNT(*) 2 FROM emp 3 WHERE job = 'DBA'; COUNT(*) 1 row selected. FUNCTION get_emp_job_count ( p_job IN VARCHAR2 ) RETURN NUMBER IS v_ret_val NUMBER; BEGIN SELECT COUNT(*) INTO v_ret_val FROM emp WHERE job = p_job; EXCEPTION WHEN no_data_found THEN v_ret_val := 0; END; RETURN v_ret_val; So what? The handler isn’t needed. It might be extra clutter, but does that make it a bug? This exception will never be raised

19 This exception will never be raised
Unused handler FUNCTION get_job_sal_sum ( p_job IN VARCHAR2 ) RETURN NUMBER IS v_ret_val NUMBER; BEGIN SELECT SUM( sal ) INTO v_ret_val FROM emp WHERE job = p_job; EXCEPTION WHEN no_data_found THEN v_ret_val := 0; END; RETURN v_ret_val; The attempt here is to return ZERO when there are no emps with the job This exception will never be raised When called for a JOB with no records in the table, the function will return NULL. Not ZERO.

20 Calling the Buggy Function
BEGIN dbms_output.put_line ( 'CLERK: ' || get_job_sal_sum ( 'CLERK')); dbms_output.put_line ( 'DBA: ' || get_job_sal_sum ( 'DBA')); dbms_output.put_line ( 'SALESMAN: ' || get_job_sal_sum ( 'SALESMAN')); END; / CLERK: 4150 DBA: SALESMAN: 5600 PL/SQL procedure successfully completed. BEGIN dbms_output.put_line ( 'CLERK: ' || get_job_sal_sum ( 'CLERK')); dbms_output.put_line ( 'DBA: ' || get_job_sal_sum ( 'DBA')); dbms_output.put_line ( 'SALESMAN: ' || get_job_sal_sum ( 'SALESMAN')); END; The function returned NULL The query DID return a record, thus no_data_found was not raised, so the function returned the value from the query. It did not get replaced with ZERO. Here’s the query that was executed on the second call SQL> SELECT SUM ( sal ) 2 FROM emp 3 WHERE job = 'DBA'; SUM(SAL) 1 row selected.

21 Fixing the Buggy Function
FUNCTION get_job_sal_sum ( p_job IN VARCHAR2) RETURN NUMBER IS v_ret_val NUMBER; BEGIN SELECT SUM( sal ) INTO v_ret_val FROM emp WHERE job = p_job; RETURN NVL ( v_ret_val, 0); END; If the variable contains NULL, it will return ZERO instead BEGIN dbms_output.put_line ( 'CLERK: ' || get_job_sal_sum ( 'CLERK')); dbms_output.put_line ( 'DBA: ' || get_job_sal_sum ( 'DBA')); dbms_output.put_line ( 'SALESMAN: ' || get_job_sal_sum ( 'SALESMAN')); END; BEGIN dbms_output.put_line ( 'CLERK: ' || get_job_sal_sum ( 'CLERK')); dbms_output.put_line ( 'DBA: ' || get_job_sal_sum ( 'DBA')); dbms_output.put_line ( 'SALESMAN: ' || get_job_sal_sum ( 'SALESMAN')); END; / CLERK: 4150 DBA: 0 SALESMAN: 5600 PL/SQL procedure successfully completed. Excellent!

22 Another Way to Fix It FUNCTION get_emp_job_count ( p_job IN VARCHAR2 ) RETURN NUMBER IS v_ret_val NUMBER; BEGIN SELECT COUNT(*) INTO v_ret_val FROM emp WHERE job = p_job GROUP BY job; EXCEPTION WHEN no_data_found THEN v_ret_val := 0; END; RETURN v_ret_val; With a GROUP BY, the query will return no records, if there is no match! SQL> SELECT COUNT(*) 2 FROM emp 3 WHERE job = 'DBA' 4 GROUP BY job; no rows selected

23 Another issue Do you see it?
PROCEDURE process_sal_increase ( p_empno IN NUMBER ) IS v_current_sal NUMBER; BEGIN SELECT sal INTO v_current_sal FROM emp WHERE empno = p_empno AND ROWNUM = 1; v_new_sal := calculate_new_sal ( p_empno, v_sal ); END; If the WHERE clause returns more than one record, and you don’t care which one you get, then why bother selecting? - Tom Kyte, paraphrased Why is this here? When you see this, it usually means that too_many_rows was raised at some point. Instead of fixing the data or the WHERE clause, the developer added this

24 An Aside: Performance Bug
DECLARE V_rec_cnt INTEGER; BEGIN SELECT COUNT(*) INTO V_rec_cnt FROM load_table; IF V_rec_cnt > 0 THEN -- table is not empty, process records . . . What is the purpose? Does the program really need to know how many records are in the table? Or, is the intent just to make sure that the table is not empty? SELECT COUNT(*) INTO V_rec_cnt FROM load_table WHERE ROWNUM = 1;

25 Opens cursor to find out how many records will be returned
Another variation SELECT empi, fcilty_id, immu_dt , COUNT (*) OVER () rec_count FROM immu_vaccinations WHERE acct_no = ' ' DECLARE V_rec_count : CURSOR my_recs IS ( SELECT empi, fcilty_id, immu_dt FROM immu_vaccinations WHERE acct_no = ' ' ) recs_t IS TABLE OF my_recs%ROWTYPE; v_recs recs_t; BEGIN OPEN my_recs; FETCH my_recs BULK COLLECT INTO v_recs; CLOSE my_recs; IF v_recs.COUNT = 1 THEN FOR j IN my_recs LOOP << processing to do if the cursor returned only one records >> END LOOP; ELSE << processing to do if the cursor returned multiple records >> END IF; END; Use analytic function to collect the record count at the same time as query records Opens cursor to find out how many records will be returned Different logic for one record versus multiple records. Reexecutes the same query

26 Bug #3 Default Values for Parameters

27 Function to calculate age
Default value for second parameter means if no value is passed, then function will use sysdate FUNCTION get_age_in_years ( p_birth_date IN DATE , p_as_of_date IN DATE DEFAULT SYSDATE ) RETURN NUMBER IS v_ret_val NUMBER; BEGIN v_ret_val := TRUNC( MONTHS_BETWEEN ( p_as_of_date, p_birth_date )/12); RETURN v_ret_val; END get_age_in_years; BEGIN dbms_output.put_line ( 'AGE=' || get_age_in_years ( DATE ' ' , DATE ' ' ) ); dbms_output.put_line ( 'AGE=' || get_age_in_years ( DATE ' ' ) ); END; BEGIN dbms_output.put_line ( 'AGE=' || get_age_in_years ( DATE ' ' , DATE ' ' ) ); dbms_output.put_line ( 'AGE=' || get_age_in_years ( DATE ' ' ) ); END; / AGE=43 PL/SQL procedure successfully completed. BEGIN dbms_output.put_line ( 'AGE=' || get_age_in_years ( DATE ' ' , DATE ' ' ) ); END; / BEGIN dbms_output.put_line ( 'AGE=' || get_age_in_years ( DATE ' ' , DATE ' ' ) ); END; / AGE=43 PL/SQL procedure successfully completed.

28 Parameter Defaults and NULL
DECLARE v_from_date DATE; v_to_date DATE; BEGIN v_from_date := DATE ' '; dbms_output.put_line ( 'AGE=' || get_age_in_years ( v_from_date, v_to_date ) ); ( 'AGE=' || get_age_in_years ( v_from_date ) ); END; / AGE= AGE=43 DECLARE v_from_date DATE; v_to_date DATE; BEGIN v_from_date := DATE ' '; dbms_output.put_line ( 'AGE=' || get_age_in_years ( v_from_date, v_to_date ) ); ( 'AGE=' || get_age_in_years ( v_from_date ) ); END; / In the first call, a variable with a NULL value was passed to the second parameter. In the second call, the second parameter was not referenced at all. In the second call, the default value of the parameter was used by the function.

29 How can you avoid this? If you need to make sure that the parameter is not null, use a variable This is needed still. Why? FUNCTION get_age_in_years ( p_birth_date IN DATE , p_as_of_date IN DATE DEFAULT SYSDATE ) RETURN NUMBER IS v_ret_val NUMBER; v_as_of_date DATE; BEGIN v_as_of_date := NVL( p_as_of_date, SYSDATE ); v_ret_val := TRUNC( MONTHS_BETWEEN ( v_as_of_date, p_birth_date )/12); RETURN v_ret_val; END get_age_in_years; Variable for param NVL to handle NULL value

30 Calling With Function Fixed
DECLARE v_from_date DATE; v_to_date DATE; BEGIN v_from_date := DATE ' '; dbms_output.put_line ( 'AGE=' || get_age_in_years ( v_from_date, v_to_date ) ); ( 'AGE=' || get_age_in_years ( v_from_date ) ); END; / AGE=43 DECLARE v_from_date DATE; v_to_date DATE; BEGIN v_from_date := DATE ' '; dbms_output.put_line ( 'AGE=' || get_age_in_years ( v_from_date, v_to_date ) ); ( 'AGE=' || get_age_in_years ( v_from_date ) ); END; / Success!

31 Bug #4 Function returned without value

32 Function FUNCTION get_age_description ( p_birth_date IN DATE , p_as_of_date IN DATE ) RETURN VARCHAR2 IS v_age NUMBER; BEGIN v_age := TRUNC( MONTHS_BETWEEN ( p_as_of_date, p_birth_date )/12); IF v_age < 18 THEN RETURN 'Minor'; ELSIF v_age = 18 THEN RETURN 'Voting age'; ELSIF v_age = 21 THEN RETURN 'Drinking age'; ELSIF v_age BETWEEN 22 AND 65 THEN RETURN 'Adult'; ELSIF v_age > 66 THEN RETURN 'Eligible for Medicare'; END IF; END get_age_description; SELECT get_age_description ( DATE ' ', SYSDATE ) AS test FROM DUAL; SELECT get_age_description ( DATE ' ', SYSDATE ) AS test FROM DUAL; * ERROR at line 1: ORA-06503: PL/SQL: Function returned without value ORA-06512: at "SCOTT.GET_AGE_DESCRIPTION", line 30 SELECT get_age_description ( DATE ' ', SYSDATE ) AS test FROM DUAL; TEST Voting age 1 row selected. SELECT get_age_description ( DATE ' ', SYSDATE ) AS test FROM DUAL; This error means that execution got all the way to the end of the function and never encountered a RETURN statement

33 2nd attempt Introduction of a variable to hold RETURN value
FUNCTION get_age_description ( p_birth_date IN DATE , p_as_of_date IN DATE ) RETURN VARCHAR2 IS v_age NUMBER; v_ret_val VARCHAR2(25); BEGIN v_age := TRUNC( MONTHS_BETWEEN ( p_as_of_date, p_birth_date )/12); IF v_age < 18 THEN v_ret_val := 'Minor'; ELSIF v_age = 18 THEN v_ret_val := 'Voting age'; ELSIF v_age = 21 THEN v_ret_val := 'Drinking age'; ELSIF v_age > 66 THEN v_ret_val := 'Eligible for Medicare'; ELSIF v_age BETWEEN 22 AND 65 THEN v_ret_val := 'Adult'; END IF; RETURN v_ret_val; END get_age_description; Introduction of a variable to hold RETURN value SELECT get_age_description ( DATE ' ', SYSDATE ) AS test FROM DUAL; TEST 1 row selected. SELECT get_age_description ( DATE ' ', SYSDATE ) AS test FROM DUAL; This time, the function returned NULL ELSE One possible fix Single point of RETURN from function

34 Using CASE FUNCTION get_age_description ( p_birth_date IN DATE , p_as_of_date IN DATE ) RETURN VARCHAR2 IS v_age NUMBER; v_ret_val VARCHAR2(25); BEGIN v_age := TRUNC( MONTHS_BETWEEN ( p_as_of_date, p_birth_date )/12); CASE WHEN v_age < 18 THEN v_ret_val := 'Minor'; WHEN v_age = 18 THEN v_ret_val := 'Voting age'; WHEN v_age = 21 THEN v_ret_val := 'Drinking age'; WHEN v_age > 66 THEN v_ret_val := 'Eligible for Medicare'; WHEN v_age BETWEEN 22 AND 65 THEN v_ret_val := 'Adult'; END CASE; RETURN v_ret_val; END get_age_description; In any PLSQL development, when you do not expect an “ELSE”, use CASE instead of IF for tighter coding. SELECT get_age_description ( DATE ' ', SYSDATE ) AS test FROM DUAL; SELECT get_age_description ( DATE ' ', SYSDATE ) AS test FROM DUAL; * ERROR at line 1: ORA-06592: CASE not found while executing CASE statement ORA-06512: at "SCOTT.GET_AGE_DESCRIPTION", line 13 Unlike IF /ELSIF, PLSQL CASE requires that one of the branches be executed

35 Bug #5 Declaration section exceptions

36 What will happen here? Division by zero here
DECLARE x NUMBER :=10/0; BEGIN dbms_output.put_line('Successful run.'); EXCEPTION WHEN zero_divide THEN dbms_output.put_line('zero_divide exception handler'); WHEN OTHERS THEN dbms_output.put_line('others exception handler'); END; / Division by zero here Two different handlers Neither of the exception handlers was raised! DECLARE * ERROR at line 1: ORA-01476: divisor is equal to zero ORA-06512: at line 3

37 Handlers Gotchas The exception was raised in the “green” block
But, it was not handled in “green” block It was handled in “blue” block But, at least it was handled! BEGIN DECLARE v NUMBER(1) := 10; v:= 20; EXCEPTION WHEN OTHERS THEN dbms_output.put_line ( 'Handler 1'); END; dbms_output.put_line ( 'Handler 2'); Handler 2

38 What can you do? Avoid variable assignment in declaration section when possible Not possible with CONSTANT Nest blocks in larger blocks so that errors can be handled

39 Bug #6 More FOR LOOP problems

40 The lower number must come first, or the loop is not executed
Loop Bugs BEGIN FOR i IN LOOP dbms_output.put_line ( i ); END LOOP; END; No output. Why? BEGIN FOR i IN LOOP dbms_output.put_line ( i ); END LOOP; END; The lower number must come first, or the loop is not executed -5 -4 -3 -2 -1

41 Even with REVERSE, the lower number must come first
Loop Bugs A backwards loop BEGIN FOR i IN REVERSE 5..1 LOOP dbms_output.put_line ( i ); END LOOP; END ; Again: No output. BEGIN FOR i IN REVERSE 1..5 LOOP dbms_output.put_line ( i ); END LOOP; END ; Even with REVERSE, the lower number must come first 5 4 3 2 1

42 FOR LOOP Endpoints i empno (1) 7369 (2) 7566 (3) 7788 (4) 7876 (5)
DECLARE TYPE number_t IS TABLE OF NUMBER; v_empnos number_t; BEGIN SELECT empno BULK COLLECT INTO v_empnos FROM scott.emp WHERE deptno = 20; FOR i IN v_empnos.FIRST..v_empnos.LAST LOOP -- Loop through records for additional processing dbms_output.put_line ( v_empnos(i) ); END LOOP; END; / i empno (1) 7369 (2) 7566 (3) 7788 (4) 7876 (5) 7902 1 5 7369 7566 7788 7876 7902 PL/SQL procedure successfully completed.

43 FOR LOOP Endpoints No emps in dept 40 i empno Empty array NULL NULL
DECLARE TYPE number_t IS TABLE OF NUMBER; v_empnos number_t; BEGIN SELECT empno BULK COLLECT INTO v_empnos FROM scott.emp WHERE deptno = 40; FOR i IN v_empnos.FIRST..v_empnos.LAST LOOP -- Loop through records for additional processing dbms_output.put_line ( v_empnos(i) ); END LOOP; END; / No emps in dept 40 i empno Empty array NULL NULL DECLARE * ERROR at line 1: ORA-06502: PL/SQL: numeric or value error ORA-06512: at line 10

44 Fix #1 When the array is empty, COUNT returns 0.
DECLARE TYPE number_t IS TABLE OF NUMBER; v_empnos number_t; BEGIN SELECT empno BULK COLLECT INTO v_empnos FROM scott.emp WHERE deptno = 40; FOR i IN 1..v_empnos.COUNT LOOP -- Loop through records for additional processing dbms_output.put_line ( v_empnos(i) ); END LOOP; END; / When the array is empty, COUNT returns 0. It does not return NULL FOR i IN 1..0 LOOP Remember: If the second number is lower than the first number, the FOR LOOP does not execute. PL/SQL procedure successfully completed.

45 Fix #2 Because it’s not a FOR LOOP, the variable MUST be declared.
TYPE number_t IS TABLE OF NUMBER; v_empnos number_t; i NUMBER; BEGIN SELECT empno BULK COLLECT INTO v_empnos FROM scott.emp WHERE deptno = 40; i := v_empnos.FIRST; WHILE i IS NOT NULL LOOP -- Loop through records for additional processing dbms_output.put_line ( v_empnos(i) ); i := v_empnos.NEXT (i); END LOOP; END; / Because it’s not a FOR LOOP, the variable MUST be declared. If the array is empty, this function returns NULL LOOP Exit mechanism When this is called after the last record, it returns NULL, too This is the only way to code this if the array is sparse PL/SQL procedure successfully completed.

46 Bug #7 Implicit COMMIT

47 In case of load error, rollback
The set-up Clear the table Loop through records EMP_MGR EMP Load Transform Clear BEGIN EXECUTE IMMEDIATE 'TRUNCATE TABLE scott.emp_mgr'; FOR rec IN ( SELECT e.ename AS ename , m.ename AS mgrname FROM scott.emp e JOIN scott.emp m ON e.mgr = m.empno ) LOOP INSERT INTO scott.emp_mgr VALUES rec; END LOOP; COMMIT; EXCEPTION WHEN OTHERS THEN dbms_output.put_line ( SQLERRM ); ROLLBACK; END; In case of load error, rollback Load Script to set this up: CREATE TABLE emp_mgr ( ename VARCHAR2(6), mgrname VARCHAR2(6)); INSERT INTO scott.emp VALUES ( 7777, 'O''REILLY', 'CORPORAL', 7902, DATE ' ', 900, NULL, 10); COMMIT; DESC scott.emp_mgr EMP_MGR ENAME VARCHAR2(6) MGRNAME Rollback

48 After the error, the table is empty. Something went wrong
ETL Run SELECT COUNT(*) FROM scott.emp_mgr; COUNT(*) 13 1 row selected. INSERT INTO scott.emp VALUES ( 7777, 'O''REILLY', 'CORPORAL' , 7902, DATE ' ', 900 , NULL, 10); 1 row created. BEGIN EXECUTE IMMEDIATE 'TRUNCATE TABLE scott.emp_mgr'; FOR rec IN ( SELECT e.ename AS ename , m.ename AS mgrname FROM scott.emp e JOIN scott.emp m ON e.mgr = m.empno ) LOOP INSERT INTO scott.emp_mgr VALUES rec; END LOOP; COMMIT; EXCEPTION WHEN OTHERS THEN dbms_output.put_line ( SQLERRM ); ROLLBACK; END; / ORA-12899: value too large for column "SCOTT"."EMP_MGR"."ENAME" (actual: 8, maximum: 6) PL/SQL procedure successfully completed. Now, run ETL procedure After the error, the table is empty. Something went wrong SELECT COUNT(*) FROM scott.emp_mgr; COUNT(*) 1 row selected.

49 Why was the load table empty?
DDL causes implicit COMMIT TRUNCATE TABLE… CREATE INDEX… Remember that EXECUTE IMMEDIATE… ? This is Oracle database behavior Not PLSQL

50 Bug Killers Review Checklist : FOR LOOP variables No_data_found
Parameter defaults Function returned without value Declaration section exceptions More FOR LOOP issues Implicit COMMIT

51 Thank-you! Dan Stober Questions? Comments?

52 Copyright All material contained herein is owned by Daniel Stober, the author of this presentation. This presentation and the queries, examples, and original sample data may be shared with others for educational purposes only in the following circumstances: That the person or organization sharing the information is not compensated, or If shared in a circumstance with compensation, that the author has granted written permission to use this work Using examples from this presentation, in whole or in part, in another work without proper source attribution constitutes plagiarism. Use of this work implies acceptance of these terms


Download ppt "Copyright All material contained herein is owned by Daniel Stober, the author of this presentation. This presentation and the queries, examples, and original."

Similar presentations


Ads by Google