How to Capture Run Time in an Oracle Stored Procedure - sql

I'm wanting to log the run time of certain SELECT stored procedures in Oracle. I've broken this down into the following steps.
STEP 1 - Get StartTime
STEP 2 - Insert into the LOG Table that the proc is running
STEP 3 - Get Inserted RowId
STEP 4 - Run the SELECT statement within the Proc
STEP 5 - Get End Time
STEP 5 - Update the row (in LOG Table) that was inserted, with the "Total Run Time".
IMPORTANT NOTE: The SELECT statement takes several minutes to run.
What happens is this:
The procedure runs
A row gets inserted into the LOG table
The LOG table immediately gets updated with the total run time.
The SELECT statement continues to take 5 minutes to run
After the SELECT statement completes, the result data finally returns.
The LOG table should NOT be updated until the entire procedure completes.
Basically what's happening is that the procedure immediately inserts, then updates the LOG table "BEFORE" the SELECT statement finishes.
I've tried wrapping and nesting additional BEGIN and END statements. The stored procedure still runs the "UPDATE" statement at the end of the procedure BEFORE the SELECT statement returns.
CREATE OR REPLACE EDITIONABLE PROCEDURE SP_CUSTOMERDATA_GET (
PARAM_USERID IN VARCHAR2,
PARAM_FIRSTNAME IN VARCHAR2,
PARAM_LASTNAME IN VARCHAR2
OUTPUT OUT types.cursor_type)
AS
BEGIN
DECLARE
l_Id Number;
l_StartTime TIMESTAMP;
l_EndTime TIMESTAMP;
l_TotalTime Number;
BEGIN
l_StartTime:= systimestamp;
INSERT INTO PROC_LOG (SPNAME, PARM1, PARM2, PARM3)
VALUES ('SP_CUSTOMERDATA_GET',I_USERNAME, PARAM_USERID, PARAM_FIRSTNAME, PARAM_LASTNAME)
RETURNING ID INTO l_Id;
COMMIT;
OPEN OUTPUT FOR
SELECT *
FROM CUSTOMER
WHERE USERID=PARAM_USERID
AND FIRSTNAME=PARAM_FIRSTNAME
AND LASTNAME=PARAM_LASTNAME;
l_EndTime:= systimestamp;
l_TotalTime:= extract(second from (l_EndTime-l_StartTime));
--ISSUE: This statement runs before the SELECT statement above completes
UPDATE PROC_LOG
SET RUNTIME_SECONDS=l_TotalTime
WHERE ID=l_Id;
COMMIT;
END;
END SP_CUSTOMERDATA_GET;
Is there a property I can set in the PROC, to force the procedure to not run the next command until the prior command finishes. It doesn't make sense that the procedure doesn't run in order?

I re-read the question and Justin's comments, and based on his proposal came up with a code solution for it.
First a general setup of database structures:
FSITJA#db01>create table customer (userid,
2 firstname,
3 lastname) as
4 select level, 'John', 'Doe'
5 from dual
6 connect by level <= 1000000;
Table created.
FSITJA#db01>create table PROC_LOG (id number generated as identity,
2 SPNAME varchar2(30),
3 PARM1 varchar2(100),
4 PARM2 varchar2(100),
5 PARM3 varchar2(100),
6 RUNTIME_SECONDS number);
Table created.
FSITJA#db01>create or replace type tp_customer_row as object (userid number,
2 firstname varchar2(100),
3 lastname varchar2(100));
4 /
Type created.
FSITJA#db01>create or replace type tp_customer as table of tp_customer_row;
2 /
Type created.
FSITJA#db01>create or replace package types as
2 type cursor_type is ref cursor return customer%rowtype;
3 end;
4 /
Package created.
Then we will need a stored procedure with the autonomous transaction to log the time, and the table function that allows us to query data from a collection. We can pass a cursor into the function in a Select to test that it works:
FSITJA#db01>create or replace procedure sp_log_customerdata_get(proc_log_id in proc_log.id%type, starttime in timestamp) as
2 pragma autonomous_transaction;
3 begin
4 UPDATE PROC_LOG
5 SET RUNTIME_SECONDS=extract(second from (systimestamp-starttime))
6 WHERE ID=proc_log_id;
7 COMMIT;
8 end;
9 /
Procedure created.
FSITJA#db01>create or replace function fn_customerdata_get(cust_cursor types.cursor_type,
2 proc_log_id in proc_log.id%type,
3 starttime in timestamp) return tp_customer
4 pipelined as
5 in_cust_rec customer%rowtype;
6 out_cust_rec tp_customer_row := tp_customer_row(null, null, null);
7 begin
8 loop
9 fetch cust_cursor into in_cust_rec;
10 exit when cust_cursor%notfound;
11 out_cust_rec.userid := in_cust_rec.userid;
12 out_cust_rec.firstname := in_cust_rec.firstname;
13 out_cust_rec.lastname := in_cust_rec.lastname;
14 pipe row(out_cust_rec);
15 end loop;
16 close cust_cursor;
17 sp_log_customerdata_get(proc_log_id, starttime);
18 return;
19 end;
20 /
Function created.
FSITJA#db01>select *
2 from table(fn_customerdata_get(cursor(select userid,
3 firstname,
4 lastname
5 from customer
6 where rownum < 5),
7 null,
8 systimestamp));
USERID FIRSTNAME LASTNAME
---------- --------------- ---------------
1 John Doe
2 John Doe
3 John Doe
4 John Doe
Now the original procedure, which will call the function passing a ref cursor, and then forward this cursor out in its parameter for the client application:
FSITJA#db01>CREATE OR REPLACE PROCEDURE SP_CUSTOMERDATA_GET (
2 PARAM_USERID IN VARCHAR2,
3 PARAM_FIRSTNAME IN VARCHAR2,
4 PARAM_LASTNAME IN VARCHAR2,
5 OUTPUT OUT types.cursor_type) AS
6 l_Id Number;
7 l_StartTime TIMESTAMP;
8 l_EndTime TIMESTAMP;
9 l_TotalTime Number;
10 l_CustResult tp_customer;
11 BEGIN
12 l_StartTime:= systimestamp;
13 INSERT INTO PROC_LOG (SPNAME, PARM1, PARM2, PARM3)
14 VALUES ('SP_CUSTOMERDATA_GET', PARAM_USERID, PARAM_FIRSTNAME, PARAM_LASTNAME)
15 RETURNING ID INTO l_Id;
16 COMMIT;
17 open output for
18 select *
19 from table(fn_customerdata_get(cursor(SELECT userid,
20 firstname,
21 lastname
22 FROM CUSTOMER
23 WHERE USERID=PARAM_USERID
24 AND FIRSTNAME=PARAM_FIRSTNAME
25 AND LASTNAME=PARAM_LASTNAME),
26 l_Id,
27 l_StartTime
28 )
29 );
30 END SP_CUSTOMERDATA_GET;
31 /
Procedure created.
And finally a piece of code to test that only after the client application fetches data from the table function there will be a log entry for time elapsed:
FSITJA#db01>declare
2 v_output types.cursor_type;
3 v_runtime_seconds number;
4 type tp_cust_table is table of customer%rowtype;
5 v_cust_table tp_cust_table;
6 begin
7 SP_CUSTOMERDATA_GET (1, 'John', 'Doe', v_output);
8 select runtime_seconds
9 into v_runtime_seconds
10 from proc_log
11 where id = 1;
12 dbms_output.put_line('Runtime before client fetches: ' || v_runtime_seconds);
13 fetch v_output
14 bulk collect into v_cust_table;
15 select runtime_seconds
16 into v_runtime_seconds
17 from proc_log
18 where id = 1;
19 dbms_output.put_line('Runtime AFTER client fetches: ' || v_runtime_seconds);
20 end;
21 /
Runtime before client fetches:
Runtime AFTER client fetches: .118791
PL/SQL procedure successfully completed.

The problem is that your procedure doesn't actually run the SELECT statement. Your procedure just opens the cursor which parses the statement and gets the statement handle. It doesn't cause the database to actually execute the statement. That happens when the caller goes to fetch data from the cursor that is returned. When the procedure finishes, it has no idea whether the caller is ever going to fetch data from the cursor, whether it is going to just fetch the first 10 rows, or whether it is going to eventually fetch every row. If your goal is to measure how long it takes to fetch the data from the cursor, you'd want to add logging to the caller not to this procedure.
Of course, you could also just run the SELECT statement separately. If the actual query is anything close to what you posted, I'd strongly wager that you're missing an index on customer. I'd guess that userID is unique so if there was an index on userID, that query should run in a few milliseconds.

Related

Stored procedure variable error in PLSQL when declaring variables

Using Oracle 11g when creating the following stored procedure
create or replace PROCEDURE sp_EqualVote(AREA IN NVARCHAR2, DATEOFVOTE IN DATE)
IS
DECLARE test nvarchar(255);
BEGIN
SELECT
AREA,
DATEOFVOTE,
CASE
WHEN (REMAINVOTES = LEAVEVOTES) THEN REMAINVOTES
END AS EqualVote
INTO test
FROM VOTING
WHERE REMAINVOTES = LEAVEVOTES;
END;
END;
I encounter the following error, I'm not quite sure where to go
PLS-00103: Encountered the symbol "DECLARE" when expecting one of the following: begin function pragma procedure subtype type <an identifier> <a double-quoted delimited-identifier> current cursor delete exists prior external language The symbol "begin" was substituted for "DECLARE" to continue.
I'm a university student and not really that familiar with PLSQL. The idea is the stored procedure should display if an an area has equal votes, given the area and date in the procedure then display an equalvotes labeled column with a value of 50
Quite a few mistakes.
you don't need DECLARE within the named PL/SQL procedure
parameters names should differ from column names, so you'd rather use - for example - p_area in nvarchar2, p_dateofvote in date
if you select 3 columns, you have to put them INTO 3 variables - you've declared only one, so either declare two more, or remove AREA and DATEOFOTE from SELECT
what are those parameters used for? Usually, as a part of the WHERE clause - which is not the case in your code
pay attention to number of rows returned by the SELECT statement. If you're selecting into a scalar variable, make sure that it returns only one row
what will you do with TEST variable, once you get its value? Currently, nothing
you've got an END that is a surplus.
Therefore, consider something like this which should at least compile (depending on table description):
SQL> create table voting (area nvarchar2(10),
2 dateofvote date,
3 remainvotes nvarchar2(10),
4 leavevotes nvarchar2(10));
Table created.
SQL> create or replace procedure
2 sp_equalvote(p_area in nvarchar2, p_dateofvote in date)
3 is
4 test nvarchar2(255);
5 begin
6 select
7 case when remainvotes = leavevotes then remainvotes end
8 into test
9 from voting
10 where remainvotes = leavevotes
11 and area = p_area
12 and dateofvote = p_dateofvote;
13 end;
14 /
Procedure created.
SQL>
[EDIT]
After reading the comment, perhaps you'd rather use a function.
Some sample values:
SQL> insert into voting values (1, date '2019-02-20', 100, 15);
1 row created.
SQL> insert into voting values (1, date '2019-03-10', 300, 300);
1 row created.
Function:
SQL> create or replace function
2 sp_equalvote(p_area in nvarchar2, p_dateofvote in date)
3 return nvarchar2
4 is
5 test nvarchar2(255);
6 begin
7 select
8 case when remainvotes = leavevotes then 'draw'
9 else 'not equal'
10 end
11 into test
12 from voting
13 where area = p_area
14 and dateofvote = p_dateofvote;
15
16 return test;
17 end;
18 /
Function created.
SQL>
Testing:
SQL> select * From voting;
AREA DATEOFVOTE REMAINVOTE LEAVEVOTES
---------- ---------- ---------- ----------
1 20.02.2019 100 15
1 10.03.2019 300 300
SQL> select sp_equalvote(1, date '2019-02-20') res from dual;
RES
--------------------
not equal
SQL> select sp_equalvote(1, date '2019-03-10') res from dual;
RES
--------------------
draw
SQL>
DECLARE is not allowed in the body of a PL/SQL procedure. The IS or AS serves the purpose of delimiting where the variable declaration section starts - so your procedure should be
create or replace PROCEDURE sp_EqualVote(AREA IN NVARCHAR2, DATEOFVOTE IN DATE)
IS
test nvarchar(255);
BEGIN
SELECT
AREA,
DATEOFVOTE,
CASE
WHEN (REMAINVOTES = LEAVEVOTES) THEN REMAINVOTES
END AS EqualVote
INTO test
FROM VOTING
WHERE REMAINVOTES = LEAVEVOTES;
END;
You also had an extra END, which I removed.
Best of luck.

Using Table type in IN-clause in PLSQL procedure

I have a procedure which takes table type input parameter. Now I have to use this parameter in IN-clause of SELECT query.
CREATE TYPE ids IS TABLE OF NUMBER;
CREATE PROCEDURE (emp_ids IN ids) IS
CURSOR IS (SELECT * FROM EMPLOYEES WHERE EMP_ID IN (SELECT * FROM TABLE(emp_ids)); .....
But I found that this code is not going to work because local collection types cannot be used in an SQL statement.
Is there any alternate way to achieve using table type parameter in a SELECT statement?
According to what you have posted, you are declaring collection as schema object, not the local type. This means that you shouldn't have any problems of using it. Here is an example:
-- collection type as schema object
SQL> create or replace type ids is table of number;
2 /
Type created
SQL> create or replace procedure proc1 (emp_ids IN ids)
2 IS
3 cursor c is (
4 select first_name
5 from employees
6 where employee_id in (select column_value
7 from table(emp_ids)
8 )
9 );
10 begin
11 for i in c
12 loop
13 dbms_output.put_line(i.first_name);
14 end loop;
15 end;
16 /
Procedure created
SQL> exec proc1(ids(101, 103, 200));
Neena
Alexander
Jennifer
PL/SQL procedure successfully completed

oracle call stored procedure inside select

I'm working on a query (a SELECT) and I need to insert the result of this one in a table.
Before doing the insert I have some checking to do, and if all columns are valid, I will do the insert.
The checking is done in a stored procedure. The same procedure is used somewhere else too.
So I'm thinking using the same procedure to do my checks.
The procedure does the checkings and insert the values is all OK.
I tryied to call the procedure inside my SELECT but it does not works.
SELECT field1, field2, myproc(field1, field2)
from MYTABLE.
This kind of code does not works.
I think it can be done using a cursor, but I would like to avoid the cursors.
I'm looking for the easiest solution.
Anybody, any idea ?
use a PL/SQL loop:
BEGIN
FOR c IN (SELECT field1, field2 FROM mytable) LOOP
my_proc(c.field1, c.field2);
END LOOP;
END;
SQL can only use functions in the projection: it needs something which returns a value. So you are going to have to write some functions. That's the bad news. The good news is, you can re-use all the investement in your stored procedures.
Here is a procedure which enforces a completely just business rule: only managers can have a high salary.
SQL> create or replace procedure salary_rule
2 ( p_sal in emp.sal%type
3 , p_job in emp.job%type)
4 is
5 x_sal exception;
6 begin
7 if p_sal > 4999 and p_job != 'MANAGER' then
8 raise x_sal;
9 end if;
10 exception
11 when x_sal then
12 raise_application_error(-20000, 'Only managers can earn that much!');
13 end salary_rule;
14 /
Procedure created.
SQL>
Because it is a procedure we cannot use it in a SELECT statement; we need to wrap it in a function. This function just calls the stored procedure. It returns the input parameter P_SAL. In other words, if the salary is valid (according to the rules) it will be returned. Otherwise the function will re-hurl the stored procedure's exception.
SQL> create or replace function validate_salary
2 ( p_sal in emp.sal%type
3 , p_job in emp.job%type)
4 return emp.sal%type
5 is
6 begin
7 salary_rule(p_sal, p_job);
8 return p_sal;
9 end validate_salary;
10 /
Function created.
SQL>
The function has to return a value which we want to insert into our table. It cannot return some meaningless phrase like "salary okay". Also, if we want to validate two columns we need a separate function for each, even if there is a relationship between them and we use the same stored procedure to validate them both. Good use for the DETERMINISTIC keyword.
Here's the test: plumbers cannot earn 5000 spondulicks ....
SQL> insert into emp
2 (empno
3 , ename
4 , job
5 , deptno
6 , sal )
7 select
8 emp_seq.nextval
9 , 'HALL'
10 , 'PLUMBER'
11 , 60
12 , validate_salary(5000, 'PLUMBER')
13 from dual
14 /
, validate_salary(5000, 'PLUMBER')
*
ERROR at line 12:
ORA-20000: Only managers can earn that much!
ORA-06512: at "APC.SALARY_RULE", line 12
ORA-06512: at "APC.VALIDATE_SALARY", line 7
SQL>
... but managers can (because they deserve it):
SQL> insert into emp
2 (empno
3 , ename
4 , job
5 , deptno
6 , sal )
7 select
8 emp_seq.nextval
9 , 'HALL'
10 , 'MANAGER'
11 , 60
12 , validate_salary(5000, 'MANAGER')
13 from dual
14 /
1 row created.
SQL>
Note that the hurled exception is crucial to this working. We cannot write some bizarre IF SALARY IS VALID THEN INSERT logic in our SQL statement. So, if the stored procedure doesn't raise an exception but instead returns some wimpy error status the wrapping function will have to interpret the output and hurl its own exception.
You can't use stored procedures in SELECT statement.
You can use functions for that.
As I understand you are calling insert in your SP, so take into consideration that you can's use INSERT/UPDATE in function body. But if you need to do some checks you can use function which will do that checks and use that function in your select statement.

default value, oracle sp call

I have an oralcle SP forced on me that will not accept an empty parameter in an update. So if I wanted to set a value back to the default of ('') it will not let me pass in the empty string. Is there a keyword you can use such as default, null, etc that oracle would interpret back to the default specified for a particular column?
Sometimes things are just as simple as you hope they might be.
First, a table with a default value ...
SQL> create table t23 (
2 id number not null primary key
3 , col_d date default sysdate not null )
4 /
Table created.
SQL> insert into t23 values (1, trunc(sysdate, 'yyyy'))
2 /
1 row created.
SQL> select * from t23
2 /
ID COL_D
---------- ---------
1 01-JAN-10
SQL>
Next a procedure which updates the default column ...
SQL> create or replace procedure set_t23_date
2 ( p_id in t23.id%type
3 , p_date in t23.col_d%type )
4 is
5 begin
6 update t23
7 set col_d = p_date
8 where id = p_id;
9 end;
10 /
Procedure created.
SQL>
... but which doesn't work as we would like:
SQL> exec set_t23_date ( 1, null )
BEGIN set_t23_date ( 1, null ); END;
*
ERROR at line 1:
ORA-01407: cannot update ("APC"."T23"."COL_D") to NULL
ORA-06512: at "APC.SET_T23_DATE", line 6
ORA-06512: at line 1
SQL>
So, let's try adding a DEFAULT option ...
SQL> create or replace procedure set_t23_date
2 ( p_id in t23.id%type
3 , p_date in t23.col_d%type )
4 is
5 begin
6 if p_date is not null then
7 update t23
8 set col_d = p_date
9 where id = p_id;
10 else
11 update t23
12 set col_d = default
13 where id = p_id;
14 end if;
15 end;
16 /
Procedure created.
SQL>
... and lo!
SQL> exec set_t23_date ( 1, null )
PL/SQL procedure successfully completed.
SQL>
SQL> select * from t23
2 /
ID COL_D
---------- ---------
1 28-FEB-10
SQL>
I ran this example on an 11g database. I can't remember when Oracle introduced this exact support for DEFAULT, but it has been quite a while (9i???)
edit
The comments are really depressing. The entire point of building PL/SQL APIs is to make it easier for application developers to interact with the database. That includes being sensible enough to rewrite stored procedures when necessary. The big difference between building something out of software and, say, welding cast-iron girders together is that software is malleable and easy to change. Especially when the change doesn't alter the signature or behaviour of an existing procedure, which is the case here.
The procedure that's been forced on you:
create or replace procedure notEditable(varchar2 bar) as
begin
--update statement
null;
end;
How to use:
begin
notEditable(bar=>null);
end;
I didn't actually compile, but I believe this is the correct syntax.

web form insert and trigger before insert

I have a oracle table called:
create table InsertHere(
generate_id varchar2(10),
name varchar2(100)
);
I have to insert data in this table from a web from. In the web form there are two
elements:
type and name
. I have to generate 'generate_id' from the type user has selected.
Task :
Before insert into the InsertHere table i have to generate the generate_id. Then i have to insert
'generate_id' and 'name' into the InsertHere table. BUT THE GENERATION OF ID MUST BE DONE INSIDE THE
DATABASE(may be with a procedure).
Question:
How this thing can be done effectively?
I need suggestions.
Thanks in advance
It all depends what you mean by "generating ID". The ID is I presume a primary key, so its value must be unique regardless of TYPE. So what rules do you want to apply to its generation?
Here is an indicative approach. This uses a sequence to get a value and prepends a character, depending on type.
SQL> create or replace function generator
2 (p_type varchar2
3 , p_name in inserthere.name%type)
4 return inserthere.generate_id%type
5 is
6 c char(1);
7 return_value inserthere.generate_id%type;
8 begin
9 case p_type
10 when 'EXTREME' then
11 c := 'X';
12 when 'REGULAR' then
13 c := 'R';
14 when 'JUMBO' then
15 c := 'J';
16 else
17 c := 'P';
18 end case;
19
20 insert into inserthere
21 ( generate_id,
22 name)
23 values
24 (c || lpad(trim(to_char(my_seq.nextval)), 9, '0')
25 , p_name )
26 returning generate_id into return_value;
27
28 return return_value;
29 end;
30 /
Function created.
SQL>
here it is in action
SQL> var n varchar2(10)
SQL> exec :n := generator ('EXTREME', 'ABC')
PL/SQL procedure successfully completed.
SQL> print n
N
--------------------------------
X000000001
SQL> exec :n := generator ('WHATEVER', 'SOMETHING')
PL/SQL procedure successfully completed.
SQL> print n
N
--------------------------------
P000000002
SQL>
You can vary the precise implementation, depending on how you want to call it. As is often the case, details matter and more information will tend to result in a more relevant answer.