Remove duplicates from associative array with multiple columns in PL/SQL - arrays

I have seen several great examples of removing duplicates in an Oracle associative array using MULTISET UNION DISTINCT and SET. It works great when there is only a single column.
I have an associative array based on a RECORD type which contains 3 columns.
Is it possible to use the methods mentioned above?
DECLARE
TYPE rec_type IS RECORD(
column1 VARCHAR2(5)
,column2 VARCHAR2(5));
TYPE my_aa IS TABLE OF rec_type;
p_tbl my_aa := my_aa();
q_tbl my_aa := my_aa();
BEGIN
p_tbl.extend(4);
p_tbl(1).column1 := 'A1';
p_tbl(1).column2 := 'a';
--
p_tbl(2).column1 := 'A1';
p_tbl(2).column2 := 'b';
--
p_tbl(3).column1 := 'A1'; -- Dup
p_tbl(3).column2 := 'a'; -- Dup
--
p_tbl(4).column1 := 'A1';
p_tbl(4).column2 := 'c';
--
dbms_output.put_line('-- First output contains duplicated');
--
FOR a IN p_tbl.first .. p_tbl.last LOOP
dbms_output.put_line(a || ' = ' || p_tbl(a).column1 || ' / ' || p_tbl(a).column2);
END LOOP;
--
--
q_tbl := p_tbl MULTISET UNION DISTINCT p_tbl;
--
--
dbms_output.new_line;
dbms_output.put_line('-- Duplicates have been removad');
FOR a IN q_tbl.first .. q_tbl.last LOOP
dbms_output.put_line(a || ' = ' || q_tbl(a).column1|| ' / '||q_tbl(a).column2);
END LOOP;
END;

It is impossible to do distinction by MULTISET UNION DISTINCT with underlying RECORD type due to doc
The element types of the nested tables must be comparable. Please
refer to "Comparison Conditions " for information on the comparability
of nonscalar types.
https://docs.oracle.com/cd/B12037_01/server.101/b10759/operators006.htm
Records are not comparable. You can use user defined objects with MAP method for this
Two objects of nonscalar type are comparable if they are of the same
named type and there is a one-to-one correspondence between their
elements. In addition, nested tables of user-defined object types,
even if their elements are comparable, must have MAP methods defined
on them to be used in equality or IN conditions.
https://docs.oracle.com/cd/B12037_01/server.101/b10759/conditions002.htm#i1033286
If you still whant do this with records then try SELECT DISTINCT ... FROM TABLE() approach, but your types must be defined on schema level for server older than oracle 12c.

Related

How to define an array variable in Snowflake

How to define an array variable in snowflake worksheet?
set columns = (SELECT array_agg(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS
where table_name='MEMBERS');
I get this error:
Unsupported feature 'assignment from non-constant source expression'.
2022 update:
Now it's possible with Snowflake Scripting:
declare
tmp_array ARRAY default ARRAY_CONSTRUCT();
rs_output RESULTSET;
begin
for i in 1 to 20 do
tmp_array := array_append(:tmp_array, OBJECT_CONSTRUCT('c1', 'a', 'c2', i));
end for;
rs_output := (select value:c1, value:c2 from table(flatten(:tmp_array)));
return table(rs_output);
end;
https://stackoverflow.com/a/71231621/132438
Previously:
Instead of storing an array, aggregate in a comma separated string:
set x = (SELECT listagg(COLUMN_NAME, ',') FROM INFORMATION_SCHEMA.COLUMNS WHERE COLUMN_NAME LIKE 'TABLE_S%');
However: "The size of string or binary variables is limited to 256 bytes" according to https://docs.snowflake.com/en/sql-reference/session-variables.html.
Which means that even if you could store an array in a variable, it would probably exceed the limits. Instead store the result in [temp] tables or so.
I found if you encapsulate with to_json it will work:
set x = (SELECT to_json(array_agg(COLUMN_NAME)) FROM INFORMATION_SCHEMA.COLUMNS
where table_name='CUSTOMER');
select $x;

ORA-06533: Subscript beyond count with associative array

I am getting Subscript beyond count error while executing the procedure. I am trying to populate an associative array in my procedure and then eventually use it to insert. I think there is somewhere error in initialization of the array
Type definitions in a package header
TYPE sonic_data_rec IS RECORD(
ep_a_num LOG_PICK.EP_A_NUM%TYPE,
wvy_num LOG_PICK.WVY_NUM%TYPE,
ltrl_name LOG_PICK.Ltrl_Name%TYPE,
vel_pick_sq_num LOG_PICK.w_vel_pick_sq_num%TYPE,
vel_sv_typ_cd LOG_PICK.W_VEL_SV_TYP_CD%TYPE,
vel_sv_dt LOG_PICK.W_VEL_SV_DT%TYPE,
w_vel_log_pick_cd LOG_PICK.w_vel_log_pick_cd%TYPE,
sq_num LOG_PICK.W_VEL_PICK_SQ_NUM%TYPE,
pick_cd LOG_PICK.W_VEL_LOG_PICK_CD%TYPE,
onew_tm LOG_PICK.W_VEL_ONE_WAY_TV_TM%TYPE,
sonic_log LOG_PICK.W_VEL_SOLOG_CALB_VSP%TYPE,
ms_dpth LOG_PICK.W_VEL_PICK_DPTH%TYPE,
tv_dpth LOG_PICK.W_VEL_PICK_DPTH%TYPE);
TYPE sonic_tab IS TABLE OF sonic_data_rec;
Procedure code, only relevant code of package_body posted
PROCEDURE LOAD_LOG_PICK
(p_wvy_num IN WELL_VEL_SURVEY.WVY_NUM%TYPE
,p_noofpts IN NUMBER
,p_data_list IN STRINGARRAY)
IS
l_son_array sonic_tab := sonic_tab();
BEGIN
count1 := 1;
LOOP
l_son_array(count1).ep_a_num := p_ep_a_num;
l_son_array(count1).wvy_num := p_wvy_num;
l_son_array(count1).vel_sv_typ_cd := 'L';
l_son_array(count1).vel_sv_dt := p_pick_date;
l_son_array(count1).vel_pick_sq_num := count1;
l_son_array(count1).w_vel_log_pick_cd := 'O';
l_son_array(count1).ms_dpth := l_ms_dpth;
l_son_array(count1).onew_tm := regexp_substr(p_data_list(count1), '[^ ]+', 1, 1);
l_son_array(count1).sonic_log := regexp_substr(p_data_list(count1), '[^ ]+', 1, 1);
EXIT WHEN count1=5000;
count1 := count1+1;
END LOOP;
You are attempting to populate a nested table, not an associative array.
If sonic_tab was an associative array, it would be declared as
TYPE sonic_tab IS TABLE OF sonic_data_rec INDEX BY BINARY_INTEGER;
See the Oracle documentation on PL/SQL collection types for further information about the different types of collections you can use in PL/SQL.
As you have a nested table, you need to add calls to sonic_tab.EXTEND to make the table larger. It starts with size 0 because when you create it by calling sonic_tab(), you aren't passing any records into the constructor. Either call sonic_tab.EXTEND(1); in each iteration of the loop before you try to add anything to that item of the table, or call sonic_tab.EXTEND(5000); once before the loop if you know in advance how many items you will have in the table.

How to add new elements to an array of a user-defined type?

I have converted some stored procedure from Oracle to PostgreSQL but faced the below issue:
I have a user-defined type in PostgreSQL:
CREATE TYPE ut_merci_row AS (
SGLCNTNR varchar(100),
CODEST_MERCEVARIA varchar(50),
CODCICLO smallint
);
CREATE TYPE ut_merci_table AS (ut_merci_table UT_MERCI_ROW[]);
In Oracle:
retTable UT_MERCI_TABLE := UT_MERCI_TABLE();
.....
retTable.extend;
retTable(retTable.last) := UT_MERCI_ROW(rec.SGLCNTNR,rec.CODEST_MERCEVARIA,rec.CODCICLO);
Could you advise me how to convert the 2 last below code lines to PostgreSQL?
retTable.extend;
retTable(retTable.last) := UT_MERCI_ROW(rec.SGLCNTNR,rec.CODEST_MERCEVARIA,rec.CODCICLO);
Someone said that we don't need to "extend" the collection in PostgreSQL.
You can simply append the value to the array using the || concatenation operator. Make sure you initialize the variable to an empty array (otherwise it would null)
retTable ut_merci_row[] := '{}';
Then in the loop, just append a value of the type to the array:
retTable := rettable || row(rec.SGLCNTNR,rec.CODEST_MERCEVARIA,rec.CODCICLO)::ut_merci_row;
Trying to mimic Oracle's complicated way of returning tables with a cursor and custom types is not the right way to do this in Postgres.
But a much better solution would be to get rid of all that cursor processing and array handling and return a set of the base type:
CREATE OR REPLACE FUNCTION uf_getstatomerce1()
returns setof ut_merci_row
AS $body$
SELECT DISTINCT row(m.SGLCNTNR, m.CODEST_MERCEVARIA, 1 as CODCICLO)::ut_merci_row
from merce.DELIVERY_ORDER_RIF rif
inner join merce.DELIVERY_ORDER_PARTITA pm on rif.DO_ID = pm.DO_ID
inner join merce.vPARTITEMERCE m on pm.CODPARTITAMERCE = m.CODPARTITAMERCE;
$body$
LANGUAGE sql
STABLE;
In fact, in Postgres you also don't need a custom type for this, just declare the function as returns table and forget all the overhead:
CREATE OR REPLACE FUNCTION uf_getstatomerce1()
returns table (SGLCNTNR varchar, CODEST_MERCEVARIA varchar, CODCICLO smallint )
AS $body$
SELECT DISTINCT row(m.SGLCNTNR, m.CODEST_MERCEVARIA, 1 as CODCICLO)::ut_merci_row
from merce.DELIVERY_ORDER_RIF rif
inner join merce.DELIVERY_ORDER_PARTITA pm on rif.DO_ID = pm.DO_ID
inner join merce.vPARTITEMERCE m on pm.CODPARTITAMERCE = m.CODPARTITAMERCE;
$body$
LANGUAGE sql
STABLE;
The SQL function that simply returns a query will be much more efficient than the cursor loop.
In both cases, you can use it like this:
select *
from uf_getstatomerce1() ;
The second type definition is pretty redundant, just use ut_merci_row[].
You don't have to “extend” an array in PostgreSQL, just assign to an index that is not yet used.

How i can add one more value to my varray list in a column in pl/sql?

I'm trying to add one more value to my varray list of numbers called burse using multiset union all but I get this error. [1
But when I insert a single value its working. Example:
What I am doing wrong ?
This is how I declare and insert into column
This should work...
update student
set istoric = istoric multiset union all bure(42, 23)
where id = 1
... except that you're now using a VARRAY (and not the nested table you had in your previous question). So you get an error message:
ORA-00932: inconsistent datatypes: expected UDT got BURE
The reason is, according to the documentation:
"While nested tables can also be changed in a piecewise fashions, varrays cannot....However, you cannot update or delete individual varray elements directly with SQL; you have to select the varray from the table, change it in PL/SQL, then update the table to include the new varray." (emphasis mine)
This is because VARRAYs are ordered sets while Nested Tables are not. Unless there is a firm requirement to maintain the order of elements it is better to use Nested Tables rather than Varrays: they're just more convenient.
So here is how you can update a Varray using PL/SQL:
declare
lv bure;
cnt pls_integer;
begin
select istoric into lv
from student
where id = 1;
cnt := lv.count();
lv.extend();
lv(cnt+1) := 23 ;
lv.extend();
lv(cnt+2) := 69 ;
update student
set istoric = lv
where id = 1;
end;
/

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

Resources