Syntax for Arrays, Loops in Oracle PL/SQL - arrays

I am trying to solve a problem in PL/SQL, but I am new to the language. I decided to solve it as I would solve it in another language, and so I learned something about how SQL declares and utilizes arrays in this thread:
Oracle PL/SQL - How to create a simple array variable?
I followed that example, but my code doesn't run. The error message is unhelpful. In particular it references line 21 whereas my code block begins on line 54. Here it is, in case it makes sense by error code, somehow:
ORA-06550: line 21, column 7:
PLS-00103: Encountered the symbol "IN" when expecting one of the following:
:= . ( # % ; 06550. 00000 - "line %s, column %s:\n%s"
*Cause: Usually a PL/SQL compilation error.
And here is my code block. I am sure I made a bunch of mistakes since I am learning by code example here. If you could please point out any syntax or other errors I would very much appreciate it. Thank you
declare
vISBNa books.ISBN%type := '1059831198';
vISBNb books.ISBN%type := '0401140733';
vISBNc books.ISBN%type := '4981341710';
vISBNd books.ISBN%type := '8843172113';
vCATEGORYtemp books.CATEGORY%type;
vRETAILtemp books.RETAIL%type;
type arry is varray(4) of books.ISBN%type;
array arry := arry(vISBNa, vISBNb, vISBNc, vISBNd);
begin
for i in 1..array.count loop
select category
into vCATEGORYtemp
from books
where ISBN = i;
select retail
into vRETAILtemp
from books
where ISBN = i;
IF vCATEGORYtemp = 'COMPUTER' THEN
vRETAILtemp := vRETAILtemp * 0.7;
ELSIF vCATEGORYtemp = 'FITNESS' THEN
vRETAILtemp := vRETAILtemp * 0.6;
ELSIF vCATEGORYtemp = 'BUSINESS' THEN
vRETAILtemp := vRETAILtemp * 0.8;
ELSE
vRETAILtemp := vRETAILtemp * 0.9;
END IF;
dbms_output.put_line(vRETAILtemp);
end loop;
end;

The problem certainly has a trivial SQL solution, but since this is PL/SQL practice (specifically), here are a few suggestions for improvement. Note that the code is almost correct; in particular, it is not clear what, if anything, throws the exact error you mentioned.
In the loop, you can assign to both local variables in a single select statement, as shown below. Notice also the where clause, where you must compare ISBN from the table to array(i), not to i. (That is the only error I found!)
for i in 1 .. array.count loop
select category, retail
into vCATEGORYtemp, vRETAILtemp
from books
where ISBN = array(i);
Finally, the assignment to vRETAILtemp can be simplified, using a case expression (not a case statement, which is a construct very similar to the if... then... elsif... else... end construct):
vRETAILtemp := vRETAILtemp * case vCATEGORYtemp when 'COMPUTER' then 0.7
when 'FITNESS' then 0.6
when 'BUSINESS' then 0.8
else 0.9 end;

There are plenty of errors in there :
You have a type called arry but you have also a variable with the same name. PL/SQL doesn't like that.
In the for loop : You are trying to loop over the type and not the array
for i in 1..array.count loop
it should be :
for i in 1..arry.count loop
Generally speaking it's not common to use arrays in Oracle. Although it's possible but it's much more efficient to use SQL which is very simple and straightforward. Your code can be simplified to :
declare
Cursor Cur is select case
when vCATEGORYtemp = 'COMPUTER' then 0.7
when vCATEGORYtemp = 'FITNESS' then 0.6
when vCATEGORYtemp = 'BUSINESS' then 0.8
else vCATEGORYtemp = 'COMPUTER' then 0.9
end * vRETAILtemp as Output
from books
where ISBN in ('1059831198','0401140733','4981341710','8843172113');
R Cur%Rowtype;
begin
Open Cur;
loop
fetch Cur into R;
exit when Cur%notfound;
dbms_output.put_line(R.Output);
end loop;
Close Cur;
end ;
I think you have to review the basics of coding which are :
Making the efforts of naming the objects properly. When you have two things called array and arry you save 0.5 seconds writing your code but you have great chances to spend hours trying to investigate what the problem is in case of a bug.
In coding, we almost never write a big code and then check if it compiles. Is should be incremental especially as beginner. Write two lines and check it compiles, etc. Otherwise, it's complicated to know where the problem is coming from.
In Oracle, you can use SQL Developer, and put your code inside a Function/Procedure or Package. At least in case of a compilation problem, it will tell you the right line which has a problem.
Hope this helps.

You can do this more simply (but still using PL/SQL for this learning exercise):
begin
for r in (
select * from books
)
loop
r.retail := r.retail *
case r.category
when 'COMPUTER' then 0.7
when 'FITNESS' then 0.6
when 'BUSINESS' then 0.8
else 0.9
end;
dbms_output.put_line(r.isbn||' '||rpad(r.category,10)||' '||r.retail);
end loop;
end;
Or using an array:
declare
type book_tt is table of books%rowtype;
l_books book_tt;
begin
select *
bulk collect into l_books
from books b;
for i in 1..l_books.last loop
l_books(i).retail := l_books(i).retail *
case l_books(i).category
when 'COMPUTER' then 0.7
when 'FITNESS' then 0.6
when 'BUSINESS' then 0.8
else 0.9
end;
dbms_output.put_line(l_books(i).isbn||' '||rpad(l_books(i).category,10)||' '||l_books(i).retail);
end loop;
end;
(I've cheated a little bit by using select * and books%rowtype, because this is just an example and I am lazy. For real code it's usually better to list the actual columns you want explicitly.)
Test data:
create table books
( isbn varchar2(10)
, category varchar2(20)
, retail number(6,2) );
insert all
into books values ( '1059831198', 'COMPUTER', 10)
into books values ( '0401140733', 'CATS', 10)
into books values ( '4981341710', 'FITNESS', 10)
into books values ( '8843172113', 'BUSINESS', 10)
select * from dual;
Output:
1059831198 COMPUTER 7
0401140733 CATS 9
4981341710 FITNESS 6
8843172113 BUSINESS 8

Related

What data structure to use in order to sort this data in PL/SQL?

This is Oracle 11.2g. In a PL/SQL function, I've got a loop whereby each iteration, I create a string and an integer associated with that string. The function returns the final concatenation of all the generated strings, sorted (depending on a function input parameter), either alphabetically or by the value of the integer. To give an idea, I'm generating something like this:
Iteration String Integer
1 Oslo 40
2 Berlin 74
3 Rome 25
4 Paris 10
If the input parameter says to sort alphabetically, the function output should look like this :
Berlin, Oslo, Paris, Rome
Otherwise, we return the concatenated strings sorted by the value of the associated integer:
Paris, Rome, Oslo, Berlin
What is the most appropriate data structure to achieve this sort? I've looked at collections, associative arrays and even varrays. I've been kind of shocked how difficult this seems to be to achieve in Oracle. I saw this question but it doesn't work in my case, as I need to be able to sort by both index and value: How to sort an associative array in PL/SQL? Is there a more appropriate data structure for this scenario, and how would you sort it?
Thanks!
It is very easy if you use PL/SQL as SQL and not like other languages. It is quite specific and sometimes is very nice exactly because of that.
Sometimes I really hate PL/SQL, but this case is absolutely about love.
See how easy it is:
create type it as object (
iter number,
stringval varchar2(100),
intval integer
);
create type t_it as table of it;
declare
t t_it := new t_it();
tmp1 varchar2(32767);
tmp2 varchar2(32767);
begin
t.extend(4);
t(1) := new it(1,'Oslo',40);
t(2) := new it(2,'Berlin',74);
t(3) := new it(3,'Rome',25);
t(4) := new it(4,'Paris',10);
select listagg(stringval,', ') within group (order by stringval),
listagg(stringval,', ') within group (order by intval)
into tmp1, tmp2
from table(t);
dbms_output.put_line(tmp1);
dbms_output.put_line(tmp2);
end;
/
drop type t_it;
drop type it;
Here you can see the problem that you must create global types, and this is what I hate it for. But they say in Oracle 12 it can be done with locally defined types so I am waiting for it :)
The output is:
Berlin, Oslo, Paris, Rome
Paris, Rome, Oslo, Berlin
EDIT
As far as you do not know the amount of iterations from the beginning the only way is to do extend on each iteration (this is only example of extending):
declare
iterator pls_integer := 1;
begin
/* some type of loop*/ loop
t.extend();
-- one way to assign
t(t.last) := new it(1,'Oslo',40);
-- another way is to use some integer iterator
t(iterator) := new it(1,'Oslo',40);
iterator := iterator + 1;
end loop;
end;
I prefer the second way because it is faster (does not calculate .last on each iteration).
This is an example of pure PL/SQL implementation that is based on the idea associative array (aka map or dictionary in other domains) is an ordered collection that is sorted by a key. That is a powerful feature that I have used multiple times. For input data structure in this example I decided to use a nested table of records (aka a list of records).
In this particular case however I'd probably go for similar implementation than in simon's answer.
create or replace package so36 is
-- input data structures
type rec_t is record (
iter number,
str varchar2(20),
int number
);
type rec_list_t is table of rec_t;
function to_str(p_list in rec_list_t, p_sort in varchar2 default 'S')
return varchar2;
end;
/
show errors
create or replace package body so36 is
function to_str(p_list in rec_list_t, p_sort in varchar2 default 'S')
return varchar2 is
v_sep constant varchar2(2) := ', ';
v_ret varchar2(32767);
begin
if p_sort = 'S' then
-- create associative array (map) v_map where key is rec_t.str
-- this means the records are sorted by rec_t.str
declare
type map_t is table of rec_t index by varchar2(20);
v_map map_t;
v_key varchar2(20);
begin
-- populate the map
for i in p_list.first .. p_list.last loop
v_map(p_list(i).str) := p_list(i);
end loop;
v_key := v_map.first;
-- generate output string
while v_key is not null loop
v_ret := v_ret || v_map(v_key).str || v_sep;
v_key := v_map.next(v_key);
end loop;
end;
elsif p_sort = 'I' then
-- this branch is identical except the associative array's key is
-- rec_t.int and thus the records are sorted by rec_t.int
declare
type map_t is table of rec_t index by pls_integer;
v_map map_t;
v_key pls_integer;
begin
for i in p_list.first .. p_list.last loop
v_map(p_list(i).int) := p_list(i);
end loop;
v_key := v_map.first;
while v_key is not null loop
v_ret := v_ret || v_map(v_key).str || v_sep;
v_key := v_map.next(v_key);
end loop;
end;
end if;
return rtrim(v_ret, v_sep);
end;
end;
/
show errors
declare
v_list so36.rec_list_t := so36.rec_list_t();
v_item so36.rec_t;
begin
v_item.iter := 1;
v_item.str := 'Oslo';
v_item.int := 40;
v_list.extend(1);
v_list(v_list.last) := v_item;
v_item.iter := 2;
v_item.str := 'Berlin';
v_item.int := 74;
v_list.extend(1);
v_list(v_list.last) := v_item;
v_item.iter := 3;
v_item.str := 'Rome';
v_item.int := 25;
v_list.extend(1);
v_list(v_list.last) := v_item;
v_item.iter := 4;
v_item.str := 'Paris';
v_item.int := 10;
v_list.extend(1);
v_list(v_list.last) := v_item;
dbms_output.put_line(so36.to_str(v_list));
dbms_output.put_line(so36.to_str(v_list, 'I'));
end;
/
show errors

Query from array

I am getting error while trying to select values from an array, like following code
declare result CLOB;
myarray selected_pkg.num_array := selected_pkg.num_array();
begin
myarray.extend(3);
myarray(1) := 1; myarray(2) := 5; myarray(3) := 9;
EXECUTE IMMEDIATE 'select column_value from table (cast(myarray AS selected_pkg.num_array))';
COMMIT;
end;
ORA-00904: "MYARRAY": invalid identifier
Please suggest.
Thanks, Alan
First off, there doesn't appear to be any reason to use dynamic SQL here.
Second, if you want to run a SELECT statement, you need to do something with the results. You'd either need a cursor FOR loop or you'd need to BULK COLLECT the results into a different collection or otherwise do something with the results.
Third, if you want to use a collection in SQL, that collection must be defined in SQL not in PL/SQL.
Something like this will work (I'm not sure if that's what you want to do with the results)
SQL> create type num_arr is table of number;
2 /
Type created.
SQL> declare
2 l_nums num_arr := num_arr( 1, 2, 3, 7 );
3 begin
4 for i in (select column_value from table( l_nums ))
5 loop
6 dbms_output.put_line( i.column_value );
7 end loop;
8 end;
9 /
1
2
3
7
PL/SQL procedure successfully completed.
execute immediate is not need at this point.
Use fetch or loop cursors in proc.

PostgreSQL PL/pgSQL random value from array of values

How can I declare an array like variable with two or three values and get them randomly during execution?
a := [1, 2, 5] -- sample sake
select random(a) -- returns random value
Any suggestion where to start?
Try this one:
select (array['Yes', 'No', 'Maybe'])[floor(random() * 3 + 1)];
Updated 2023-01-10 to fix the broken array literal. Made it several times faster while being at it:
CREATE OR REPLACE FUNCTION random_pick()
RETURNS int
LANGUAGE sql VOLATILE PARALLEL SAFE AS
$func$
SELECT ('[0:2]={1,2,5}'::int[])[trunc(random() * 3)::int];
$func$;
random() returns a value x where 0.0 <= x < 1.0. Multiply by 3 and truncate it with trunc() (slightly faster than floor()) to get 0, 1, or 2 with exactly equal chance.
Postgres indexes are 1-based by default (as per SQL standard). This would be off-by-1. We could increment by 1 every time, but for efficiency I declare the array index to start with 0 instead. Slightly faster, yet. See:
Normalize array subscripts so they start with 1
The manual on mathematical functions.
PARALLEL SAFE for Postgres 9.6 or later. See:
PARALLEL label for a function with SELECT and INSERT
When to mark functions as PARALLEL RESTRICTED vs PARALLEL SAFE?
You can use the plain SELECT statement if you don't want to create a function:
SELECT ('[0:2]={1,2,5}'::int[])[trunc(random() * 3)::int];
Erwin Brandstetter answered the OP's question well enough. However, for others looking for understanding how to randomly pick elements from more complex arrays (like me some two months ago), I expanded his function:
CREATE OR REPLACE FUNCTION random_pick( a anyarray, OUT x anyelement )
RETURNS anyelement AS
$func$
BEGIN
IF a = '{}' THEN
x := NULL::TEXT;
ELSE
WHILE x IS NULL LOOP
x := a[floor(array_lower(a, 1) + (random()*( array_upper(a, 1) - array_lower(a, 1)+1) ) )::int];
END LOOP;
END IF;
END
$func$ LANGUAGE plpgsql VOLATILE RETURNS NULL ON NULL INPUT;
Few assumptions:
this is not only for integer arrays, but for arrays of any type
we ignore NULL data; NULL is returned only if the array is empty or if NULL is inserted (values of other non-array types produce an error)
the array don't need to be formatted as usual - the array index may start and end anywhere, may have gaps etc.
this is for one-dimensional arrays
Other notes:
without the first IF statement, empty array would lead to an endless loop
without the loop, gaps and NULLs would make the function return NULL
omit both array_lower calls if you know that your arrays start at zero
with gaps in the index, you will need array_upper instead of array_length; without gaps, it's the same (not sure which is faster, but they shouldn't be much different)
the +1 after second array_lower serves to get the last value in the array with the same probability as any other; otherwise it would need the random()'s output to be exactly 1, which never happens
this is considerably slower than Erwin's solution, and likely to be an overkill for the your needs; in practice, most people would mix an ideal cocktail from the two
Here is another way to do the same thing
WITH arr AS (
SELECT '{1, 2, 5}'::INT[] a
)
SELECT a[1 + floor((random() * array_length(a, 1)))::int] FROM arr;
You can change the array to any type you would like.
CREATE OR REPLACE FUNCTION pick_random( members anyarray )
RETURNS anyelement AS
$$
BEGIN
RETURN members[trunc(random() * array_length(members, 1) + 1)];
END
$$ LANGUAGE plpgsql VOLATILE;
or
CREATE OR REPLACE FUNCTION pick_random( members anyarray )
RETURNS anyelement AS
$$
SELECT (array_agg(m1 order by random()))[1]
FROM unnest(members) m1;
$$ LANGUAGE SQL VOLATILE;
For bigger datasets, see:
http://blog.rhodiumtoad.org.uk/2009/03/08/selecting-random-rows-from-a-table/
http://www.depesz.com/2007/09/16/my-thoughts-on-getting-random-row/
https://blog.2ndquadrant.com/tablesample-and-other-methods-for-getting-random-tuples/
https://www.postgresql.org/docs/current/static/functions-math.html
CREATE FUNCTION random_pick(p_items anyarray)
RETURNS anyelement AS
$$
SELECT unnest(p_items) ORDER BY RANDOM() LIMIT 1;
$$ LANGUAGE SQL;

How do I write arrays in pascal?

Is this program correctly written with the array?
Program Malaria_Outbreak (input,output);
Var
BC:real;
LO:real;
count:integer;
Num:integer;
BloodTest:integer;
Registration:integer;
Clinic:string;
DoctorFee:integer;
Total:integer;
NMB_Payable:real;
Company:string;
Name:string;
Patient:Array[1..10] of string
Begin
clrscr;
BC:=(0.8);
LO:=(0.7);
Count:=(0);
Num:=(0);
BloodTest:=(Num * 700);
Registration:=(500);
Writeln('Please enter the name of the patient');
Readln(Name);
While (Name <> 'END')Do
Begin
For count:= 1 to 10 Do
Begin
Writeln('Please enter the clinic the patient attends');
Readln(Clinic);
If (Clinic = 'Type 1') Then
Begin
DoctorFee:=(800);
End;
If (Clinic = 'Type 2') Then
Begin
DoctorFee:=(1200);
End;
Writeln('The doctor fee for the patient is $',DoctorFee);
Writeln('Please enter the number of blood tests the patient has had');
Readln(Num);
BloodTest:=(Num * BloodTest);
Writeln('The blood test for the patient is $',BloodTest);
TMB:=(Registration + DoctorFee + BloodTest);
Writeln('The total medical bill for the patient is $',TMB);
Writeln('Please enter the insurance company the clinic is affiliated with');
Readln(Company);
If (Company = 'Blue Cross') Then
Begin
NMB_Payable:=(BC * TMB);
End;
If (Company = 'LOJ') Then
Begin
NMB_Payable:=(LO * TMB);
End;
Writeln('The net medical bill for the patient is $',NMB_Payable);
End;
Readln;
Readln;
End
Looks good, but you might want to include the ; after the datatype (string)
Patient : Array[1..10] of String;
There are some problems in the code.
Your code was not formatted. Especially the lack of indenting makes it hard to understand what's going on. (thanks to GolezTrol for fixing that)
You're missing a semi-colon (;) after Array[1..10] of string
Some end; statement is missing. Either While (Name <> 'END')Do begin or For count:= 1 to 10 Do begin should have a matching end; statement.
Variable Tmb is not declared.
Bloodtest will always be 0. It's initialized to 0, and the only time you write to Bloodtest is on this line: BloodTest := (Num * BloodTest);. That's probably not what you want to do.
DoctorFee is uninitialized unless the user types Type 1 or Type 2. NMB_Payable has a similar problem.
There's a variable Count that's initialized, but never used afterwards. Doesn't do any damage, but for readability I'd clean it up.
To answer your question: No, you're not using the array that's declared, and I don't think this program does what you want it to do.
If you explain what you're trying to accomplish, we can help you out with that.
I don't see where it's writing to the array at all, nor where it would make any use of the array in the first place. It's simply processing each item it gets, nothing is carried to be stored in an array in the first place.
It's also going to ask and bill each patient 10 times. I've heard of double-billing but this is crazy!
You should always run your code and see what actually happens. It's quite obvious you didn't.

I need to know how i can write IF statements and CASE break statements that use and execute queries, etc in MySQL?

I need to execute a query that is highly dependent on several conditions what need to be checked by analyzing that data in other tables, the base goal is to return the rows from the tables if all of the conditions in the other tables are true, I am fed up with INNER joins LEFT joins and WHERE statement, i need to look up one table, if the returned value is 1, 0 or 4 or a set of values, i need to execute an other statement, and based on the resuts of that i need to execute one last statement which is my final result.
as far as functions are procedures are concerned, i studies the MySQL documentation like hell and all it gives me is this ::
DELIMITER $$
CREATE DEFINER=`root`#`localhost` FUNCTION `SimpleCompare`(n INT, m INT) RETURNS varchar(20) CHARSET latin1
BEGIN
DECLARE s VARCHAR(20);
IF n > m THEN SET s = '>';
ELSEIF n = m THEN SET s = '=';
ELSE SET s = '<';
END IF;
SET s = CONCAT(n, ' ', s, ' ', m);
RETURN s;
END
Now this is so plain, i dont even know where to start, I the "returns varchar(20)" what does it need to be if im expecting it to return a table of 10 rows and not a VARCHAR(20), what do I declare "DECLARE s VARCHAR(20);" as if i want it to be a table not a VARCHAR(20).
the (n > m) after the "IF" how to i replace it with my own query ,
and after I do that, the "THEN SET s = '>'" how do i set s = to the query results ?, this is driving me crazy the syntax is beyond me, and the documentation does not explain anything.
Thanks in advance
To my knowledge, MySQL doesn't support a table valued data type. The use of the function you posted would be:
SELECT simplecompare(yt.n, yt.m) AS eval
FROM YOUR_TABE yt
...which would return:
eval
--------
1 = 1
2 < 3
etc.
SQL is set based, which is different from typical programming (procedural or OO).

Resources