SELECT INTO do not store null in to the variables - sql

I was trying to check the values from the two tables, and I only want to proceed to the next query only if the value is found in TableA. There is something wrong in the below logic, also when there is not data, it goes to exception instead of storing NULL into the variables.
SET SERVEROUTPUT ON;
SET FEEDBACK OFF;
SPOOL temp.txt;
DECLARE
v_ATM TableA.CODE%TYPE := NULL;
v_TBL TableB.CODE%TYPE := NULL;
BEGIN
SELECT TableA.CODE, TableB.CODE INTO v_ATM,v_TBL FROM TableA LEFT JOIN TableB ON TableA.CODE = TableB.CODE WHERE TableA.CODE = 'ABC';
IF (v_ATM IS NULL) AND (v_TBL IS NULL) THEN
DBMS_OUTPUT.PUT_LINE('No value found');
ELSIF (v_ATM IS NOT NULL) AND (v_TBL IS NOT NULL) THEN
DBMS_OUTPUT.PUT_LINE('Found in both');
ELSIF (v_ATM IS NULL) AND (v_TBL IS NOT NULL) THEN
DBMS_OUTPUT.PUT_LINE('Found in Table B');
ELSIF (v_ATM IS NOT NULL) AND (v_TBL IS NULL) THEN
DBMS_OUTPUT.PUT_LINE('Found in Table A');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Exception');
END;
Alternately I was trying:
SELECT TableA.CODE INTO v_ATM FROM TableA WHERE TableA.CODE = 'ABC';
SELECT TableB.CODE INTO v_TBL FROM TableB WHERE TableB.CODE = 'ABC';
but still, v_ATM does not store NULL if ABC is not found, and goes to exception.

--instead of selcting code col to get null/not null, select count(1) to get either 0/1 as output w/o errors
with
TableA as (
--select 'ABC' as code from dual
--union all
select 'DEF' as code from dual
),
TableB as (
--select 'ABC' as code from dual
--union all
select 'DEF' as code from dual
),
q1 as (
SELECT 1 as id, count(1) as cnt FROM TableA WHERE TableA.CODE = 'ABC'
),
q2 as (
SELECT 1 as id, count(1) as cnt FROM TableB WHERE TableB.CODE = 'ABC'
)
select
cast(
case
when q1.cnt = 0 and q2.cnt = 0 then 'Both missing'
when q1.cnt = 1 and q2.cnt = 0 then 'TableA present, TableB missing'
when q1.cnt = 0 and q2.cnt = 1 then 'TableA missing, TableB present'
when q1.cnt = 1 and q2.cnt = 1 then 'Both present'
else 'NA'
end as varchar2(40)) as output_txt
from q1 inner join q2 on (q1.id = q2.id)
;
Play with the sql by hiding ABC and DEF and union all lines of code as desired to get different outputs for different conditions.

You're selecting from a row, not a scalar value so if there is no row there is no value.
If you SELECT MAX(TableA.CODE) ... WHERE ...
You will get a value to use

Related

How do I update the two different tables using if and else condition in Oracle SQL server?

Here is the Query which I need to convert into the Procedure.
MERGE INTO table1 SAI
USING
<IF> :SITE_PROJECTS_ID: != null &&:SITE_PROJECTS_ID: != 0 <THEN>(SELECT * from table2)AX
ON (SAI.SITE_INFO_ID=AX.SITE_INFO_ID)
WHEN MATCHED THEN UPDATE SET
SAI.column1 = AX.column1,
SAI.column2 = AX.column2,
SAI.LAST_MODIFIED_BY = AX.LAST_MODIFIED_BY,
SAI.LAST_MODIFIED_DATE = SYSDATE
<ELSE>
DUAL ON (SAI.SITE_INFO_ID=:SITE_INFO_ID:)
WHEN MATCHED THEN UPDATE SET
<IF> :value1: != null && :value2: != '' <THEN> SAI.SITE_TRAKER_SITE_ID = :value3:, <ENDIF>
SAI.LAST_MODIFIED_DATE = sysdate
<ENDIF>"
Above merge statement needs to get converted into the below PL/SQL format:
DECLARE v_rowcount NUMBER DEFAULT 0;
BEGIN
FOR ax IN ( SELECT * FROM table2 )
LOOP
UPDATE table1 SET status = ax.status
WHERE project_id = ax.site_projects_id;
v_rowcount := SQL%rowcount;
IF ( v_rowcount = 0 )
THEN INSERT INTO table1 ( fuze_project_id, status )
VALUES ( ax.site_projects_id, ax.status ); v_rowcount := 0;
END IF;
END LOOP;
END;/
I am confused for updating two tables as when the condition met I need a different table and on else I am getting DUAL table, and appropriately updation takes place.
Please guide me about how to proceed and accomplish this task.
Just use an IF-ELSE statement and split the MERGE statement into two:
BEGIN
IF :SITE_PROJECTS_ID != 0 THEN
MERGE INTO table1 SAI
USING table2 AX
ON (SAI.SITE_INFO_ID=AX.SITE_INFO_ID)
WHEN MATCHED THEN
UPDATE
SET SAI.column1 = AX.column1,
SAI.column2 = AX.column2,
SAI.LAST_MODIFIED_BY = AX.LAST_MODIFIED_BY,
SAI.LAST_MODIFIED_DATE = SYSDATE;
ELSIF :value1 IS NOT NULL AND :value2 IS NOT NULL THEN
UPDATE table1
SET SITE_TRAKER_SITE_ID = :value3,
LAST_MODIFIED_DATE = sysdate;
END IF;
END;
/
Note: != NULL will never be true; you want IS NOT NULL. Also, in Oracle, '' is identical to NULL.

Oracle optimize select after update status performance

I have a store procedure that will
Update maximum 500 rows status from 0 to 1
Return those rows to program via cursor
Here is my store procedure code
PROCEDURE process_data_out (
o_rt_cursor OUT SYS_REFCURSOR
) IS
v_limit NUMBER;
l_data_ids VARCHAR2(32000);
BEGIN
v_limit := 500; -- limit 500
l_data_ids := '';
-- Create loop to get data
FOR i IN (
SELECT *
FROM
(
SELECT id FROM
TBL_DATA a
WHERE
a.created_at BETWEEN SYSDATE - 0.5 AND SYSDATE + 0.1
AND a.status = 0
AND a.phone NOT IN (SELECT phone FROM TBL_BIG_TABLE_1)
AND a.phone NOT IN (SELECT phone FROM TBL_BIG_TABLE_2 WHERE IS_DENY = 1)
ORDER BY
priority
)
WHERE
ROWNUM <= v_limit
) LOOP
BEGIN
-- Build string of ids like id1,id2,id3,
l_data_ids := l_data_ids
|| i.id
|| ',';
-- update row status to prevent future repeat
UPDATE TBL_DATA
SET
status = 1
WHERE
id = i.id;
END;
END LOOP;
COMMIT;
-- If string of ids length >0 open cursor to take data
IF ( length(l_data_ids) > 0 )
THEN
-- Cut last comma id1,id2,id3, --> id1,id2,id3
l_data_ids := substr(l_data_ids,1,length(l_data_ids) - 1);
-- open cursor
OPEN o_rt_cursor FOR
SELECT
id,
phone
FROM
TBL_DATA a
WHERE
a.id IN (
SELECT
to_number(column_value)
FROM
XMLTABLE ( l_data_ids )
);
END IF;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END process_data_out;
I want to optimize this performance and here is my question
Should I replace in by exists
Replace
AND a.phone NOT IN (SELECT phone FROM TBL_BIG_TABLE_1)
AND a.phone NOT IN (SELECT phone FROM TBL_BIG_TABLE_2 WHERE IS_DENY = 1)
by
AND NOT Exists (SELECT phone FROM TBL_BIG_TABLE_1 where TBL_BIG_TABLE_1.phone = a.phone)
AND NOT Exists (SELECT phone FROM TBL_BIG_TABLE_2 WHERE TBL_BIG_TABLE_2.phone = a.phone and IS_DENY = 1)
Is there a better way than
Save a string of ids like id1,id2,id3 after update row status
Open cursor by select from string of ids
I appreciate for any suggestion.
Thank for your concern
Row-by-row processing is always slower and you are also creating the string of ids, which again takes time so overall performance is going down.
You can use the collection DBMS_SQL.NUMBER_TABLE to store the updated ids from the UPDATE statement using the RETURNING clause and use it in the cursor query.
Also, I have changed your update statement so that it does not use NOT IN and uses the LEFT JOINS and ROW_NUMBER analytical function for increasing the performance as follows:
CREATE OR REPLACE PROCEDURE PROCESS_DATA_OUT (
O_RT_CURSOR OUT SYS_REFCURSOR
) IS
V_LIMIT NUMBER;
L_DATA_IDS DBMS_SQL.NUMBER_TABLE;
BEGIN
V_LIMIT := 500; -- limit 500
UPDATE TBL_DATA A
SET A.STATUS = 1
WHERE A.ID IN (
SELECT ID
FROM ( SELECT ID,
ROW_NUMBER() OVER(ORDER BY PRIORITY) AS RN
FROM TBL_DATA B
LEFT JOIN TBL_BIG_TABLE_1 T1 ON T1.PHONE = B.PHONE
LEFT JOIN TBL_BIG_TABLE_2 T2 ON T2.IS_DENY = 1 AND T2.PHONE = B.PHONE
WHERE B.CREATED_AT BETWEEN SYSDATE - 0.5 AND SYSDATE + 0.1
AND B.STATUS = 0
AND T1.PHONE IS NULL
AND T2.PHONE IS NULL)
WHERE RN <= V_LIMIT ) RETURNING ID BULK COLLECT INTO L_DATA_IDS;
OPEN O_RT_CURSOR FOR SELECT ID, PHONE
FROM TBL_DATA A
WHERE A.ID IN (SELECT COLUMN_VALUE FROM TABLE ( L_DATA_IDS ));
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END PROCESS_DATA_OUT;
/

Converting Merge clause with Bulk collect/FORALL in pl/sql

I wrote a procedure where the data gets updated/inserted simultaneously to the destination table from source table. The procedure is working fine for less no of records, but when i try to execute more records its taking more time to perform the operation.
Can we convert merge clause with bulk collect where the logic remains same ? i dint find any useful resources.
I have attached my merge procedure .
create or replace PROCEDURE TEST1 (
p_array_size IN NUMBER
) IS
CURSOR dtls IS SELECT DISTINCT
account_num
FROM
table1
WHERE
rprtd_till_dt = (
SELECT
dt - 1
FROM
dates
WHERE
id = 'odc'
);
TYPE data_tbl IS TABLE OF dtls%rowtype;
data data_tbl;
BEGIN
DECLARE
v_noofDays NUMBER:=0;
currentDt DATE;
BEGIN
SELECT dt INTO currentDt FROM dates WHERE id = 'odc';
BEGIN
OPEN dtls;
LOOP
FETCH dtls BULK COLLECT INTO data LIMIT p_array_size;
EXIT WHEN data.COUNT = 0;
FOR i IN 1..data.COUNT
LOOP
IF(TRUNC(data(i).creation_dt,'MM') = TRUNC(currentDt,'MM')) THEN
v_noofDays := currentDt - 1 - data(i).creation_dt;
ELSE
v_noofDays := currentDt - TRUNC(currentDt,'MM');
END IF;
MERGE INTO table1 updtbl USING ( SELECT
d.*
FROM
table2 d,
(
SELECT
b.prdct_id,
FROM
table3 a,
table2 b
WHERE
a.ir_id = b.ir_id
AND a.price_component_id = b.price_component_id
AND a.financial_institution_id = b.financial_institution_id
GROUP BY
b.prdct_id,
) e
WHERE
d.prdct_id = e.prdct_id
AND d.bndng_typ = data(i).bndng_typ
AND d.bndng_val = data(i).bndng_val
AND d.financial_institution_id = data(i).financial_institution_id
AND d.prdct_id = data(i).prdct_id
AND d.prdct_sub_id = data(i).prdct_sub_id
AND d.instrmnt_id = data(i).instrmnt_id
)
inp ON (
updtbl.POS_NUM = data(i).POS_NUM
AND updtbl.POS_TYPE = data(i).POS_TYPE
AND updtbl.PRICE_COMPONENT_ID = inp.PRICE_COMPONENT_ID
AND updtbl.RPRTD_TILL_DT = data(i).RPRTD_TILL_DT
)
WHEN NOT MATCHED THEN
INSERT VALUES (
data(i).loan_account_num,
inp.ir_id,
inp.price_component_id,
)
WHEN MATCHED THEN
update SET SEQ_NUM=1,
NET_INTRST_AMT=round(data(i).curr_loan_bal*inp.price_component_value*v_noofDays/36000,2),
DM_BTID=200
WHERE SEQ_NUM=2;
COMMIT;
END LOOP;
END LOOP;
CLOSE dtls;
END;
END;
END TEST1;
/
If anyone can help me to guide the syntax on how to achieve the above procedure using bulk collect will be helpful.
I know its a bit late, but use the following for future if you haven't solved it yet
drop table projects;
create table projects (
proj_id integer not null primary key,
proj_title varchar2(20)
);
insert into projects (proj_id, proj_title) values (1, 'Project One');
insert into projects (proj_id, proj_title) values (2, 'Project Two');
commit;
select *
from projects;
declare
type varray_t is varray(2) of projects%rowtype;
arr varray_t;
begin
with test_data as (select 2 as proj_id, 'New Project Two' as proj_title from dual
union all select 3 as proj_id, 'New Project Three' as proj_title from dual)
select proj_id, proj_title
bulk collect into arr
from test_data;
forall i in arr.first .. arr.last
merge into projects
using (select arr(i).proj_id as proj_id,
arr(i).proj_title as proj_title
from dual) mrg
on (projects.proj_id = mrg.proj_id)
when matched then update set projects.proj_title = mrg.proj_title
when not matched then insert (proj_id, proj_title) values (mrg.proj_id, mrg.proj_title);
dbms_output.put_line(sql%rowcount || ' rows merged');
commit;
end;
I hope this will give you kind of idea. Avoid the copy and paste and check the syntax.
create or replace PROCEDURE TEST1 (
p_array_size IN NUMBER
) IS
CURSOR dtls IS SELECT DISTINCT
account_num
FROM
table1
WHERE
rprtd_till_dt = (
SELECT
dt - 1
FROM
dates
WHERE
id = 'odc'
);
TYPE data_tbl IS TABLE OF dtls%rowtype;
data data_tbl;
BEGIN
DECLARE
v_noofDays NUMBER:=0;
currentDt DATE;
BEGIN
SELECT dt INTO currentDt FROM dates WHERE id = 'odc';
BEGIN
OPEN dtls;
LOOP
FETCH dtls BULK COLLECT INTO data LIMIT p_array_size;
EXIT WHEN data.COUNT = 0;
FORALL rec in data.first .. data.last
MERGE INTO table1 updtbl USING (
SELECT
d.* FROM
table2 d,(
SELECT
b.prdct_id
FROM
table3 a,
table2 b
WHERE
a.ir_id = b.ir_id
AND a.price_component_id = b.price_component_id
AND a.financial_institution_id = b.financial_institution_id
GROUP BY
b.prdct_id
) e
WHERE
d.prdct_id = e.prdct_id
AND d.bndng_typ = data(rec).bndng_typ
AND d.bndng_val = data(rec).bndng_val
AND d.financial_institution_id = data(rec).financial_institution_id
AND d.prdct_id = data(rec).prdct_id
AND d.prdct_sub_id = data(rec).prdct_sub_id
AND d.instrmnt_id = data(rec).instrmnt_id
)
inp ON (
updtbl.POS_NUM = data(rec).POS_NUM
AND updtbl.POS_TYPE = data(rec).POS_TYPE
AND updtbl.PRICE_COMPONENT_ID = data(rec).PRICE_COMPONENT_ID
AND updtbl.RPRTD_TILL_DT = data(rec).RPRTD_TILL_DT
)
WHEN NOT MATCHED THEN
INSERT VALUES (
data(rec)
)
WHEN MATCHED THEN
update SET SEQ_NUM=1,
NET_INTRST_AMT=round(data(rec).curr_loan_bal*inp.price_component_value*v_noofDays/36000,2),
DM_BTID=200
WHERE SEQ_NUM=2;
END LOOP;
CLOSE dtls;
END;
END;
END TEST1;
Merge is always better than forall for atomic updates.
A simplistic use case is
https://ograycoding.wordpress.com/2012/10/13/oracle-merge-v-bulk-collect-and-forall/

How can I have multiple CASE in select statements in PL/SQL function?

I'm trying to write a function that has 2 case statements. For background, a user can have either an A, B, or both A and B (but on separate lines, which is why I can't use a single case statement, unless I use LISTAGG, which I was told not to do for this.
Sample Data:
User State
1 A
1 B
2 A
3 B
SQL
CREATE OR REPLACE Function F_Calc_State(code Varchar2, id Number, time varchar2) Return Varchar2 AS
Calc_State(10) := null;
l_A varchar2(10) := null;
l_B varchar2(10) := null;
BEGIN
SELECT CASE WHEN state = 'A' THEN 'A'
ELSE null
END into l_A,
CASE WHEN state = 'B' THEN 'B'
ELSE null
END into l_B
FROM TABLE1
WHERE state in ('A', 'B')
AND TABLE1_CODE = code
AND TABLE1_ID = id
AND TABLE1_TIME = time;
CASE WHEN l_A = 'A' and l_B = 'B' then 'AB'
WHEN l_A = 'A' THEN 'A'
WHEN 1_B = 'B' THEN 'B'
ELSE stafford_recip_ind :='NEITHER';
END CASE;
RETURN Calc_State;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 'NO DATA';
WHEN OTHERS THEN
RETURN SQLERRM ;
END F_Calc_State;`
For wanted results, when I enter user 1, I want AB to be returned, for user 2 = A, and user 3 = B. I also tried having two different select statement blocks but couldn't get that to work either, it would just hit the exception handler for some reason. Thanks!
The following code would return NULL if no data is found:
CREATE OR REPLACE Function F_Calc_State (
in_code Varchar2,
in_id Number,
in_time varchar2 -- "time" as a string is highly suspicious
) Return Varchar2
AS
v_ab varchar2(10) := null;
BEGIN
SELECT MAX(CASE WHEN t1.state = 'A' THEN 'A' END) ||
MAX(CASE WHEN t1.state = 'B' THEN 'B' END)
INTO v_AB
FROM TABLE1 t1
WHERE t1.state in ('A', 'B') AND
t1.TABLE1_CODE = in_code AND
t1.TABLE1_ID = in_id AND
t1.TABLE1_TIME = in_time;
RETURN(v_ab);
END; -- F_Calc_State
However, this does not return return an error if no data is found.
CREATE OR REPLACE Function F_Calc_State (
in_code Varchar2,
in_id Number,
in_time varchar2 -- "time" as a string is highly suspicious
) Return Varchar2
AS
v_ab varchar2(10) := null;
BEGIN
SELECT MAX(CASE WHEN t1.state = 'A' THEN 'A' END) ||
MAX(CASE WHEN t1.state = 'B' THEN 'B' END)
INTO v_AB
FROM TABLE1 t1
WHERE t1.state in ('A', 'B') AND
t1.TABLE1_CODE = in_code AND
t1.TABLE1_ID = in_id AND
t1.TABLE1_TIME = in_time;
GROUP BY t1.TABLE1_CODE; -- this will return no rows if there are no matches
RETURN(v_ab);
EXCEPTION
WHEN NO_DATA_FOUND THEN RETURN 'NO DATA';
WHEN OTHERS THEN RETURN SQLERRM ;
END; -- F_Calc_State
A very trivial approach could be this:
It's not clear if you want to check for existance of records or not, as in your exception block you return "NO DATA" while in your case statement you have the value "NEITHER".. anyway, you can adjust this accordingly:
CREATE OR REPLACE Function F_Calc_State( code varchar2
, id number
, time varchar2
) return varchar2
as
l_count_a number;
l_count_b number;
l_result varchar2(10);
begin
select count(*)
into l_count_a
from table1
where state = 'A'
and table1_code = id
and table1_time = time;
select count(*)
into l_count_b
from table1
where state = 'B'
and table1_code = id
and table1_time = time;
if l_count_a = 0 and l_count_b = 0 then l_result := 'NEITHER';
elsif l_count_a > 0 and l_count_b = 0 then l_result := 'A';
elsif l_count_a = 0 and l_count_b > 0 then l_result := 'B';
elsif l_count_a > 0 and l_count_b > 0 then l_result := 'AB';
else l_result := '';
end if;
return l_result;
end F_Calc_State;
Use below code, Now you will get only a single row of result.
SELECT MAX(CASE WHEN state = 'A' THEN 'A'
ELSE null END),
MAX(CASE WHEN state = 'B' THEN 'B'
ELSE null END)
INTO l_A, l_B
FROM TABLE1
WHERE state in ('A', 'B')
AND TABLE1_CODE = code
AND TABLE1_ID = id
AND TABLE1_TIME = time;

how to set a null value to default in oracle sql

How do I set an empty set or null value to a default value like 1?
So far, I have this statement, but in case I get null values i want to handle that:
select case when count(*)=0
then 0
else 1
end OUTPUT
from TESTTBL
where timestamp = to_char(sysdate-1, 'yyyymmdd')||'0000';
Do you mean to check for Null value and set as some default, if so
select nvl(column_name,'DEFAULT') from TESTBL where timestamp = to_char(sysdate-1, 'yyyymmdd')||'0000';
SELECT CASE WHEN EXISTS
( SELECT *
FROM TESTTBL
WHERE timestamp = to_char(sysdate-1, 'yyyymmdd') || '0000'
)
THEN 1
ELSE 0
END AS OUTPUT
FROM dual
EDIT
Added FROM dual as Oracle does not allow SELECT without FROM clause.
Here you go
SELECT DECODE(count(*),0,0,
1) OUTPUT
FROM TESTTBL
WHERE TIMESTAMP = TO_CHAR(SYSDATE-1, 'yyyymmdd')||'0000';
Use Decode like
SELECT supplier_name,
decode(supplier_id, 10000, 'Google',
10001, 'Microsoft'
'Sony') result
FROM suppliers;
equivalent to
IF supplier_id = 10000 THEN
result := 'Google';
ELSIF supplier_id = 10001 THEN
result := 'Microsoft';
ELSE
result := 'Sony';
END IF;
Or Use coalesce
SELECT coalesce( address1, address2) result
FROM suppliers;
which is equivalent to
IF address1 is not null THEN
result := address1;
ELSIF address2 is not null THEN
result := address2;
ELSE
result := null;
END IF;