IF statement to start query - plsql

Can I begin a PL/SQL query with an IF statement? I'm trying to execute the proper SELECT statement based on a parameter.

Yes, you can; if the variable/parameter you are trying to compare is already populated with data. Something like this:
DECLARE
v_num1 NUMBER := 5;
v_num2 NUMBER := 3;
v_temp NUMBER;
BEGIN
-- if v_num1 is greater than v_num2 rearrange their values
IF v_num1 > v_num2 THEN
v_temp := v_num1;
v_num1 := v_num2;
v_num2 := v_temp;
END IF;
Provide more information based on what you are actually trying to do.

You can do it by two ways
First
if your_condition then
select * from your_table;
Second
Write your query and add your_condition to where clause of query
select * from your_table where your_condition;

Related

PLS-00382: expression is of wrong type in populating an associative array table

I have this structure of table in my database
Now i want all the values inside of it in a associative array that is indexed by the job_id%type which is a varchar2, so i created this anonymous block first before creating a procedure to test it and i want to populate my associative array with the results:
DECLARE
TYPE jobs_tab_type IS TABLE OF jobs%rowtype INDEX BY jobs.job_id%type;
jobstab jobs_tab_type;
BEGIN
FOR rec IN (SELECT * FROM jobs)
LOOP
jobstab(rec.job_id) := rec.job_id;
END LOOP;
END;
I know for sure that this is a wrong way to do it since I've encountered this error: PLS-00382: expression is of wrong type , but at the same time i don't know if there is any proper way to do this. I have seen the documentation but all the examples were to use a user defined string that will hold the job_id, but the problem is that i need the job_id to be indexed and not a user defined string. Is there any way to fix this problem in PL/SQL?
You don't need the index by INDEX BY jobs.job_id%type. It is making your code more complicated. The INDEX BY determines the index of the collection - that is the character used to indicate the position in the collection. That doesn't need to be a column from the table you are taking a %rowtype from.
Typically an INDEX BY VARCHAR2 is used if you want to reference the collection based on a meaningful string, like capitals('United States') = 'Washingston'
Now, about your question. Here is the answer to the original question:
create table jobs
(job_id VARCHAR2(10)
,job_title VARCHAR2(30)
,min_salary NUMBER
,max_salary NUMBER
);
INSERT INTO jobs (job_id, job_title, min_salary, max_salary) VALUES ('a','CLERK',100,500);
INSERT INTO jobs (job_id, job_title, min_salary, max_salary) VALUES ('b','PRESIDENT',1000,5000);
INSERT INTO jobs (job_id, job_title, min_salary, max_salary) VALUES ('c','SALESMAN',500,800);
DECLARE
TYPE jobs_tab_type IS TABLE OF jobs%rowtype INDEX BY jobs.job_id%type;
jobstab jobs_tab_type;
l_idx VARCHAR2(100);
BEGIN
FOR rec IN (SELECT * FROM jobs)
LOOP
jobstab(rec.job_id) := rec;
END LOOP;
l_idx := jobstab.FIRST;
WHILE (l_idx IS NOT NULL) LOOP
dbms_output.put_line(l_idx ||': '||jobstab(l_idx).job_title);
l_idx := jobstab.NEXT(l_idx);
END LOOP;
END;
/
a: CLERK
b: PRESIDENT
c: SALESMAN
Notice that in this block, it's a bit tedious to loop through the elements, just because the index is not an integer. Now lets look at the same functionality, but using an INDEX BY BINARY_INTEGER:
DECLARE
TYPE jobs_tab_type IS TABLE OF jobs%rowtype INDEX BY BINARY_INTEGER;
jobstab jobs_tab_type;
l_idx NUMBER := 1;
BEGIN
-- example 1: using a cursor for loop
-- FOR rec IN (SELECT * FROM jobs)
-- LOOP
-- jobstab(l_idx) := rec;
-- l_idx := l_idx + 1;
-- END LOOP;
-- example 2: using bulk collect
SELECT * BULK COLLECT INTO jobstab FROM jobs;
FOR r IN 1 .. jobstab.COUNT LOOP
dbms_output.put_line(r ||': '||jobstab(r).job_title);
END LOOP;
END;
/
1: CLERK
2: PRESIDENT
3: SALESMAN
It's simpler to loop through the values of a collection because of the numeric index and it also can be implicitly assigned as well, using the BULK COLLECT statement.
The value of your associative array is the entire record. Hence just remove .job_id from this line of your code.
jobstab(rec.job_id) := rec.job_id;
In other words, the line should be
jobstab(rec.job_id) := rec;
Refer to this db<>fiddle

Struggling to collect and return DBMS_SQL.COLUMN_VALUE using User Defined Type

This is my first question on StackOverflow and I'm self taught so please be gentle.
My goal here is to be able to bulk collect headers/values from a dynamic query/cursor in a generated package via
SELECT *
BULK COLLECT INTO l_cur_val
FROM TABLE (CUSTOM.GET_REF_VAL(l_cursor));
I have this working successfully for column headers in a similar block of code to GET_REF_VAL (simply RETURN l_col_head before entering the /* COLUMN VALUES */ section).
The errors are coming when I'm trying to assign the return value of DBMS_SQL.COLUMN_VALUE into my t_col_val UDT. (Type definitions are in comments)
TYPES
CREATE OR REPLACE TYPE CUSTOM.r_col_val IS OBJECT (l_col_val VARCHAR2(250 byte));
CREATE OR REPLACE TYPE CUSTOM.t_col_val IS TABLE OF CUSTOM.r_col_val;
ERRORS
--l_val(n) := r_col_val(l_dum_val); --returns: ORA-06533: Subscript beyond count
--l_val(n) := l_dum_val; --returns: PLS-00382: expression is of wrong type
Table return function GET_REF_VAL
CREATE OR REPLACE FUNCTION
CUSTOM.GET_REF_VAL
(
p_cursor IN SYS_REFCURSOR
)
RETURN t_col_val
IS
l_val t_col_val := t_col_val();
l_col t_col_head := t_col_head();
n INTEGER := 0;
l_cursor SYS_REFCURSOR := p_cursor;
l_cursor_id INTEGER;
l_dummy INTEGER;
l_col_cnt INTEGER;
l_tab_rec DBMS_SQL.DESC_TAB2;
l_dum_val VARCHAR2(250);
BEGIN
l_cursor_id := DBMS_SQL.TO_CURSOR_NUMBER(l_cursor);
DBMS_SQL.DESCRIBE_COLUMNS2(l_cursor_id, l_col_cnt, l_tab_rec);
/* COLUMN HEADERS */
FOR r IN 1..l_col_cnt
LOOP
l_col.extend;
n := n + 1;
l_col(n) := r_col_head(l_tab_rec(r).col_name);
DBMS_SQL.DEFINE_COLUMN(l_cursor_id, r, l_dum_val, 4000);
END LOOP;
/* COLUMN VALUES */
LOOP
IF DBMS_SQL.FETCH_ROWS(l_cursor_id)> 0 THEN
FOR i IN 1 .. l_col_cnt
LOOP
l_val.extend;
n := n + 1;
DBMS_SQL.COLUMN_VALUE(l_cursor_id, i, l_dum_val);
DBMS_OUTPUT.PUT_LINE(l_dum_val); -- This return l_dum_val with no issues
--l_val(n) := r_col_val(l_dum_val); -- ORA-06533: Subscript beyond count
--l_val(n) := l_dum_val; --PLS-00382: expression is of wrong type
END LOOP;
ELSE
EXIT;
END IF;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(l_cursor_id);
RETURN l_val;
END;
/
Execution block
DECLARE
l_sql_stmt VARCHAR(10000) :=
q'!
SELECT
SYS_CONTEXT('USERENV','OS_USER') AS OS_USER ,
SYS_CONTEXT('USERENV','SESSION_USER') AS SESSION_USER,
SYS_CONTEXT('USERENV','ISDBA') AS ISDBA,
SYS_CONTEXT('USERENV','SID') AS SID,
SYS_CONTEXT('USERENV','CURRENT_SQL') AS CURRENT_SQL,
SYS_CONTEXT('USERENV','DB_NAME') AS DB_NAME,
SYS_CONTEXT('USERENV','HOST') AS HOST,
SYS_CONTEXT('USERENV','IP_ADDRESS') AS IP_ADDRESS,
SYS_CONTEXT('USERENV','SERVICE_NAME') AS SERVICE_NAME
FROM
DUAL
!';
l_cursor SYS_REFCURSOR;
l_cursor_id INTEGER;
l_dummy VARCHAR2(50);
TYPE t_cur_head IS TABLE OF VARCHAR2(250) INDEX BY BINARY_INTEGER;
l_cur_head t_cur_head;
TYPE t_cur_val IS TABLE OF VARCHAR2(250) INDEX BY BINARY_INTEGER;
l_cur_val t_cur_val;
BEGIN
l_cursor := CUSTOM.GET_REF_CUR(l_sql_stmt);
IF l_cursor%ISOPEN
THEN
/* Header fetch works fine */
/*
SELECT *
BULK COLLECT INTO l_cur_head
FROM TABLE (CUSTOM.GET_REF_HEAD(l_cursor));
FOR i IN 1 .. l_cur_head.COUNT
LOOP
DBMS_OUTPUT.PUT_LINE(l_cur_head(i));
END LOOP;
*/
/* Values fetch fails */
SELECT *
BULK COLLECT INTO l_cur_val
FROM TABLE (CUSTOM.GET_REF_VAL(l_cursor));
FOR i IN 1 .. l_cur_val.COUNT
LOOP
DBMS_OUTPUT.PUT_LINE(l_cur_val(i));
END LOOP;
END IF;
END;
So I guess in summary what I want to know is
a) How to handle the return value of dbms_sql.column_value using a user defined type
b) How insert a VARCHAR2 value (l_dum_val) into a UDT object with VARCHAR2 records (l_col_val)
c) Any other obvious errors/bad practices in the code?
Thank you for your time an patience.
The first of your commented-out lines:
--l_val(n) := r_col_val(l_dum_val); -- ORA-06533: Subscript beyond count
gets that error because you are not resetting n to zero before the second loop. You don't really need that counter variable at all though, you can do use l_val.count instead (in both loops).
The second of your commented-out lines:
--l_val(n) := l_dum_val; --PLS-00382: expression is of wrong type
gets that error because the l_val(n) is pointing to an object, which has a string attribute; it isn't pointing directly to a string. So you can assign a new object via its constructor; which is what the first version was trying to do, but it should be:
l_val(l_val.count) := r_col_val(l_dum_val);
Once that object exists you can assign the attribute directly with:
l_val(some_index).l_col_val := r_col_val(l_dum_val);
but you have to create an object before you can access its attributes, and as you only have a default constructor, that probably isn't going to be much use to you in this case.
So with those changes (and some indentation, and refactoring slightly to get rid of the else) this now works:
CREATE OR REPLACE FUNCTION
GET_REF_VAL
(
p_cursor IN SYS_REFCURSOR
)
RETURN t_col_val
IS
l_val t_col_val := t_col_val();
l_col t_col_head := t_col_head();
l_cursor SYS_REFCURSOR := p_cursor;
l_cursor_id INTEGER;
l_dummy INTEGER;
l_col_cnt INTEGER;
l_tab_rec DBMS_SQL.DESC_TAB2;
l_dum_val VARCHAR2(250);
BEGIN
l_cursor_id := DBMS_SQL.TO_CURSOR_NUMBER(l_cursor);
DBMS_SQL.DESCRIBE_COLUMNS2(l_cursor_id, l_col_cnt, l_tab_rec);
/* COLUMN HEADERS */
FOR r IN 1..l_col_cnt
LOOP
l_col.extend;
l_col(l_col.count) := r_col_head(l_tab_rec(r).col_name);
DBMS_SQL.DEFINE_COLUMN(l_cursor_id, r, l_dum_val, 4000);
END LOOP;
/* COLUMN VALUES */
LOOP
IF DBMS_SQL.FETCH_ROWS(l_cursor_id) = 0 THEN
EXIT;
END IF;
FOR i IN 1 .. l_col_cnt
LOOP
l_val.extend;
DBMS_SQL.COLUMN_VALUE(l_cursor_id, i, l_dum_val);
DBMS_OUTPUT.PUT_LINE(l_dum_val);
l_val(l_val.count) := r_col_val(l_dum_val);
END LOOP;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(l_cursor_id);
RETURN l_val;
END;
/
db<>fiddle
Your code suggests you have a separate function to get the headers, so you're duplicating code. You could simplify into one procedure with two out variables instead:
CREATE OR REPLACE PROCEDURE
GET_REF_HEAD_AND_VAL
(
p_cursor IN OUT SYS_REFCURSOR,
p_col OUT SYS.odcivarchar2list,
p_val OUT SYS.odcivarchar2list
)
IS
l_cursor_id INTEGER;
l_col_cnt INTEGER;
l_tab_rec DBMS_SQL.DESC_TAB3;
l_value VARCHAR2(250 byte);
BEGIN
l_cursor_id := DBMS_SQL.TO_CURSOR_NUMBER(p_cursor);
DBMS_SQL.DESCRIBE_COLUMNS3(l_cursor_id, l_col_cnt, l_tab_rec);
/* COLUMN HEADERS */
p_col := SYS.odcivarchar2list();
FOR r IN 1..l_col_cnt
LOOP
p_col.extend;
p_col(p_col.count) := l_tab_rec(r).col_name;
DBMS_SQL.DEFINE_COLUMN(l_cursor_id, r, l_value, 250);
END LOOP;
/* COLUMN VALUES */
p_val := SYS.odcivarchar2list();
LOOP
IF DBMS_SQL.FETCH_ROWS(l_cursor_id) = 0 THEN
EXIT;
END IF;
FOR i IN 1 .. l_col_cnt
LOOP
p_val.extend;
DBMS_SQL.COLUMN_VALUE(l_cursor_id, i, l_value);
--DBMS_OUTPUT.PUT_LINE(l_dum_val);
p_val(p_val.count) := l_value;
END LOOP;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(l_cursor_id);
END;
/
This is using a built-in collection type rather than creating your own object/table types (though you could still create your own collection type; it doesn't need to used objects though).
db<>fiddle

Deal with column list of multiple queries executed at runtime.

I have a list of queries stored in an Oracle DB table. My requirement is to fetch each of those queries one by one and fire them in a procedure and log their start end and elapsed times in another table
My problem is how should I handle the column list as that's going to be different for each of those queries and the number of columns and their datatypes cannot be anticipated at runtime.
Please suggest a way out.
For now, I have written down the code below. Here I have enclosed each query fetched with a count() to avoid the problem. However, the actual time taken for the count() query will be different from the time taken for the original query to execute.
Thanks a lot!
DECLARE
before_time TIMESTAMP;
after_time TIMESTAMP;
elapsed_time_in_ms NUMBER;
stmnt CLOB; --varchar(32000);
counts NUMBER;
sql_no NUMBER;
err_mess VARCHAR2(100);
CURSOR get_queries
IS
SELECT * FROM SLOW_RUNNING_SQL WHERE curr_ind = 1;
FUNCTION get_elapsed_time(
start_time_in TIMESTAMP ,
end_time_in TIMESTAMP )
RETURN NUMBER
AS
l_days NUMBER;
hours NUMBER;
minutes NUMBER;
seconds NUMBER;
milliseconds NUMBER;
BEGIN
<calculates elapsed time in milliseconds and returns that>
RETURN milliseconds ;
END;
BEGIN
dbms_output.put_line(CURRENT_TIMESTAMP);
before_time := SYSTIMESTAMP;
FOR i IN get_queries
LOOP
stmnt := i.SQL_DESC;
sql_no := i.sql_no;
stmnt := 'SELECT count(*) FROM ('||stmnt||') a';
dbms_output.put_line(stmnt);
EXECUTE IMMEDIATE stmnt INTO counts;
after_time := SYSTIMESTAMP;
elapsed_time_in_ms:= get_elapsed_time(before_time,after_time);
dbms_output.put_line(elapsed_time_in_ms);
INSERT
INTO query_performance_log VALUES
(
i.sql_no,
stmnt,
counts,
before_time,
after_time,
elapsed_time_in_ms/1000,
'No exception',
elapsed_time_in_ms );
dbms_output.put_line(stmnt);
dbms_output.put_line(counts);
dbms_output.put_line(after_time);
dbms_output.put_line(TO_CHAR(after_time - before_time));
COMMIT;
END LOOP;
ROLLBACK;
EXCEPTION
WHEN OTHERS THEN
err_mess:= SQLERRM;
INSERT
INTO query_performance_log VALUES
(
sql_no,
stmnt,
0,
NULL,
NULL,
0,
err_mess,
0
);
dbms_output.put_line(SQLERRM);
ROLLBACK;
END;
A solution that might suit you is to select a constant for every line returned by your query and make a bulk collect INTO a collection of varchar2 variables.
Here is what you're looking for:
-- declare a list of varchar2:
CREATE OR REPLACE TYPE t_my_list AS TABLE OF VARCHAR2(100);
-- then use this type in your proc.:
[..]
declare
v_res t_my_list;
[..]
-- then run the query
execute immediate 'SELECT ''x'' FROM ('||stmnt||') '
bulk collect into v_res;
If the columns selected by your queries are "simple", the above should work fair enough to evaluate performances. But if you start calling other functions and procedures for the data you retrieve in the select, then its more complicate.
In this other case, then you should try to work something out to build a concatenation of the columns returned (and enlarge the VARCHAR2(100) in the declaration of t_my_list). This implies you start work on stmnt and extract the columns, a part being the replacement of , by ''||'' or so.

PL/SQL List of items, check if record exists, if yes update if not create

Im trying to learn PL/SQL and I was given an assignment which I am not sure how to tackle.
I am given a list of orders. I want to check my ORDER table for each of them in the following way:
Check if order exists, if no create a record
Check if order fullfilled (0 or 1)
If order is not fullfilled (0), update to 1
I put together a script which I think can do this for one order, but I'm sure it's not very good:
DECLARE
tmp NUMBER;
tmp2 NUMBER;
o_id NUMBER := 999;
BEGIN
/*Checking if order exists */
SELECT COUNT (*)
INTO tmp
FROM ORDERS
WHERE ORDERID = o_id;
IF ( tmp = 0 ) THEN
/* INSERT HERE */
END IF;
SELECT FULLFILLED INTO tmp2
FROM ORDERS
WHERE ORDERID = o_id;
IF (tmp2 = 0) THEN
/* UPDATE... */
END IF;
end;
I would appreciate any advice, what should I look into to make this script efficient? Thank you.
MERGE statement is what you need. It is based on SELECT statement and let's you UPDATE or INSERT data using it's WHEN (NOT) MATCHED THEN clauses. Here's a good explanation with some examples: Oracle Base MERGE Statement.
Here's also some code snippet you might find useful:
DECLARE
o_id NUMBER := 999;
BEGIN
MERGE INTO ORDERS o
USING
(SELECT o_id AS orderid FROM dual) o_id
ON
(o.orderid = o_id.orderid)
WHEN MATCHED THEN
UPDATE SET
o.fulfilled = CASE WHEN o.fulfilled = 0 THEN 1 ELSE o.fulfilled END
WHEN NOT MATCHED THEN
INSERT (fulfilled, <some_other_columns>)
VALUES (1, <values_for_other_columns>);
END;
/
Please read up on the merge statement: https://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_9016.htm
Also called an "upsert". Basically if the row does not exist, insert. If it does, update.
It does what you are trying to do in one statement.

Run a query based on a condition

I am trying to run a query based on the day
For example
if it is the first of the month then select * from thistable
if it is the 2nd of the month then select * from thattable
Etc
What would be the best way to do this?
Apologize in advance if this is vague, new to PL/SQL. I know how to do it in tsql.
I've tried below
DECLARE
DATENUM INT := 1;
begin
if DATENUM = 1
then
select * from thistable;
else
select '* from thattable;
end if;
end;
DECLARE
v NUMBER;
BEGIN
v:=To_number(To_char(SYSDATE,'dd')) ;
IF v=1 THEN
--statement_1-----
ELSIF v=2 THEN
NULL;
--statement_2-----
END IF;
END;
Note : In PL/SQL INTO clause should be there in select.

Resources