Need inner and outer join in one function - oracle

I have one table that holds a record for each customer (main table). I then have a table with additional detail for some customers. The additional detail table sometimes has no records for a record in the main table. Sometimes the detail table has multiple records for a record in the main table & if this is the case I need the most recent record (hence the max subselect).
The trouble is my function only returns values for the few records in the detail table. If I comment out the portion of the function that looks at the detail table and just return the STAT3 value it seems to work. How do I make the second select statment below only apply if there is a result for that query?
create or replace FUNCTION "F_RETURN_STAT" (
N_UNIQUE IN NUMBER)
RETURN VARCHAR2
IS
V_STAT3 varchar2(20);
V_STAT varchar2(20);
V_STAT2 varchar2(20);
D_ACTDATE date;
D_STARTDATE date;
BEGIN
select expire into D_ACTDATE
from main_table a
where a.uniquefield = N_UNIQUE;
IF
D_ACTDATE > SYSDATE
or
D_ACTDATE is null
then
V_STAT :='TRUE';
else
v_STAT :='FALSE';
end if;
select b.startdate into D_STARTDATE
from main_table a, detail_table b
where a.uniquefield= b.main_table_id(+) and
b.main_table_id = N_UNIQUE and
b.uniquefield in
(select max(c.uniquefield) from detail_table c group by main_table_id);
if
D_STARTDATE is not null
then
V_STAT2 :='FALSE';
end if;
if
V_STAT2 ='FALSE'
then
V_STAT3 :='FALSE';
ELSE
V_STAT3 := V_STAT;
end if ;
RETURN(V_STAT3);
end;

I think this version of your function will solve your problem:
CREATE OR REPLACE FUNCTION f_return_stat(n_unique IN NUMBER)
RETURN VARCHAR2 IS
v_stat3 VARCHAR2(20);
v_stat VARCHAR2(20);
v_stat2 VARCHAR2(20);
d_actdate DATE;
d_startdate DATE;
BEGIN
--First Query
SELECT expire
INTO d_actdate
FROM main_table a
WHERE a.uniquefield = n_unique;
IF d_actdate > SYSDATE OR d_actdate IS NULL THEN
v_stat := 'TRUE';
ELSE
v_stat := 'FALSE';
END IF;
BEGIN
--Second Query
SELECT b.startdate
INTO d_startdate
FROM detail_table b
WHERE b.main_table_id = n_unique
AND b.uniquefield IN (SELECT MAX(c.uniquefield)
FROM detail_table c
GROUP BY main_table_id);
EXCEPTION
WHEN NO_DATA_FOUND THEN
d_startdate := NULL;
END;
IF d_startdate IS NOT NULL THEN
v_stat2 := 'FALSE';
END IF;
IF v_stat2 = 'FALSE' THEN
v_stat3 := 'FALSE';
ELSE
v_stat3 := v_stat;
END IF;
RETURN (v_stat3);
END;
In your version of the second query, your join (a.uniquefield= b.main_table_id) and your filter (b.main_table_id = N_UNIQUE) are equivalent, so main_table a can be removed altogether. The only reason to leave it in is to make sure that your query always returns a row. If you use exception handling to catch the NO_DATA_FOUND exception, that need goes away and you can simplify your query to just select from detail_table b.

I believe there could be a more efficient way however this might do the job:
SELECT b.startdate
INTO d_startdate
FROM detail_table b
WHERE b.main_table_id = n_unique
and
(b.uniquefield in
(select max(c.uniquefield) from detail_table c group by main_table_id)
or b.uniquefield is null);

Related

Ref Cursor with sysdate in where clause that use in LOOPING

I want to insert looping with procedure, but when it's execute the error message is "ORA-01555: snapshot too old: rollback segment number with name "" too small". Is that a problem to do looping with sysdate in ref cursor clause?
here is my procedure
PROCEDURE AUTO_REGISTER_EXPIRED_POLICY
(
P_STATUS OUT VARCHAR2
,P_ERROR_MESSAGE OUT VARCHAR2
)
IS
P_USER_ID varchar2(3);
CURSOR C_GET_DTL_POLIS IS
SELECT pm.OFFICE, pm.SUBCLASS, pm.RESV, pm.YEAR, pm.MONTH, pm.SEQUENCE, pm.END_NO
FROM POLICY_MAIN pm
where to_char(trunc(exp_date), 'yyyymm') = to_char(add_months(trunc(sysdate)+0, 3),'yyyymm')
and not exists
;
BEGIN
p_user_id := 'adm';
FOR REC IN C_GET_DTL_POLIS
LOOP
PACKAGE_POLICY_RENEWAL_REVIEW.INSERT_RENEW
(
rec.office,
rec.subclass,
rec.resv,
rec.year,
rec.month,
rec.sequence,
rec.end_no
p_status,
p_error_message
);
END LOOP;
insert into log_renewal_review
select office, subclass, resv, year, month, sequence, end_no,
'LAPSED BY SYSTEM' REMARKS,
sysdate from renewal_review
where to_char(exp_date, 'yyyymm') = to_char(add_months(trunc(sysdate)+0, 3),'yyyymm')
and review_sts = '1'
;
P_STATUS := '1';
P_ERROR_MESSAGE := 'OK';
EXCEPTION WHEN OTHERS THEN
P_STATUS := '0';
P_ERROR_MESSAGE := SUBSTR(SQLERRM, 1, 100)
ROLLBACK;
END AUTO_REGISTER_EXPIRED_POLICY;```
when I look for solution mostly said id because sysdate is change time by time and it changed every time it insert data one by one.
Please Help me to find what is the problem? is it ok to use sysdate in ref cursor for looping?

Assign variable to subquery result inside a PL/SQL block

Here is a bit of more simplified pseudocode describing what I am trying to do:
DECLARE
CURSOR CURSOR_A IS
SELECT FIELD_A1, FIELD_A2
FROM TABLE_A;
vNAME NVARCHAR2(100) := NULL;
BEGIN
FOR RECORD_A IN CURSOR_A LOOP
IF (RECORD_A.FIELD_A1 IS NOT NULL) THEN
vNAME := RECORD_A.FIELD_A1;
ELSE
vNAME := (SELECT FIELD_B
FROM TABLE_B
WHERE TABLE_B.B2 = RECORD_A.A2)
END LOOP;
END;
/
Am I not allowed to have a SELECT statement inside of a PL/SQL block?
All you need - one select. Try it:
declare
name nvarchar2(100) := null;
begin
for row_ in (
select field_a1, field_b from table_a left outer join table_b on b2 = a2
) loop
name := coalesce(row_.field_a1, row_.field_b);
-- do something
end loop;
end;
/
You could still add ... and field_a1 is null to the on-clause if you have too many rows in both tables and concerned about performance.
You are allowed to do select but you need to specify variable that will store selected data
DECLARE
CURSOR CURSOR_A IS
SELECT FIELD_A1, FIELD_A2
FROM TABLE_A;
vNAME NVARCHAR2(100) := NULL;
BEGIN
FOR RECORD_A IN CURSOR_A LOOP
IF (RECORD_A.FIELD_A1 IS NOT NULL) THEN
vNAME := RECORD_A.FIELD_A1;
ELSE
SELECT FIELD_B into vNAME
FROM TABLE_B
WHERE TABLE_B.B2 = RECORD_A.A2;
END LOOP;
END;
/
We required into clause to store a value of select statement inside Begin and end block.
Please look into below example highlighted line:
DECLARE
CURSOR CURSOR_A IS
SELECT FIELD_A1, FIELD_A2
FROM TABLE_A;
vNAME NVARCHAR2(100) := NULL;
BEGIN
FOR RECORD_A IN CURSOR_A LOOP
IF (RECORD_A.FIELD_A1 IS NOT NULL) THEN
vNAME := RECORD_A.FIELD_A1;
ELSE
SELECT FIELD_B into vNAME --**highlighted line**
FROM TABLE_B
WHERE TABLE_B.B2 = RECORD_A.A2
END LOOP;
END;

PL/SQL INSERT Ignored statement

What I have to do is to INSERT in table "info" different content, depending on the select result: if it is one row, no rows or more than one row.
I want to set the outretvalue variable on the exception section, then do the insert in the IF section, depending on outretvalue value.
Anyway, I get an error at compiling saying that f2 function is in an invalid state. I have 2 errors: for the INSERT and for not recognising rowcount. Why?
CREATE OR REPLACE FUNCTION f2 (v_nume employees.last_name%TYPE DEFAULT 'Bell')
RETURN NUMBER
IS
salariu employees.salary%type;
outretvalue number(2) := 0;
BEGIN
SELECT salary
INTO salariu
FROM employees
WHERE last_name = v_nume;
RETURN salariu;
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := 1;
WHEN TOO_MANY_ROWS THEN
--at this row I have 2 errors: for the INSERT and for not recognising rowcount
INSERT INTO info(`no_lines`) VALUES(SQL%ROWCOUNT);
END f2;
/
SELECT f2('King') FROM dual;
Your function:
DECLARE
BEGIN
END;
... something
END;
Add another BEGIN at begin or move your IF inside existing BEGIN END block and remove second END.
EDIT: after clarification
CREATE OR REPLACE FUNCTION f2 (v_nume employees.last_name%TYPE DEFAULT 'Bell')
RETURN NUMBER
IS
salariu employees.salary%type;
outretvalue number(2) := 0;
BEGIN
SELECT salary
INTO salariu
FROM employees
WHERE last_name = v_nume;
RETURN salariu;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN -1;
WHEN TOO_MANY_ROWS THEN
SELECT count(*)
INTO salariu
FROM employees
WHERE last_name = v_nume;
INSERT INTO info(no_lines) VALUES(salariu);
RETURN -2;
WHEN OTHERS THEN
RETURN -3;
END f2;
/
SET SERVEROUTPUT on
DECLARE
l_ret NUMBER;
BEGIN
dbms_output.put_line(f2('Bell'));
dbms_output.put_line(f2('noBell'));
dbms_output.put_line(f2('King'));
END;
Try this. It will definelty help you out.
CREATE OR REPLACE FUNCTION f2(
v_nume employees.last_name%TYPE DEFAULT 'Bell')
RETURN NUMBER
IS
salariu employees.salary%type;
outretvalue NUMBER(2) := 0;
lv_cnt PLS_INTEGER;
BEGIN
SELECT salary INTO salariu FROM employees WHERE last_name = v_nume;
RETURN salariu;
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := 1;
WHEN TOO_MANY_ROWS THEN
SELECT COUNT(1) INTO lv_cnt FROM employees WHERE last_name = v_nume;
INSERT INTO info
( no_lines
) VALUES
( lv_cnt
);
RETURN 2;
WHEN OTHERS THEN
RETURN 3;
END f2;
Oracle saves the compile errors in a table. I use the following query for retrieving the PL/SQL errors in my stored procs/funcs:
SELECT '*** ERROR in ' || TYPE || ' "' || NAME || '", line ' || LINE || ', position ' || POSITION || ': ' || TEXT
FROM SYS.USER_ERRORS
You could try running it and see if it helps identify the error in the function.

Is it possible to use sql%rowcount for SELECT?

The code below may return more than one row. Will sql%rowcount return the number of rows fetched?
select * from emp where empname = 'Justin' and dept='IT'
if sql%rowcount>0
...
This is my sample proc; am I using sql%rowcount in correct way?
CREATE PROCEDURE Procn(in_Hid IN VARCHAR2,outInststatus OUT VARCHAR2,outSockid IN NUMBER,outport OUT VARCHAR2,outIP OUT VARCHAR2,outretvalue OUT NUMBER)
AS
BEGIN
select INST_STATUS into outInststatus from TINST_child where INST_ID = in_Hid and INST_STATUS = 'Y';
if outInststatus = 'Y' then
select PORT_NUMBER,STATIC_IP into outport,outIP from TINST where INST_ID = in_Hid and IP_PORT_STATUS = 'Y';
if sql%rowcount >= 1 then
select SOCK_ID into outSockid from TINST where PORT_NUMBER = outport AND STATIC_IP = outIP;
outretvalue := 0;
else
outretvalue := -12;
end if;
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := -13;
end if;
END;
Yes, you can use SQL%ROWCOUNT. It's valid in PL/SQL.
However, in PL/SQL the result of your query needs to go somewhere e.g. into a PL/SQL table. PL/SQL will never send the result to the output (terminal, window etc.). So SELECT * FROM won't work.
Your code could look like this:
DECLARE
TYPE emp_t ...;
emp_tab emp_t;
BEGIN
SELECT *
BULK COLLECT INTO emp_tab
FROM emp
WHERE empname = 'Justin' AND dept='IT';
IF sql%rowcount > 0 THEN
.. do something ...
END IF;
END;
/
Update:
The updated questions suggests that you're looking for something else.
Option 1: Use exceptions
If there are 0 rows or more than 1 row, these cases are handled separately (as errors):
BEGIN
select PORT_NUMBER,STATIC_IP into outport, outIP
from TINST
where INST_ID = in_Hid AND IP_PORT_STATUS = 'Y';
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := -12;
RETURN;
WHEN TOO_MANY_ROWS THEN
outretvalue := -13;
RETURN;
END;
Option 2: Use aggregations
Using aggregations, the query will always return exactly one row. If now source row matched the WHERE clause, then both result values will be NULL. If there WHERE clause matched more than one row, the maximum will be taken.
Note that this query might return a port number and an IP address that originally were not on the same row.
select MAX(PORT_NUMBER), MAX(STATIC_IP) into outport, outIP
from TINST
where INST_ID = in_Hid AND IP_PORT_STATUS = 'Y';
IF outport IS NULL OR outIP IS NULL THEN
outretvalue := -12;
RETURN;
END IF;
Option 3: Use ROWNUM
This query returns at most one row. If no row matched the WHERE clause, an exception is thrown and needs to be handled:
BEGIN
select PORT_NUMBER, STATIC_IP into outport, outIP
from TINST
where INST_ID = in_Hid AND IP_PORT_STATUS = 'Y'
AND ROWNUM = 1;
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := -12;
RETURN;
END;
Based on your comment
If 2nd 'select' query returns more than one row i want to take the first one and process with it
... this ought to work, but perhaps not quite as you expect, as you haven't defined what the 'first one' means.
CREATE PROCEDURE Procn(in_Hid IN VARCHAR2, outInststatus OUT VARCHAR2,
outSockid IN NUMBER, outport OUT VARCHAR2, outIP OUT VARCHAR2,
outretvalue OUT NUMBER)
AS
BEGIN
select INST_STATUS into outInststatus
from TINST_child
where INST_ID = in_Hid and INST_STATUS = 'Y';
-- no need to check if outInstatus is Y, that's all it can be here
-- restricting with `rownum` means you'll get at most one row, so you will
-- not get too_many_rows. But it will be an arbitrary row - you have no
-- criteria to determine which of the multiple rows you want. And you can
-- still get no_data_found which will go to the same exception and set -12
select PORT_NUMBER, STATIC_IP into outport, outIP
from TINST
where INST_ID = in_Hid and IP_PORT_STATUS = 'Y'
and rownum < 2;
-- no need to check sql%rowcount; it can only be 1 here
-- not clear if this can return multiple rows too, and what should happen
-- if it can; could use rownum restriction but with the same caveats
select SOCK_ID into outSockid
from TINST
where PORT_NUMBER = outport AND STATIC_IP = outIP;
outretvalue := 0;
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := -12;
END;
The exception handler applies to the whole block. If any of the select statements find no rows, the no_data_found exception will be handled by that block and will set outretvalue to -12.
If you want a different outretvalue for each select then you can wrap them in sub-blocks, each with their own exception handling section:
CREATE PROCEDURE Procn(in_Hid IN VARCHAR2, outInststatus OUT VARCHAR2,
outSockid IN NUMBER, outport OUT VARCHAR2, outIP OUT VARCHAR2,
outretvalue OUT NUMBER)
AS
BEGIN
BEGIN
select INST_STATUS into outInststatus
from TINST_child
where INST_ID = in_Hid and INST_STATUS = 'Y';
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := -12;
END;
BEGIN
select PORT_NUMBER, STATIC_IP into outport, outIP
from TINST
where INST_ID = in_Hid and IP_PORT_STATUS = 'Y'
and rownum < 2;
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := -13;
END;
BEGIN
select SOCK_ID into outSockid
from TINST
where PORT_NUMBER = outport AND STATIC_IP = outIP;
EXCEPTION
WHEN NO_DATA_FOUND THEN
outretvalue := -14;
END;
outretvalue := 0;
END;
You only need to do that if the caller needs to know which select failed, and if you never really expect any of them to fail then it's probably more common not to catch the exception at all and let the caller see the raw no_data_found and decide what to do. Depends what the exception condition means to you and your application though.

Dynamically add where clauses to a cursor in oracle

I have plsql procedure which accepts certain parameters e.g. v_name, v_country, v_type.
I wish to have a cursor with a select statement like this:
select column from table1 t1, table2 t2
where t1.name = v_name
and t1.country = v_country
and t1.id = t2.id
and t2.type = v_type
If certain parameters are empty can I only add the relevant where clauses to the cursor? Or is there a better way to accomplish this?
The best way to use this is with DBMS_SQL.
You create a string that represents your SQL statement. You still use bind variables. It's painful.
It goes something like this (I haven't compiled this, but it should be close) :-
CREATE OR REPLACE FUNCTION find_country( v_name t1.country%TYPE,
v_type t2.type%TYPE) /* Hmm, column called type? */
DECLARE
v_SQL varchar2(2000);
v_select INTEGER; /* "Pointer" to a DBMS_SQL select statement */
v_execute INTEGER;
BEGIN
v_SQL := 'select column from table1 t1, table2 t2 ||
'where t1.id = t2.id';
IF v_name IS NOT NULL THEN
v_SQL := v_SQL || ' AND t1.country = :v_name'
END IF;
IF v_type IS NOT NULL THEN
v_SQL := v_SQL || ' AND t2.type = :v_type';
END IF;
/* Setup Cursor */
v_select := dbms_sql.open_cursor;
dbms_sql.parse( v_select, v_SQL, DBMS_SQL.native);
IF v_name IS NOT NULL THEN
dbms_sql.bind_variable( v_select, ':v_name', v_name );
END IF;
IF v_type IS NOT NULL THEN
dbms_sql.bind_variable( v_select, ':v_type', v_type );
END IF;
DBMS_SQL.DEFINE_COLUMN(v_select, 1, v_column); /* This is what we have selected */
/* Return value from EXECUTE is undefined for a SELECT */
v_execute := DBMS_SQL.EXECUTE( v_select );
IF DBMS_SQL.FETCH_ROWS( v_select ) > 0 THEN
/* A row was found
DBMS_SQL.COLUMN_VALUE( v_select, 1, v_column);
/* Tidy Up */
DBMS_SQL.CLOSE_CURSOR(v_select);
RETURN v_ID_address;
ELSE
DBMS_SQL.CLOSE_CURSOR(v_select);
/* No row */
RETURN NULL;
END IF;
EXCEPTION
WHEN OTHERS THEN
IF DBMS_SQL.IS_open(v_select) THEN
DBMS_SQL.CLOSE_CURSOR(v_select);
END IF;
RAISE;
END;
This approach is so painful compared to just writing the SQL inline that unless you have heaps of columns sometimes it's just easier writing a couple of different versions using this syntax:
FOR r IN (SELECT blah FROM blah WHERE t1 = v_t1) LOOP
func( r.blah );
END LOOP;
It's not directly what you're asking, but it may be an acceptable solution:
select column from table1 t1, table2 t2
where
(v_name is null or t1.name = v_name)
and (v_country is null or t1.country = v_country)
and t1.id = t2.id
and (v_type is null or t2.type = v_type)
One way would be to build up your query as a string then use execute immediate
The best way to do this would be to use Oracle's Application Context feature, best defined as best performance and security.
The faster way would be what hamishmcn suggested, using EXECUTE IMMEDIATE. I'd choose that over WW's suggestion of DBMS_SQL every time.
Another way that's quickest to write but won't perform as well would be something like this:
select column from table1 t1, table2 t2
where t1.name = nvl(v_name, t1.name)
and t1.country = nvl(v_country, t1.country)
and t1.id = t2.id
and t2.type = nvl(v_type, t2.type)
You do not have to use dbms_sql to solve this problem
and you can still use normal cursor by using a ref cursor.
Sample:
DECLARE
TYPE cursor_ref IS REF CURSOR;
c1 cursor_ref;
r1 table1.column%type;
BEGIN
l_sql := 'select t1.column from table1 t1, table2 t2 where t1.id = t2.id ';
if v_name is not null then
l_sql := l_sql||' and t1.name = '||v_name ;
end if;
if v_country is not null then
l_sql := l_sql||' and t1.country = '||v_country';
end if;
if v_type is not null then
l_sql := l_sql||' and t2.type = '||v_type';
end if;
open c1 for l_sql;
loop
fetch c1 into r1;
exit when c1%notfound;
-- do something
end loop;
close c1;
end;
/
You can make this better by binding the variables with the command 'using' like this:
open c1 for l_sql using v_name, v_country;

Resources