I'm trying to print keys of an associative array for the simple logic shown below
declare
type std_marks_arr is table of number(10)
index by BINARY_INTEGER;--
std_marks std_marks_arr;
cursor std_id is select student_code from student_master;
begin
for i in std_id
loop
select sum(marks) into std_marks(i.student_code) from student_marks
where student_code=i.student_code;
end loop;
for i in std_marks.first..std_marks.last
loop
dbms_output.put_line(rpad(std_marks(i).key,10,' ')||rpad(std_marks(i),10,' '));
end loop;
end;`
Its giving an error like
'Invalid reference to variable 'NUMBER' in line where i used key
I Know that it can be done by other means but i have to know how can i print index in this situation.
Your variable std_marks has an associative array type as its type, which has no attribute key (and indeed, has no attributes - it's just an array of values).
In your code, i is the index, which will be a simple number, so you would just use this:
for i in std_marks.first..std_marks.last
loop
dbms_output.put_line(rpad(i,10,
' ')||rpad(std_marks(i),10,' '));
end loop
You have a few other issues in your code, however, that will stop it compiling (e.g. missing semicolon) and running (e.g. unless your student codes form a series with no gaps in numeric sequence, your code will raise NO_DATA_FOUND because your loop tries to access all array elements from the First index value to the Last index value, including gaps).
Related
I have the following code which works just fine in building a 2D array indexed by VARCHAR2. But I have a question about how I would instantiate the top-level array with an "empty" second level array. (See the comments in the code.)
The only way I've found to instantiate the initial array (inside the Add() procedure) is to declare a dummy variable (which is itself an empty array), and assign it to the top-level array:
DECLARE
TYPE pt_array IS TABLE OF number INDEX BY VARCHAR2(25);
TYPE prj_array IS TABLE OF pt_array INDEX BY VARCHAR2(25);
pt_array_ pt_array;
pj_array_ prj_array;
PROCEDURE Add (
act_ IN VARCHAR2,
pt_ IN VARCHAR2,
val_ IN NUMBER )
IS
dummy_empty_array pt_array; -- initiate a dummy variable
BEGIN
IF not pj_array_.EXISTS(act_) THEN
pj_array_(act_) := dummy_empty_array; -- can I initiate here without having to declare a variable?
END IF;
IF not pj_array_(act_).EXISTS(pt_) THEN
pj_array_(act_)(pt_) := val_;
END IF;
END Add;
BEGIN
Add ('A', '123', 1);
Add ('A', '456', 1);
Add ('B', '456', 1);
END;
Most languages offer the ability to assign an anonymous array when required (i.e. without having to define an intermittent variable). For example, in Javascript you'd just do x = [];. But I haven't found a way to do this in PL/SQL.
I've tried various different syntaxes, one obvious example being the following (which errors at compile time with "no function with name 'PT_ARRAY' exists"):
pj_array_(act_) := pt_array();
Also, the concept of using keyword EXTEND doesn't apply if you're declaring you array/tables with an INDEX BY clause.
So my question is whether it's possible to write this code (i.e. to initiate the empty array) without the dummy variable?
i'm trying to pass an array as a parameter in my procedure but i keep getting a command unknown error
code
SET SERVEROUTPUT ON;
TYPE pourcentage_remise IS TABLE OF NUMBER INDEX BY commandeproduit.ref_produit%type;
CREATE OR REPLACE PROCEDURE remise_produit( pourcent IN pourcentage_remise,
ref_comm IN commande.ref_commande%type,
c_ht OUT commandeproduit.prix_ht%type,
c_ttc OUT commandeproduit.prix_ttc%type)
IS
CURSOR p_curs IS
SELECT ref_produit, prix_ttc, prix_ht FROM commandeproduit WHERE concerne = ref_comm ;
ref commandeproduit.ref_produit%type;
ttc commandeproduit.prix_ttc%type;
ht commandeproduit.prix_ht%type;
BEGIN
open p_curs;
LOOP
FETCH p_curs into ref, ttc, ht;
EXIT WHEN p_curs%notfound;
dbms_output.put_line(ref, ' ',ht, ' ', ttc);
IF pourcent(ref) THEN
ttc := ttc - ttc * pourcent(ref);
ht := ht - ttc * pourcent(ref);
INSERT INTO commandeproduit(prix_ht, prix_ttc) VALUES(ht, ttc) WHERE concerne = ref_comm AND ref_produit = ref;
END IF;
dbms_output.put_line(ref, ' ',ht, ' ', ttc);
END LOOP;
close p_curs;
END remise_produit;
/
procedure call
DECLARE
pourcentage pourcentage_remise;
reference commande.ref_commande%type :=1;
BEGIN
pourcentage('A01') :=0.15;
pourcentage('B15') :=0.2;
remise_produit(pourcentage, reference);
END;
/
the table
the error in french which means command unknown
please help
Your syntax error is on the declaration of your type so the rest of your code isn't really needed.
TYPE pourcentage_remise IS TABLE OF NUMBER INDEX BY commandeproduit.ref_produit%type;
Several problems
If you are trying to declare a type in SQL, you'd need to use a CREATE TYPE so you're missing the CREATE.
If you are trying to declare a table type in SQL, you can't use an associative array. You'd realistically want a nested table instead.
If you are trying to declare a PL/SQL type, your statement would need to be in a PL/SQL block. You could declare a package that contains an associative array type.
If you want to declare a nested table type in SQL
CREATE TYPE pourcentage_remise IS TABLE OF NUMBER;
If you want to declare an associative array in a PL/SQL package
CREATE OR REPLACE PACKAGE my_collection_pkg
AS
TYPE pourcentage_remise IS TABLE OF NUMBER INDEX BY commandeproduit.ref_produit%type;
END;
If you want to use a nested table type, that changes how you need to initialize your associative array. It should change how you reference elements of that array, but I'm confused by your code. Your procedure appears to be using a numeric index to access an element of the associative array which doesn't make sense if the associative array uses a string as the index.
I have a cursor which fetches records from table based on filename (filenames are passed from array). now if filename is present more than once in table i need to add the filename to duparray such many number of times.
For example, if test2.txt is present 2 times and test3.txt is present 3 times, i need to have duparray as
{test2.txt,test2.txt,test3.txt,test3.txt,test3.txt}
But as per below code, duparray is coming as
{test2.txt,test3.txt,test3.txt} since i am having ROWCOUNT>1 check.
If that check is not there, filename which is present single time in table also gets added to it. Please advise where should i correct it.
CURSOR duplicateData IS
SELECT file_name from tablename where file_name=p_filearray(i)
dupRow duplicateData%rowtype;
Inside the procedure:
OPEN duplicateData ;
loop
fetch duplicateData INTO dupRow;
EXIT WHEN duplicateData %NOTFOUND;
IF duplicateData %ROWCOUNT >1
THEN
p_duparray.EXTEND;
p_duparray(p_duparray.LAST):=dupRow.file_name;
END IF;
end loop;
CLOSE duplicateData ;
Bob's First Law Of Database Programming states:
NEVER USE A LOOP TO DO A SELECT'S JOB
In this case you can use
DECLARE
FILENAME_COL_TYPE AS TABLE OF TABLENAME.FILENAME%TYPE INDEX BY PLS_INTEGER;
colFile_names ROW_COL_TYPE;
BEGIN
SELECT FILE_NAME
BULK COLLECT INTO colFile_names
FROM TABLENAME
ORDER BY FILE_NAME;
END;
This doesn't address the issue of the desired filenames already being in a collection, but presumably that collection of filenames was derived from a SELECT statement, so the criteria for choosing the appropriate filenames can be included in the above.
Loops are bad. NO LOOPS! Never! (What, never?) No, never! (Never?) Well, hardly ever...
:-)
I would suggest to collect into cursor file_name and number of occurances (cnt)
CURSOR duplicateData IS
select file_name,count(*) cnt from tablename where file_name=p_filearray(i)
group by file_name;
dupRow duplicateData%rowtype;
i number:=0;
An then use for loop to fill the array ...
OPEN duplicateData ;
loop
fetch duplicateData INTO dupRow;
EXIT WHEN duplicateData %NOTFOUND;
for i in 1..dupRow.cnt loop
p_duparray.EXTEND;
p_duparray(p_duparray.LAST):=dupRow.file_name;
end loop;
CLOSE duplicateData ;
Perhaps I'm missing something, but it looks as though you do nothing for the first loop iteration due to the IF duplicateData%ROWCOUNT > 1, and this is why you're losing the first value.
I'd automatically refactor the open-fetch-exit-end-close loop into the simple form:
for r in (
select file_name from tablename where file_name=p_filearray(i)
)
loop
p_duparray.extend;
p_duparray(p_duparray.last) := r.file_name;
end loop;
Although as Bob Jarvis mentioned you don't even need a loop just to fetch cursor results into an array, when you can just bulk collect it.
Depending on what you need the array for, it might be possible to make it an associative array of counts indexed by the filename. That is, store each filename only once and maintain the count for each one, so that filecounts('test3.txt') = 3, rather than storing test3.txt three times.
I am having a table Course which have column DepId and Course and some other value. I need to search this table for a some set of (DepId, Course). This set will be decided at runtime.
I want to write a single query in a procedure to fetch all the record pertaining to above set.
For example consider the table has data like
DepId Course ...
------------------
1 A
2 B
3 C
4 D
5 E
6 F
Now only I want to search for below records:
DepId Course ...
------------------
1 A
4 D
What would be most efficient way to write above query?
I was thinking of creating an Array of record and passing it in the 'IN' operator. But I was not able to get any example for this. Can someone guide me on this?
Thanks
Leveraging Oracle Collections to Build Array-typed Solutions
The answer to your question is YES, dimensioned variables such as ARRAYS and COLLECTIONS are viable data types in solving problems where there are multiple values in either or both the input and output values.
Additional good news is that the discussion for a simple example (such as the one in the OP) is pretty much the same as for a complex one. Solutions built with arrays are nicely scalable and dynamic if designed with a little advanced planning.
Some Up Front Design Decisions
There are actual collection types called ARRAYS and ASSOCIATIVE ARRAYS. I chose to use NESTED TABLE TYPES because of their accessibility to direct SQL queries. In some ways, they exhibit "array-like" behavior. There are other trade-offs which can be researched through Oracle references.
The query applied to search the COURSE TABLE would apply a JOIN condition instead of an IN-LIST approach.
The use of a STORED PROCEDURE typed object improves database response. Queries within the procedure call can leverage and reuse already compiled code plus their cached execution plans.
Choosing the Right Collection or Array Type
There are a lot of choices of collection types in Oracle for storing variables into memory. Each has an advantage and some sort of limitation. AskTom from Oracle has a good example and break-down of what a developer can expect by choosing one variable collection type over another.
Using NESTED TABLE Types for Managing Multiple Valued Variables
For this solution, I chose to work with NESTED TABLES because of their ability to be accessed directly through SQL commands. After trying several different approaches, I noticed that the plain-SQL accessibility leads to more clarity in the resulting code.
The down-side is that you will notice that there is a little overhead here and there with respect to declaring an instance of a nested table type, initializing each instance, and managing its size with the addition of new values.
In any case, if you anticipate a unknown number of input variables or values (our output), an array-typed data type (collection) of any sort is a more flexible structure for your code. It is likely to require less maintenance in the end.
The Example: A Stored Procedure Search Query
Custom TYPE Definitions
CREATE OR REPLACE TYPE "COURSE_REC_TYPE" IS OBJECT (DEPID NUMBER(10,0), COURSE VARCHAR2(10));
CREATE OR REPLACE TYPE "COURSE_TBL_TYPE" IS TABLE of course_rec_type;
PROCEDURE Source Code
create or replace PROCEDURE ZZ_PROC_COURSE_SEARCH IS
my_input course_tbl_type:= course_tbl_type();
my_output course_tbl_type:= course_tbl_type();
cur_loop_counter pls_integer;
c_output_template constant varchar2(100):=
'DEPID: <<DEPID>>, COURSE: <<COURSE>>';
v_output VARCHAR2(200);
CURSOR find_course_cur IS
SELECT crs.depid, crs.course
FROM zz_course crs,
(SELECT depid, course
FROM TABLE (CAST (my_input AS course_tbl_type))
) search_values
WHERE crs.depid = search_values.depid
AND crs.course = search_values.course;
BEGIN
my_input.extend(2);
my_input(1):= course_rec_type(1, 'A');
my_input(2):= course_rec_type(4, 'D');
cur_loop_counter:= 0;
for i in find_course_cur
loop
cur_loop_counter:= cur_loop_counter + 1;
my_output.extend;
my_output(cur_loop_counter):= course_rec_type(i.depid, i.course);
end loop;
for j in my_output.first .. my_output.last
loop
v_output:= replace(c_output_template, '<<DEPID>>', to_char(my_output(j).depid));
v_output:= replace(v_output, '<<COURSE>>', my_output(j).course);
dbms_output.put_line(v_output);
end loop;
end ZZ_PROC_COURSE_SEARCH;
Procedure OUTPUT:
DEPID: 1, COURSE: A
DEPID: 4, COURSE: D
Statement processed.
0.03 seconds
MY COMMENTS: I wasn't particularly satisfied with the way the input variables were stored. There was a clumsy kind of problem with "loading" values into the nested table structure... If you can consider using a single search key instead of a composite pair (i.e., depid and course), the problem condenses to a simpler form.
Revised Cursor Using a Single Search Value
This is the proposed modification to the table design of the OP. Add a single unique key id column (RecId) to represent each unique combination of DepId and Course.
Note that the RecId column represents a SURROGATE KEY which should have no internal meaning aside from its property as a uniquely assigned value.
Custom TYPE Definitions
CREATE OR REPLACE TYPE "NUM_TBL_TYPE" IS TABLE of INTEGER;
Remove Array Variable
This will be passed directly through an input parameter from the procedure call.
-- REMOVE
my_input course_tbl_type:= course_tbl_type();
Loading and Presenting INPUT Parameter Array (Nested Table)
The following can be removed from the main procedure and presented as part of the call to the procedure.
BEGIN
my_input.extend(2);
my_input(1):= course_rec_type(1, 'A');
my_input(2):= course_rec_type(4, 'D');
Becomes:
create or replace PROCEDURE ZZ_PROC_COURSE_SEARCH (p_search_ids IN num_tbl_type) IS...
and
my_external_input.extend(2);
my_external_input:= num_tbl_type(1, 4);
Changing the Internal Cursor Definition
The cursor looks about the same. You can just as easily use an IN-LIST now that there is only one search parameter.
CURSOR find_course_cur IS
SELECT crs.depid, crs.course
FROM zz_course_new crs,
(SELECT column_value as recid
FROM TABLE (CAST (p_search_ids AS num_tbl_type))
) search_values
WHERE crs.recid = search_values.recid;
The Actual SEARCH Call and Output
The searching portion of this operation is now isolated and dynamic. It does not need to be changed. All the Changes happen in the calling PL/SQL block where the search ID values are a lot easier to read and change.
DECLARE
my_input_external num_tbl_type:= num_tbl_type();
BEGIN
my_input_external.extend(3);
my_input_external:= num_tbl_type(1,3,22);
ZZ_PROC_COURSE_SEARCH (p_search_ids => my_input_external);
END;
-- The OUTPUT (Currently set to DBMS_OUT)
DEPID: 1, COURSE: A
DEPID: 4, COURSE: D
DEPID: 7, COURSE: G
Statement processed.
0.01 seconds
This is something I havee used in the past in a situation similar to yours. Hopefully it helps.
The main benefit of this method would be that if you only passed it a single paramter it would still return all records for that single parameter. This way a single stored procedure with 5 input parameters could be used to search for all combinations of inputs.
Just call the stored procedure passing in the set and should return all values mathcing the criteria
usp_custom_search '1','A'
usp_custom_search '4','D'
usp_custom_search '4',NULL
usp_custom_search NULL,'A'
etc
Stored Procedure:
CREATE OR REPLACE PROCEDURE custom_search (
dep_id IN VARCHAR2,
course_id IN VARCHAR2,
result_set OUT SYS_REFCURSOR)
BEGIN
query_str VARCHAR2(1000);
query_str := 'SELECT';
query_str := query_str || ' DepId, Course';
query_str := query_str || ' FROM Course';
query_str := query_str || ' WHERE 1=1';
IF (dep_id is not null) then query_str := query_str || ' AND DepId = ''' || dep_id || ''''; END IF;
IF (course_id is not null) then query_str := query_str || ' AND Course = ''' || course_id || ''''; END IF;
open result_set for query_str;
END custom_search;
/
UPDATE View the edits if you care to see the long original question. This is the clearer short version of the question...
I need to see if GroupA (not always GroupA, this changes each loop iteration) exists in a [list,varray,temp table, whatever] of 200 or so groups. How I store those 200 groups is totally in my control. But I want to store them in a construct that lends itself to the FASTEST "existence" checking because I will have to check this list MANY times within a loop against different values (not always GroupA). So whats fastest in PL/SQL, checking a list...
IF 'GroupA' IN ('GroupA','GroupB') THEN...
or checking a VARRAY using MEMBER OF...
IF 'GroupA' MEMBER OF myGroups THEN
or checking a VARRAY this way...
FOR i IN myGroups.FIRST .. myGroups.LAST
LOOP
IF myGroups(i) = 'GroupA' THEN
v_found := TRUE;
EXIT;
END IF;
END LOOP;
or checking associative arrays...
will test this tomorrow
UPDATE: FINAL RESULTS OF TESTING FROM EVERYONE'S SUGGESTIONS
Thanks all.
I ran these tests, looped 10 million times and the commas separated string using a LIKE seemed to be the fastest so I guess the points have to go to #Brian McGinity (the times are in the comments below). But since the times were all so close it probably doesn't matter which method I go with. I think I'll go with the VARRAY MEMBER OF method since I can load the array with a single line of code (bulk collect) instead of having to loop a cursor to build a string (thanks #Wernfried for bringing MEMBER OF to my attention)...
comma separated list, example: ,GroupA,GroupB,GroupC,...around 200 groups... (list made by looping a cursor)
FOR i IN 1 .. 10000000 loop
if myGroups like '%,NONE,%' then
z:=z+1;
end if;
end loop;
--690msec
same commas separated list (list made by looping a cursor)...
FOR i IN 1 .. 10000000 loop
if instr(myGroups, ',NONE,') > 0 then
z:=z+1;
end if;
end loop;
--818msec
varray, same 200 groups (varray made by bulk collect)...
FOR i IN 1 .. 10000000 loop
IF 'NONE' MEMBER of myGroups THEN
z:=z+1;
end if;
end loop;
--780msec
associative array method suggested by #Yaroslav Shabalin (assoc. array made by looping a cursor)...
FOR i IN 1 .. 10000000 loop
if (a_values('NONE') = 1) then
z:=z+1;
end if;
end loop;
--851msec
Is myGroup a varray? If it is a string try something like:
select 1
from dual
where 'abc,NONE,def' like '%,NONE,%'
It is hard to follow the constraints you're working under... If at all possible, do everything inside of sql and it will be faster.
Update:
So if you're already in a plsql unit and wanted to stay in a plsql unit then the logic above would go something like this:
declare
gp varchar2(200) := 'abc,def,NONE,higlmn,op';
begin
if ','||gp||',' like '%,NONE,%' then
dbms_output.put_line('y');
else
dbms_output.put_line('n');
end if;
end;
if this itself is in a loop then, make the list once as:
declare
gp varchar2(200) := 'abc,def,NONE,higlmn,op';
gp2 varchar2(200) := ',' || gp || ',';
begin
if g2 like '%,NONE,%' then
dbms_output.put_line('y');
else
dbms_output.put_line('n');
end if;
end;
Also try instr which is probably faster than like:
declare
gp varchar2(200) := ',abc,def,NONE,hig,';
begin
if instr(gp, ',NONE,') > 0 then
dbms_output.put_line('y');
else
dbms_output.put_line('n');
end if;
end;
I have no idea if this faster than the other solutions mentioned (it stands a good chance), it is something else to try.
I did not get your full question but perhaps this function helps you:
MEMBER Condition
WHERE 'groupA' MEMBER of myGroups
Have you considered using associative arrays also formerly known as "index-by tables"? Associative arrays indexed by string are optimized for efficient lookup by implicitly using the B*-tree organization of the values. It is PL/SQL equivalent to hash tables in other programming languages.
For example if you define an array as:
type t_values is table of number index by varchar2(20);
Then assign GroupA etc. to keys and 1 to each respective value:
a_values t_values;
for c_cursor in (select ...)
loop
a_value(c_cursor.group_name) := 1;
end loop;
When you try to access the value for non-existent index, you will get null. Whereas for any real index you have 1 returned;
(a_value('GroupA') = 1) => TRUE
(a_value('Some_not_existent_index') IS NULL) => TRUE