PLSQL - Searching for record in a Nested Table that was Bulk Collected - sql

I used bulk collect to fetch records into a nested table. I want to search for a record with exists method but it's not working out. I then found out the exists method uses index and does not look for the values. Do I need to go across each record and search for a match? Is there a shorter way to do it because I am going to use the same logic for large set of records?
I read in websites that bulk collect doesn't work properly with an associative array when using a varchar as a key so I used nested tables instead. Also, I don't want to read each record and store it in a hashmap as it degrades performance.
Create table sales(
name varchar2(100)
)
insert into sales(name) values('Test');
insert into sales(name) values('alpha');
insert into sales(name) values(null);
declare
type sales_tab is table of varchar2(1000);
t_sal sales_tab;
begin
select name bulk collect into t_sal from sales;
if(t_sal.exists('Test')) THEN
dbms_output.put_line('Test exists');
END IF;
dbms_output.put_line(t_sal.count);
end;

exists() function tells you if a particular element with integer or varchar2(for associative arrays index by varchar2 collections ) index of a collection exists. It does not test for membership. To be able to check if a collection contains an element with specific value member of condition can be used:
SQL> declare
2 type sales_tab is table of varchar2(1000);
3 t_sal sales_tab;
4 begin
5 select name
6 bulk collect into t_sal
7 from sales;
8
9 if('Test' member of t_sal) THEN
10 dbms_output.put_line('Test exists');
11 END IF;
12
13 dbms_output.put_line(t_sal.count);
14 end;
15 /
Test exists
3
PL/SQL procedure successfully completed

Related

Use column name as key for PL/SQL associative array when updating another column

I have an Excel table with two columns of data. Column A are codes, column B are the corresponding country names. I turned it into an associative array, ueln_country.
Now the task is to update HORSE table's column COUNTRY_OF_RESIDENCE. Horses have a column UELN where first three letters correspond to the codes in the Excel table.
I have to check if the code exists in the Excel table. If it does, I have to update HORSE.country_of_residence with a CLASSIFICATOR.code where CLASSIFICATOR.name = **the corresponding country in column B** andCLASSIFICATOR.dom_code = 'ISOCODE'`.
First try gets the error
PLS-00201: identifier 'UELN' must be declared
As I understood, it's because I can only use declared variables in PL/SQL statement.
declare
type TBL_UELN_COUNTRY is table of varchar2(50) index by varchar2 (3);
test TBL_UELN_COUNTRY;
ueln_country TBL_UELN_COUNTRY;
begin
ueln_country('008') := 'ALBAANIA';
ueln_country('010') := 'ANTARKTIS';
ueln_country('011') := 'ANTARKTIS';
....
update HORSE
set COUNTRY_OF_RESIDENCE=
when (...dummy_case...) then
(select code from meta.classifcator
where dom_code = 'ISOCODE'
and name = ueln_country(substr(UELN, 1, 3)))
where UELN is not null;
end;
/
Second try.
So because of the first error I tried to somehow declare the variables.
I knew it wouldn't work (ORA-01422: exact fetch returns more than requested number of rows) but made it to show where my idea is going:
declare
type TBL_UELN_COUNTRY is table of varchar2(50) index by varchar2 (3);
test TBL_UELN_COUNTRY;
ueln_country TBL_UELN_COUNTRY;
v_ueln horse.UELN%TYPE;
begin
select UELN into v_ueln from HORSE;
ueln_country('008') := 'ALBAANIA';
ueln_country('010') := 'ANTARKTIS';
ueln_country('011') := 'ANTARKTIS';
....
update HORSE
set COUNTRY_OF_RESIDENCE=
when (...dummy_case...) then
(select code from meta.classifcator
where dom_code = 'ISOCODE'
and name = ueln_country(substr(v_ueln, 1, 3)))
where UELN is not null;
end;
/
So I want pick a value from associative array where the key = substr(specific_horse.UELN, 1, 3).
Searched through Google and Stack for hours and didn't find an answer.
The ugly and very slow working solution was just where I didn't make the associate array and made 400+ cases for every Excel table row in the form like when -key- then select code from meta.classificator where dom_code = 'ISOKOOD' and name = -value-
associative array can not be used in SQL.
If you use expression like array(index) in SQL then, in fact, PL/SQL engine gets value by index and then result is bound into SQL engine before execution of the SQL statement.
More specifically
declare
type TBL_UELN_COUNTRY is table of varchar2(50) index by varchar2 (3);
test TBL_UELN_COUNTRY;
dummy varchar2(30);
begin
test('GBP') := 'UK';
test('USD') := 'USA';
select /*+ qwerty */ test('GBP')
into dummy
from dual;
end;
/
If we check binds for a cursor we see that actual bind value has a type VARCHAR(128) - :B1. test('GBP') in PL/SQL code is passed as bind variable B1.
SQL> column sql_text format a50
SQL> select sbc.datatype_string, sql_text
2 from v$sql s join v$sql_bind_capture sbc
3 on s.sql_id = sbc.sql_id
4 where lower(sql_text) not like '%v$sql%'
5 and lower(sql_fulltext) like 'select %qwerty%';
DATATYPE_STRING SQL_TEXT
--------------- --------------------------------------------------
VARCHAR2(128) SELECT /*+ qwerty */ :B1 FROM DUAL
SQL engine knows nothing about associative array and apparently it cannot pass and index value to array and get an element of the array back.
If you still want to use associative array to look-up some values you can declare package variable and a getter function (you may also want to implement the logic to handle a case when there is no element in array for a given index - otherwise you'll get run-time exception in such case).
create or replace package pkg as
function GetCountry(idx in varchar2) return varchar2;
end pkg;
/
sho err
create or replace package body pkg as
type TBL_UELN_COUNTRY is table of varchar2(50) index by varchar2 (3);
test pkg.TBL_UELN_COUNTRY;
function GetCountry(idx in varchar2) return varchar2 as
begin return test(idx); end;
-- initializing
begin
test('GBP') := 'UK';
test('USD') := 'USA';
end pkg;
/
sho err
And finally
SQL> set serveroutput on
SQL> declare
2 dummy varchar2(30);
3 begin
4 with t(idx) as (select 'GBP' from dual)
5 select pkg.GetCountry(t.idx)
6 into dummy
7 from t;
8 dbms_output.put_line(dummy);
9 end;
10 /
UK
PL/SQL procedure successfully completed.

PL/SQL Statement Ignored. ORA-22905: cannot access rows from a non-nested table item

I tried to select values from the nested table and bulk collecting into an associative array collection. When I try to bulk collect oracle throwing the above exception(PL/SQL: SQL Statement ignored
PL/SQL: ORA-22905: cannot access rows from a non-nested table
item) though I fetch the data from the nested table.
It is not happening in all the cases. When the same package compiled in the different client database, Some case it is not throwing an error and in some environment, it is throwing an error. Can you please help what was the exact issue.
I have not attached the entire package. Instead provided the case where the issue occurs.
DECLARE
TYPE rc_rec_multiset IS record (
rc_id NUMBER,
doc_num VARCHAR2(100)
);
TYPE rc_type IS TABLE OF rc_rec_multiset;
TYPE tab_rec_type_multiset IS
TABLE OF rc_rec_multiset INDEX BY pls_integer;
rc_tab_type rc_type := rc_type() ;
rc_tab_type_dist_rc tab_rec_type_multiset;
BEGIN
SELECT DISTINCT rc_id,
doc_num BULK COLLECT
INTO rc_tab_type_dist_rc
FROM TABLE(rc_tab_type);
END;
You cannot do this using SQL; an associative array is a PL/SQL data type and cannot be used in the SQL scope. In a similar vein, collections defined in the PL/SQL scope cannot be used in SQL scope (in 11g and earlier) - you either need to define collections in the SQL scope (Note - you cannot do this for associative arrays as they are purely PL/SQL) or just use PL/SQL.
Assuming the rc_tab_type collection is not sparse then you can use pure PL/SQL like this:
DECLARE
TYPE rc_rec_multiset IS record (
rc_id NUMBER,
doc_num VARCHAR2(100)
);
TYPE rc_type IS TABLE OF rc_rec_multiset;
TYPE tab_rec_type_multiset IS
TABLE OF rc_rec_multiset INDEX BY pls_integer;
rc_tab_type rc_type := rc_type();
rc_tab_type_dist_rc tab_rec_type_multiset;
BEGIN
/*
* Populate rc_tab_type here.
*/
FOR i IN 1 .. rc_tab_type.COUNT LOOP
rc_tab_type_dist_rc( rc_tab_type(i).rc_id ) := rc_tab_type(i).doc_num;
END LOOP;
END;
If it is sparse then, instead of the FOR loop, you will have to use:
i := rc_tab_type.FIRST;
WHILE i IS NOT NULL LOOP
rc_tab_type_dist_rc( rc_tab_type(i).rc_id ) := rc_tab_type(i).doc_num;
i := rc_tab_type.NEXT(i);
END LOOP;

Get number of inserted rows by just plain sql

is there a way to get the number of inserted rows inside of the same transaction?
I see that PL/SQL command:
SQL%ROWCOUNT
does the job, however I don't want to create a procedure just for that!
I tried to simply call
insert into T ...
select SQL%ROWCOUNT;
but it gives me "invalid character".
If I remember well mysql actually had a way to obtain this information, does oracle really not provide any means for that?
I don't want to create a procedure just for that
No need to create any procedure, you could simply use an anonymous PL/SQL block.
For example,
SQL> SET serveroutput ON
SQL> DECLARE
2 var_cnt NUMBER;
3 BEGIN
4 var_cnt :=0;
5 FOR i IN(SELECT empno FROM emp)
6 LOOP
7 INSERT INTO emp(empno) VALUES(i.empno);
8 var_cnt := var_cnt + SQL%ROWCOUNT;
9 END loop;
10 DBMS_OUTPUT.PUT_LINE(TO_CHAR(var_cnt)||' rows inserted');
11 END;
12 /
14 rows inserted
PL/SQL procedure successfully completed.
SQL>
Update If you cannot use PL/SQL, and just plain SQL, then you cannot use SQL%ROWCOUNT.
The only option that comes to my mind is to have a timestamp column in your table, and query the count based on the timestamp to know the number of rows inserted.
Try following,
DBMS_OUTPUT.put_line(TO_CHAR(SQL%ROWCOUNT)||' rows inserted');

need to get the output of 2 cursors in one temp table

Here is my first procedure (sample)
CREATE OR REPLACE PROCEDURE GPTOWNER_CORP_AMF.testt1
AS
po_status VARCHAR2(100);
po_cur_1 SYS_REFCURSOR;
po_cur_2 SYS_REFCURSOR;
BEGIN
OPEN po_cur_1 FOR
select app_var_row_seq,app_var_name,app_var_value,app_var_description,r_date
from TMP_PMT_APP_VARIABLES_REF
where ROWNUM < 5;
OPEN po_cur_2 FOR
select config_to_lob_row_seq,config_row_seq,lobref_row_seq,r_date
from TMP_PMT_CONFIG_TO_LOB_DAT
where ROWNUM < 6;
TESTT2(po_cur_1,po_cur_2,po_status);
DBMS_output.put_line(po_status);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERRM||SQLCODE);
END;
Here is my second procedure (sample)
CREATE OR REPLACE procedure GPTOWNER_CORP_AMF.testt2 (pi_cur_1 IN sys_refcursor, pi_cur_2 IN sys_refcursor,po_status OUT VARCHAR2)
AS
app_var_row_seq NUMBER;
app_var_name VARCHAR2(100);
app_var_value VARCHAR2(1000);
app_var_description VARCHAR2(1000);
r_date1 DATE;
config_to_lob_row_seq NUMBER;
config_row_seq VARCHAR2(100);
lobref_row_seq NUMBER;
r_date2 DATE;
BEGIN
LOOP
FETCH pi_cur_1 into app_var_row_seq,app_var_name,app_var_value,app_var_description,r_date1;
FETCH pi_cur_2 into config_to_lob_row_seq,config_row_seq,lobref_row_seq,r_date2;
EXIT WHEN (pi_cur_2%NOTFOUND AND pi_cur_1%NOTFOUND ) ;
INSERT INTO testt1testt2 (colid,col1,col2,col3,col4,col5,col6,col7,col8,col9)
VALUES(colid.nextval,app_var_row_seq,app_var_name,app_var_value,app_var_description,r_date1,config_to_lob_row_seq,config_row_seq,lobref_row_seq,r_date2);
END LOOP;
DBMS_OUTPUT.PUT_LINE ('rows inserted:' || pi_cur_1%ROWCOUNT || 'and' || pi_cur_2%ROWCOUNT);
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(SQLERRM||SQLCODE);
END;
My problem statement is that from first procedure I am getting two refcursor as output and in the second procedure I am trying to read them and put them into a temp table which will be used by another procedure. Cant union the two select statements as they are having different set of output. Is there any better mechanism to do so , as by my approach I am facing issue as when I run the first procedure (say first select return 4 row and second select return 6 rows) the need is that 6 rows would be inserted into temp table but the columns that are read from first select will be inserted as NULL when there is now row fetched , but in my case duplicate row is getting inserted. Any help would be appreciated. And do post if anyone needs more info on the same.
If I understand you right, then you don't really need to union them - but join them.
Since there is no really relation between the 2 tables and you want nulls in "both side"s you need to full outer join them.
I will not ask you, why you want them both on the same temp table if there is no relation between them. But if you do this why not just use an insert-select ?
INSERT INTO testt1testt2 (colid,col1,col2,col3,col4,col5,col6,col7,col8,col9)
SELECT colid.nextval, app_var_row_seq,app_var_name,app_var_value,app_var_description, t1.r_date,
config_to_lob_row_seq,config_row_seq,lobref_row_seq, t2.r_date
FROM (select app_var_row_seq,app_var_name,app_var_value,app_var_description,r_date
from TMP_PMT_APP_VARIABLES_REF
where ROWNUM < 5) t1
FULL OUTER JOIN (select config_to_lob_row_seq,config_row_seq,lobref_row_seq,r_date
from TMP_PMT_CONFIG_TO_LOB_DAT
where ROWNUM < 6) t2 on 1=2
UPDATE:
If the requirement is to get 2 refcursors, then my approach isn't relevant...
What you can do though, is have 2 insert commands one like this:
INSERT INTO testt1testt2 (colid,col1,col2,col3,col4,col5,col6,col7,col8,col9)
VALUES (colid.nextval,app_var_row_seq,app_var_name,app_var_value,app_var_descript‌​ion,r_date1,null,null,null,null);
and the other like:
INSERT INTO testt1testt2 (colid,col1,col2,col3,col4,col5,col6,col7,col8,col9)
VALUES (colid.nextval,null,null,null,null,null,config_to_lob_row_seq,config_row_s‌​eq,lobref_row_seq,r_date2);
If you really want to do it nicely, you can use bulk insert for performance, see example here

How efficiently does Oracle handle a very long IN operator list

I have the following query (this is the simplified version of a much more complicated query):
SELECT * FROM TPM_TASK
WHERE (PROJECTID, VERSIONID) IN ((3,1), (24,1), (4,1))
In code I will be building that (PROJECTID,VERSIONID) key list programmatically, and this list could potentially be a couple thousand pairs long.
My question is how Oracle will optimize this query given that ProjectId and VersionId are indexed. Will the list be converted to a hash table, similar to a join against a temp table? Or will each key lookup be done one at a time?
I tried this query under my test database and got:
SELECT STATEMENT 68.0 68 2989732 19 8759 68 ALL_ROWS
TABLE ACCESS (FULL) 68.0 68 2989732 19 8759 1 TPMDBO TPM_TASK FULL TABLE ANALYZED 1
However, I believe this database doesn't have enough data to warrant an index scan. I tried the query on production and got:
SELECT STATEMENT 19.0 19 230367 23 9683 19 ALL_ROWS
INLIST ITERATOR 1
TABLE ACCESS (BY INDEX ROWID) 19.0 19 230367 23 9683 1 TPMDBO TPM_TASK BY INDEX ROWID TABLE ANALYZED 1
INDEX (RANGE SCAN) 4.0 4 64457 29 1 TPMDBO TPM_H1_TASK RANGE SCAN INDEX ANALYZED 1
This seems to hit the index, however I'm not sure what INLIST ITERATOR means. I'm guessing this means that Oracle is iterating through the list and doing a table access for each item in the list, which would probably not be too efficient with thousands of keys. However, perhaps Oracle is smart enough to optimize this better if I actually did give it several thousand keys.
NOTE: I don't want to load these keys into a temp table because frankly I don't like the way temp tables work under Oracle, and they usually end up in more frustration than they're worth (in my non-expert opinion anyway.)
The optimizer should base its decision on the number of items in the list and the number of rows in the table. If the table has millions of rows and the list has even a couple of thousand items, I would generally expect that it would use the index to do a couple thousand single-row lookups. If the table has a few thousand rows and the list has a couple thousand items, I'd expect that the optimizer to do a full scan of the table. In the middle, of course, is where all the interesting stuff happens and where it gets harder to work out exactly what plan the optimizer is going to choose.
In general, however, dynamically building this sort of query is going to be problematic from a performance perspective not because of how expensive a particular query execution is but because the queries you're generating are not sharable. Since you can't use bind variables (or, if you are using bind variables, you'll need a different number of bind variables). That forces Oracle to do a rather expensive hard parse of the query every time and puts pressure on your shared pool which will likely force out other queries that are sharable which will cause more hard parsing in the system. You'll generally be better served tossing the data you want to match on into a temporary table (or even a permanent table) so that your query can then be made sharable and parsed just once.
To Branko's comment, while Oracle is limited to 1000 literals in an IN list, that is only if you are using the "normal" syntax, i.e.
WHERE projectID IN (1,2,3,...,N)
If you use the tuple syntax that you posted earlier, however, you can have an unlimited number of elements.
So, for example, I'll get an error if I build up a query with 2000 items in the IN list
SQL> ed
Wrote file afiedt.buf
1 declare
2 l_sql_stmt varchar2(32000);
3 l_cnt integer;
4 begin
5 l_sql_stmt := 'select count(*) from emp where empno in (';
6 for i in 1..2000
7 loop
8 l_sql_stmt := l_sql_stmt || '(1),';
9 end loop;
10 l_sql_stmt := rtrim(l_sql_stmt,',') || ')';
11 -- p.l( l_sql_stmt );
12 execute immediate l_sql_stmt into l_cnt;
13* end;
SQL> /
declare
*
ERROR at line 1:
ORA-01795: maximum number of expressions in a list is 1000
ORA-06512: at line 12
But not if I use the tuple syntax
SQL> ed
Wrote file afiedt.buf
1 declare
2 l_sql_stmt varchar2(32000);
3 l_cnt integer;
4 begin
5 l_sql_stmt := 'select count(*) from emp where (empno,empno) in (';
6 for i in 1..2000
7 loop
8 l_sql_stmt := l_sql_stmt || '(1,1),';
9 end loop;
10 l_sql_stmt := rtrim(l_sql_stmt,',') || ')';
11 -- p.l( l_sql_stmt );
12 execute immediate l_sql_stmt into l_cnt;
13* end;
SQL> /
PL/SQL procedure successfully completed.
A better solution, which doesn't require temp tables, may be to put the data into a PL/SQL table, and then join to it. Tom Kyte has an excellent example here:
PL/SQL Table join example
Hope that helps.