PL/SQL Select and Update statement inside OPEN - FOR - oracle

So I have a pl/sql function and I want to select and update table inside open for statement
It looks like this:
table_a
id | status | document_id |
create or replace function a(p_document_id in number)
return sys_refcursor
is
result sys_refcursor;
begin
open result for
select id
from table_a t
where t.document_id = p_document_id;
update table_a t
set t.status = 1
where id in (select id
from table_a t
where t.document_id = p_document_id);
return result;
end;
/
But it is not working. Is there some method to do this? thanks in advance

This is a sample table:
SQL> select * from table_a;
ID DOCUMENT_ID STATUS
---------- ----------- ----------
1 100 0
Function you wrote compiles, but doesn't work because functions - normally - don't do DML:
SQL> select a(100) from dual;
select a(100) from dual
*
ERROR at line 1:
ORA-14551: cannot perform a DML operation inside a query
ORA-06512: at "SCOTT.A", line 11
SQL>
It, though, would work from PL/SQL, e.g.
SQL> declare
2 l_rc sys_refcursor;
3 begin
4 l_rc := a(100);
5 end;
6 /
PL/SQL procedure successfully completed.
SQL> select * from table_a;
ID DOCUMENT_ID STATUS
---------- ----------- ----------
1 100 1
SQL>
But, if you want to be able to call the function from PL/SQL, it has to be an autonomous transaction, which also means that you have to commit within:
SQL> rollback;
Rollback complete.
SQL> create or replace function a(p_document_id in number)
2 return sys_refcursor
3 is
4 result sys_refcursor;
5 pragma autonomous_transaction; --> this
6 begin
7 open result for
8 select id
9 from table_a t
10 where t.document_id = p_document_id;
11
12 update table_a t
13 set t.status = 1
14 where id in (select id
15 from table_a t
16 where t.document_id = p_document_id);
17
18 commit; --> this
19
20 return result;
21 end;
22 /
Function created.
Let's try it:
SQL> select a(100) from dual;
A(100)
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
ID
----------
1
SQL> select * from table_a;
ID DOCUMENT_ID STATUS
---------- ----------- ----------
1 100 1
SQL>
This might, or might not be OK. Autonomous transactions can be tricky and we usually use them for logging purposes, so that their commit doesn't affect the main transaction.

Related

Calling a function in select statement

I have a function:
CREATE OR REPLACE FUNCTION numOfOrders(name VARCHAR2) RETURN sys_refcursor AS
test_cur sys_refcursor;
BEGIN
OPEN test_cur for
SELECT C_CustKey, C_Name, COUNT(*) AS Num_Of_Orders
FROM Customer c
INNER JOIN Orders o
ON o.O_CustKey = c.C_CustKey
WHERE C_Name = name
GROUP BY C_CustKey, C_Name;
RETURN test_cur;
END;
/
the screenshot of the code :
and i called it by typing
SELECT numOfOrders('john') FROM customer;
I get my intended output:
C_CustKey C_Name Num_Of_Orders
0001 john 10
but it gets repeated for the number of rows in my customer table, i can fix this by adding fetch first 1 rows only; but how do I prevent it from happening.
Link to output (I can assure you all 1214 rows are the same) :
Tables
Customer :
Orders :
To cut a long story short: this is wrong:
select numOfOrders('john') from customer;
--------
this
because you're selecting the same dataset for all rows in customer table. What would be better? This:
select numOfOrders('john') from dual;
----
this
Why dual? Because it contains only one row.
A simple example based on Scott's schema (I don't have your data): there are 14 employees in the whole emp table, some of them working in department 10.
Function, similar to yours:
SQL> create or replace function num_of_employees (p_deptno in dept.deptno%type)
2 return sys_refcursor
3 is
4 l_rc sys_refcursor;
5 begin
6 open l_rc for
7 select d.dname,
8 count(*) as num_of_employees
9 from dept d join emp e on e.deptno = d.deptno
10 where d.deptno = p_deptno
11 group by d.dname;
12 return l_rc;
13 end;
14 /
Function created.
This is what you should do - select from dual; the result is just a single line:
SQL> select num_of_employees(10) from dual;
NUM_OF_EMPLOYEES(10)
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DNAME NUM_OF_EMPLOYEES
-------------- ----------------
ACCOUNTING 3 --> this is the result
SQL>
But, if you select from emp (which has 14 rows), you'll get 14 duplicate rows as the result:
SQL> select num_of_employees(10) from emp;
NUM_OF_EMPLOYEES(10)
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DNAME NUM_OF_EMPLOYEES
-------------- ----------------
ACCOUNTING 3
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DNAME NUM_OF_EMPLOYEES
-------------- ----------------
ACCOUNTING 3
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DNAME NUM_OF_EMPLOYEES
-------------- ----------------
ACCOUNTING 3
<snip>
14 rows selected.
SQL>
You asked whether it'll work (the dual table) if function returns more than a single row: it will.
I modified the function so that - if no department is passed to the function - it returns number of employees in all departments (i.e. multiple rows):
SQL> create or replace function num_of_employees (p_deptno in dept.deptno%type)
2 return sys_refcursor
3 is
4 l_rc sys_refcursor;
5 begin
6 open l_rc for
7 select d.dname,
8 count(*) as num_of_employees
9 from dept d join emp e on e.deptno = d.deptno
10 where d.deptno = p_deptno or p_deptno is null --> modified WHERE clause
11 group by d.dname;
12 return l_rc;
13 end;
14 /
Function created.
Calling the function:
SQL> select num_of_employees(null) from dual;
NUM_OF_EMPLOYEES(NUL
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DNAME NUM_OF_EMPLOYEES
-------------- ----------------
ACCOUNTING 3
RESEARCH 5
SALES 6
SQL>

Update second column

I have a table "TEST_TABLE" with two columns TABLE_NAME and RECORD_COUNT.
enter image description here
We want to update the column RECORD_COUNT by taking the total records in table specified in TABLE_NAME.
You could do this with dynamic SQL but why be complicated?
Create a view
Create view my_tables as
Select
'table_1' as "table name",
count(*) as "rows"
From table_1
Add the following for each table
Union all
Select 'table_2',count(*) From table_2
You can then use the view like a table:
Select * from my_tables;
OK, but - why wouldn't you use information Oracle already provides for you? You should regularly gather statistics anyway, so:
SQL> execute dbms_stats.gather_schema_stats(ownname => 'SCOTT', estimate_percent => null);
PL/SQL procedure successfully completed.
and then fetch num_rows from user_tables:
SQL> select table_name, num_rows from user_tables where rownum <= 10;
TABLE_NAME NUM_ROWS
-------------------- ----------
EMP 14
DEPT 4
BONUS 0
SALGRADE 5
DUMMY 1
TBL_ERROR 1
AUDIT_TAB 2
SOURCE_DET 3
EXAMPLE 1
FLIGHT 0
10 rows selected.
SQL>
It can't be much worse than your attempt (due to possible frequent changes to tables' contents; inserts and deletes) because you'd collect your own data periodically (how often?) anyway.
If it has to be your way, then you'd use dynamic SQL, looping through all tables in your schema and actually count number of rows:
SQL> create table test_table
2 (table_name varchar2(30),
3 num_rows number);
Table created.
SQL> create or replace procedure p_test as
2 l_cnt number;
3 begin
4 execute immediate 'truncate table test_table';
5 for cur_R in (select table_name from user_tables) loop
6 execute immediate 'select count(*) from ' ||cur_R.table_name into l_Cnt;
7 insert into test_table (table_name, num_rows) values (cur_r.table_name, l_cnt);
8 end loop;
9 end;
10 /
Procedure created.
Running the procedure and viewing the result:
SQL> exec p_test;
PL/SQL procedure successfully completed.
SQL> select * From test_Table where rownum <= 10;
TABLE_NAME NUM_ROWS
-------------------- ----------
EMP 14
DEPT 4
BONUS 0
SALGRADE 5
DUMMY 1
TBL_ERROR 1
AUDIT_TAB 2
SOURCE_DET 3
EXAMPLE 1
FLIGHT 0
10 rows selected.
SQL>
Note that performance will suffer as number of tables and number of rows stored in them grows.
If I were you, I'd go for the first option.

Getting the SQL ID's of each statement executed in a stored procedure

I have the following Stored Procedure:
create or replace procedure insert_employee_to_dept (f_name IN varchar2, l_name IN varchar2, dept IN varchar2, tier IN char)
is
dept_count number;
t1_count number;
begin
INSERT INTO employees (id, first_name, last_name, department, tier)
VALUES (employee_sequence.NEXTVAL,
f_name,
l_name,
dept,
tier);
select count(*) into dept_count from employees where department = dept;
update dept_info set emp_count = dept_count where id = dept;
select count(*) into t1_count from employees where tier = 1;
update company set tier_one_count = t1_count where name = 'MyCompany';
end;
I enable SQL ID feedback using below statement
set feedback on sql_id;
Finally I run my Stored Procedure as such:
call insert_employee_to_dept('John', 'Doe', 'Finance', '5')
The output of the above command contains the SQL id of the PL/SQL block but this block does not have an execution block which can be queried using DBMS_XPLAN.DISPLAY_CURSOR
Is there a way to get all the SQL ID's that were executed when the stored procedure was called?
Following this answer, I enabled tracing and ran the procedure which generated the trace file on the server. Unfortunately in my use case I do not have access the file system of the database server
You may use all_statements system view to get all the statements executed in PL/SQL unit and their SQL_ID's.
create table t (
id int,
val int
)
/
create or replace procedure test_proc(
p_id in int
)
as
begin
insert into t(id, val) values(p_id, 100);
update t set val = 10;
delete from t where id = p_id;
end;
/
begin
test_proc(1);
end;
/
select
owner
, object_type
, object_name
, type
, line
, sql_id
, text
from all_statements
/
OWNER
OBJECT_TYPE
OBJECT_NAME
TYPE
LINE
SQL_ID
TEXT
DEMO
PROCEDURE
TEST_PROC
DELETE
8
85cd3d95cya5x
DELETE FROM T WHERE ID = :B1
DEMO
PROCEDURE
TEST_PROC
UPDATE
7
d6au94gzv1ydh
UPDATE T SET VAL = 10
DEMO
PROCEDURE
TEST_PROC
INSERT
6
bm74chwppp8w4
INSERT INTO T(ID, VAL) VALUES(:B1 , 100)
If you doing that for testing purpose, you can use dbms_application_info to set module and action, and then use them to select from v$sql/v$sqlarea/v$active_session_history/etc:
SQL> declare
2 n number;
3 begin
4 dbms_application_info.set_module('test_module','test_action');
5 select/*+test1*/ count(*) into n from dual;
6 select/*+test2*/ count(*) into n from dual;
7 dbms_application_info.set_module('','');
8 end;
9 /
PL/SQL procedure successfully completed.
SQL> select sql_id,substr(sql_text,1,50) sqltext50
2 from v$sqlarea
3 where module='test_module'
4 and action='test_action';
SQL_ID SQLTEXT50
------------- ------------------------------------
bavxnddfrxju6 SELECT/*+test2*/ COUNT(*) FROM DUAL
7d8853usybzdg SELECT/*+test1*/ COUNT(*) FROM DUAL
Btw, you can use module/action/client_id also for dbms_monitor:
Performing Application Tracing
DBMS_MONITOR
Or sometimes you can even use v$open_cursor (depends on many factors like open_cursor parameter, etc):
SQL> declare
2 n number;
3 begin
4 dbms_application_info.set_module('test_module','test_action');
5 select/*+test10*/ count(*) into n from dual;
6 select/*+test11*/ count(*) into n from dual;
7 dbms_application_info.set_module('','');
8 end;
9 /
PL/SQL procedure successfully completed.
SQL> select sql_id,substr(sql_text,1,50) sqltext50
2 from v$open_cursor
3 where sid=userenv('sid')
4 and user_name=user
5 and last_sql_active_time is not null
6 order by last_sql_active_time desc
7 fetch first 5 rows only;
SQL_ID SQLTEXT50
------------- ---------------------------------------------------
3669hp0tndbgu declare n number; begin dbms_application_inf
9szagfb62bhgs SELECT/*+test10*/ COUNT(*) FROM DUAL
9h8pabdtrb1wm select sql_id,substr(sql_text,1,50) sqltext50 from
1901dfp1ktg5d SELECT/*+test11*/ COUNT(*) FROM DUAL
bavxnddfrxju6 SELECT/*+test2*/ COUNT(*) FROM DUAL

In Oracle SQL, I would like to call a Oracle stored procedure and then select the value of an OUT parameter as a column result. Is this possible?

CREATE OR REPLACE PROCEDURE myStoredProcedure (idParam IN VARCHAR2,
outputParam OUT VARCHAR2)
AS
BEGIN
SELECT OUTPUTCOL INTO outputParam FROM MyTable WHERE ID = idParam;
END;
DECLARE
v_OutputResults VARCHAR2(20);
BEGIN
myStoredProcedure('123', v_OutputResults);
SELECT v_OutputResults AS ColumnResult FROM DUAL;
END;
If we understand your goal, you should be using a function, not a procedure:
First, we set up the example:
SQL> -- conn system/halftrack#pdb01
SQL> conn scott/tiger#pdb01
Connected.
SQL> --
SQL> CREATE TABLE my_table (
2 user_id number not null,
3 Name varchar2(10)
4 )
5 ;
Table created.
SQL> --
SQL> insert into my_table values (1,'Bob');
1 row created.
SQL> insert into my_table values (2,'Carol');
1 row created.
SQL> insert into my_table values (3,'Ted');
1 row created.
SQL> insert into my_table values (4,'Alice');
1 row created.
SQL> commit;
Commit complete.
SQL> select * from my_table;
USER_ID NAME
---------- ----------
1 Bob
2 Carol
3 Ted
4 Alice
4 rows selected.
Now we create the function, then use it:
SQL> --
SQL> create or replace function my_function (id_param number)
2 return varchar2
3 is
4 v_name varchar2(10);
5 begin
6 select name
7 into v_name
8 from my_table
9 where user_id = id_param
10 ;
11 --
12 return v_name;
13 end;
14 /
Function created.
SQL> show errors
No errors.
SQL> --
SQL> select my_function(1) from dual;
MY_FUNCTION(1)
--------------------------------------------------------------------------------
Bob
1 row selected.
And clean up our example:
SQL> --
SQL> drop table my_table purge;
Table dropped.
No but you can do so using a stored function.

Check if both column and table exist and run queries based on the result

I am trying to run some SQL queries on Oracle database, but before running the query I need to check if both table and column exists. If table exists and column does not exist, then run another query:
if table `testtable` exists and if table has column `testcolumn`
Run a SQL which returns the result
else if table `testtable` exists but column `testcolumn` not present
Run a different sql which also returns the result
else
print some defined string
You can use:
DECLARE
nCount NUMBER;
BEGIN
SELECT COUNT(*)
INTO nCount
FROM USER_TAB_COLS
WHERE TABLE_NAME = 'TESTTABLE' AND
COLUMN_NAME = 'TESTCOLUMN';
IF nCount > 0 THEN
-- Run a SQL which returns the result
ELSE
SELECT COUNT(*)
FROM USER_TABLES
WHERE TABLE_NAME = 'TESTTABLE';
IF nCount > 0 THEN
Run a different sql which also returns the result
ELSE
print some defined string
END;
You'll have to add code to run whatever SQL you're trying to run, and to print whatever message you need.
Best of luck.
Here's one option - check contents of USER_TAB_COLUMNS and - depending on what you find - use refcursor in order to return the result.
SQL> create or replace function f_test
2 return sys_refcursor
3 is
4 l_cnt number;
5 cur_r sys_refcursor;
6 begin
7 -- 1st test - this one fails
8 select count(*)
9 into l_cnt
10 from user_tab_columns
11 where table_name = 'EMP'
12 and column_name = 'DOES_NOT_EXIST';
13
14 if l_cnt > 0 then
15 open cur_r for select ename, job, sal from emp;
16 end if;
17
18 -- 2nd test - this one is OK
19 select count(*)
20 into l_cnt
21 from user_tab_columns
22 where table_name = 'DEPT'
23 and column_name = 'DEPTNO';
24
25 if l_cnt > 0 then
26 open cur_r for select dname, loc from dept;
27 end if;
28
29 return cur_r;
30 end;
31 /
Function created.
SQL> select f_test from dual;
F_TEST
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
DNAME LOC
-------------- -------------
ACCOUNTING NEW YORK
RESEARCH DALLAS
SALES CHICAGO
OPERATIONS BOSTON
SQL>
It has to be some kind of a dynamic code because you can't just write a static SELECT statement that selects non-existent columns as you'd get ORA-00904: "DOES_NOT_EXIST": invalid identifier error.

Resources