How to update multiple rows with values of two arrays in Postgres? - arrays

I need to update a set of records in the x table with values stored in arrays. Anyone know how I could do it?
IDS_NEW_SPONSORS INTEGER[] := ARRAY[]::INTEGER[]; --possible values [1,2,3,4,5,6]
IDS_DEBTORS INTEGER[] := ARRAY[]::INTEGER[]; --possible values [20,21,22,23,24,25]
-- the arrays will be the same size--
UPDATE x
SET id_sponsor = [IDS_NEW_SPONSORS],-- HOW ADD
id_sponsor_historial = [IDS_DEBTORS], -- HOW ADD
id_patron = 5 -- ALWAYS SAME
WHERE id_sponsor = ANY(IDS_DEBTORS); -- this line working!!! loop each row
I'm working with postgres 11...
After update. the table would look like this:
id_sponsor | id_sponsor_historial | id_patron
1 20 5
2 21 5
3 22 5
4 23 5
5 24 5
6 25 5

The next a little tricky query do this work:
(The trick is match value in new values array by position in old values array)
UPDATE sponsors
SET
id_sponsor_historial = id_sponsor, -- save previous value as historical
id_sponsor = (array[1,2,3,4,5,6])[array_position(array[20,21,22,23,24,25], id_sponsor)],-- update new value
id_patron = 5
WHERE id_sponsor = ANY(array[20,21,22,23,24,25]); -- find rows
I rest the integers array for example. In your case it should be changed to variables. The example works only if new and old values arrays have equals lengths.

You can use a function to do that. Here is an example:
create table x(
id_sponsor int primary key,
id_sponsor_historial int,
id_patron int);
CREATE OR REPLACE FUNCTION update_x(
integer,
integer[],
integer[])
RETURNS integer AS
$BODY$
DECLARE
in_pat ALIAS FOR $1;
in_sp_arr ALIAS FOR $2;
in_sp_hi_arr ALIAS FOR $3;
BEGIN
IF in_sp_arr ISNULL
THEN
RETURN 0;
END IF;
IF array_lower(in_sp_arr,1) ISNULL
OR array_upper(in_sp_arr,1) ISNULL
THEN
RETURN 0;
END IF;
FOR index IN array_lower(in_sp_arr,1) .. array_upper(in_sp_arr,1) LOOP
BEGIN
INSERT INTO x(id_sponsor, id_sponsor_historial, id_patron )
VALUES ( in_sp_arr[index], in_sp_hi_arr[index], in_pat);
--EXCEPTION WHEN unique_violation THEN
--do nothing ?
END;
END LOOP;
RETURN 1;
END;
$BODY$
LANGUAGE plpgsql ;
select update_x(5, ARRAY[1,2,3,4,5,6],ARRAY[20,21,22,23,24,25]);
This would probably need more error checking: It assumes for example, that the arrays are of the same size, have the same bounds and all the elements are defined.

What I was trying to do is update the records as follows
A: Replacing the old sponsors (old_sponsor) with the new ones (new_sponsor)
B: Updating the historical_sponsor_id with the values ​​of old_sponsor
C: Adding all of them the same id_patron
I could do it this way based on the #clamp response..
DECLARE
new_sponsor INTEGER[] := array[2300,2034,2032];
old_sponsor INTEGER[] := array[1500,1501,1502];
BEGIN
FOR i IN array_lower(new_sponsor, 1) .. array_upper(new_sponsor, 1)
LOOP
UPDATE x SET id_sponsor = new_sponsor[i],
id_sponsor_historial = old_sponsor[i], id_patron = 5
WHERE id_sponsor = old_sponsor[i];
END LOOP;
END
I would like to know if it is useful for processing more than 10,000 records or if there is a better way to do it efficiently.

The most efficient way to achieve this would be using UNNEST:
UPDATE sponsors
SET
-- save previous value as historical
id_sponsor_historial = bulk_query.id_sponsor,
-- update new value
id_sponsor = bulk_query.updated_id_sponsor,
-- set another field
id_patron = 5
FROM
(
SELECT * FROM UNNEST(
array[20,21,22,23,24,25]::INT[],
array[1,2,3,4,5,6]:INT[]
) AS t(id_sponsor, updated_id_sponsor)
) AS bulk_query
WHERE
sponsors.id_sponsor=bulk_query.id_sponsor
This effectively creates a temporary table from the two arrays, and then uses that to perform the update.
I've written more about using UNNEST with Postgres in this blog post: https://www.atdatabases.org/blog/2022/01/21/optimizing-postgres-using-unnest

Related

How i can initialize a array with record items in pl/sql?

I'm relatively new to pl/sql and i'm trying to make a list with records objects but i dont know how to initialize for each item of the list both fields from record item. For example : in procedure "new item" how i can initialize example(1) ? with example(1).id_std := integer and example(1).procent := integer ? Thanks!
This is how my code looks like :
set serveroutput on;
CREATE OR REPLACE PACKAGE newExercise IS
TYPE item IS RECORD(
id_std INTEGER,
procent INTEGER
);
TYPE tabel IS VARRAY(5) OF item;
PROCEDURE newItem (example tabel);
example2 tabel := tabel();
end newExercise;
/
CREATE OR REPLACE PACKAGE BODY newExercise IS
PROCEDURE newItem (example tabel) IS
BEGIN
FOR i IN 1..example.LIMIT LOOP
DBMS_OUTPUT.PUT_LINE(example(i));
end loop;
end newItem;
end newExercise;
/
Record types are for storing the results of queries. So you could do this:
declare
recs newExercise.tabel;
begin
select level, level * 0.25
bulk collect into recs
from dual
connect by level <= 5;
newExercise.newItem (recs);
end;
/
Note that VARRAY is not a suitable collection type for this purpose, because it's not always possible to predict how many rows a query will return. It's better to use
TYPE tabel IS table OF item;
When you refer to the record you usually have to specify specific fields. This populates the records with calculated values; to be able to do that I've had to changed the procedure argument from the default IN direction to IN OUT, both in the specification:
CREATE OR REPLACE PACKAGE newExercise IS
TYPE item IS RECORD(
id_std INTEGER,
procent INTEGER
);
TYPE tabel IS VARRAY(5) OF item;
PROCEDURE newItem (example IN OUT tabel);
-- ^^^^^^ make in/out to be updateable
-- example2 tabel := tabel(); -- not used
END newExercise;
/
and in the body:
CREATE OR REPLACE PACKAGE BODY newExercise IS
PROCEDURE newItem (example IN OUT tabel) IS
-- ^^^^^^ make in/out to be updateable
BEGIN
FOR i IN 1..example.LIMIT LOOP
-- extend collection to create new record
example.extend();
-- assign values to record fields
example(i).id_std := i;
example(i).procent := 100 * (1/i);
END LOOP;
END newItem;
END newExercise;
/
The LIMIT is five, from the definition, but the varray instance is initially empty (from tabel()). For population you can loop from 1 to that limit of five, but you have to extend() the collection to actually create the record in that position. Records are created with all fields set to null by default. You can then assign values to the fields of each record. (I've just made something up, obviously).
You can then test that with an anonymous block:
declare
example newExercise.tabel := newExercise.tabel();
begin
-- call procedure
newExercise.newItem(example);
-- display contents for debuggibg
FOR i IN 1..example.COUNT LOOP
DBMS_OUTPUT.PUT_LINE('Item ' || i
|| ' id_std: ' || example(i).id_std
-- ^^^^^^^ refer to field
|| ' procent: ' || example(i).procent);
-- ^^^^^^^ refer to field
END LOOP;
end;
/
Item 1 id_std: 1 procent: 100
Item 2 id_std: 2 procent: 50
Item 3 id_std: 3 procent: 33
Item 4 id_std: 4 procent: 25
Item 5 id_std: 5 procent: 20
PL/SQL procedure successfully completed.
I've put the original loop to display the contents of the array in that block, as you wouldn't generally have that as part of a procedure. You could still use LIMIT for that loop, but COUNT is safer in case the procedure doesn't fully populate it.
You can also extend once before the loop:
PROCEDURE newItem (example IN OUT tabel) IS
BEGIN
-- extend collection to create all new records
example.extend(example.LIMIT);
FOR i IN 1..example.LIMIT LOOP
example(i).id_std := i;
example(i).procent := 100 * (1/i);
END LOOP;
END newItem;
If you already know the values you want to assign - and they aren't coming from a table, in which case you'd use APC's approach - you can just assign to the last created record; this is a rather contrived example:
PROCEDURE newItem (example IN OUT tabel) IS
BEGIN
example.extend(); -- first record
example(example.LAST).id_std := 1;
example(example.LAST).procent := 7;
example.extend(); -- second record, left with null fields
example.extend(); -- third record
example(example.LAST).id_std := 3;
example(example.LAST).procent := 21;
example.extend(); -- fourth record, left with null fields
END newItem;
and the same anonymous block now gives:
Item 1 id_std: 1 procent: 7
Item 2 id_std: procent:
Item 3 id_std: 3 procent: 21
Item 4 id_std: procent:
PL/SQL procedure successfully completed.
Notice the null values, and that there is no 5th row.
Or again extend the collection once, and refer to the numbered records directly:
PROCEDURE newItem (example IN OUT tabel) IS
BEGIN
example.extend(4);
example(1).id_std := 1;
example(1).procent := 7;
example(3).id_std := 3;
example(3).procent := 21;
END newItem;
which gets the same result from the anonymous block.

Populate a database gives strange results

I have a database and I need to populate it's first 2 columns on every row. The first column is the date and the second column is an id.
My code is as follows:
.......
febr29:array[1..12] of byte = (31,29,31,30,31,30,31,31,30,31,30,31);
.......
procedure TForm.populate_database;
var
i,j,m,n: Integer;
begin
for i := 1 to 12 do
for j := 1 to febr29[i] do
for m := 1 to 9 do
for n := 1 to 15 do begin
database.tbl1.Append;
database.tbl1['date']:= inttostr(j)+'.'+inttostr(i)+'.2016';
database.tbl1['id']:='a'+inttostr(m)+inttostr(n);
database.tbl1.Post;
end;
end;
So basically I need to have all the ids on all the days of the year. But I have a problem with the code above: it gives me some strange output in the database, as in the following picture:
What am I doing wrong?
If your ID field is supposed to identify the data row, it would be better to declare it in the database as an integer column, not a character/string one.
It would also be better & less error prone not to try and calculate it from your loop variables, but use a running counter instead
procedure TForm.populate_database;
var
i,j,m,n: Integer;
ID : Integer;
begin
ID := 0;
for i := 1 to 12 do
for j := 1 to febr29[i] do
for m := 1 to 9 do
for n := 1 to 15 do begin
Inc(ID);
database.tbl1.Append;
database.tbl1['date']:= inttostr(j)+'.'+inttostr(i)+'.2016';
database.tbl1['id'].AsInteger :=ID;
database.tbl1.Post;
Of course, if you must have the 'a' prefix and a character coumn type for some reason, you could do
database.tbl1['id'].AsString :='a' + IntToStr(ID);
but even that may give you results you aren't expecting, unless to pad the result of IntToStr(ID) to a fixed length with leading zeroes.

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.

Find first non-null value along a path (array of nodes) in a hierarchical table

I have been fruitlessly trying for several hours to make a function that filter array subscripts based upon a criteria on the array from which the subscripts and then create an array of those subscripts.
The data structure I am dealing with is similar to the following sample (except with many more columns to compare and more complicated rules and mixed data types):
id hierarchy abbreviation1 abbreviation2
1 {1} SB GL
2 {2,1} NULL NULL
3 {3,2,1} NULL TC
4 {4,2,1} NULL NULL
I need to run a query that takes the next non-null value closest to the parent for abbreviation1 and abbreviation2 and compares them based upon the hierarchical distance from the current record in order to get a single value for an abbreviation. So, for example, if the first non-null values of abbreviation1 and abbreviation2 are both on the same record level abbreviation1 would take priority; on the other hand, if the first non-null abbreviation2 is closer to the current record then the corresponding non-null value for abbreviation1, then abbreviation2 would be used.
Thus the described query on the above sample table would yield;
id abbreviation
1 SB
2 SB
3 TC
4 SB
To accomplish this task I need to generate a filtered array of array subscripts (after doing an array_agg() on the abbreviation columns) which only contain subscripts where the value in an abbreviation column is not null.
The following function, based on all the logic in my tired mind, should work but does not
CREATE OR REPLACE FUNCTION filter_array_subscripts(rawarray anyarray,criteria anynonarray,dimension integer, reverse boolean DEFAULT False)
RETURNS integer[] as
$$
DECLARE
outarray integer[] := ARRAY[]::integer[];
x integer;
BEGIN
for i in array_lower(rawarray,dimension)..array_upper(rawarray,dimension) LOOP
IF NOT criteria IS NULL THEN
IF NOT rawarray[i] IS NULL THEN
IF NOT rawarray[i] = criteria THEN
IF reverse = False THEN
outarray := array_append(outarray,i);
ELSE
outarray := array_prepend(i,outarray);
END IF;
ELSE
IF reverse = False THEN
outarray := array_append(outarray,i);
ELSE
outarray := array_prepend(i,outarray);
END IF;
END IF;
END IF;
ELSE
IF NOT rawarray[i] is NULL THEN
IF reverse = False THEN
outarray := array_append(outarray,i);
ELSE
outarray := array_prepend(i,outarray);
END IF;
END IF;
END IF;
END LOOP;
RETURN outarray;
END;
$$ LANGUAGE plpgsql;
For example, the below query returns {5,3,1} when it should return {5,4,2,1}
select filter_array_subscripts(array['This',NULL,'is',NULL,'insane!']::text[]
,'is',1,True);
I have no idea why this does not work, I have tried using the foreach array iteration syntax but I cannot figure out how to cast the iteration value to the scalar type contained within the anyarray.
What can be done to fix this?
You can largely simplify this whole endeavor with the use of a RECURSIVE CTE, available in PostgreSQL 8.4 or later:
Test table (makes it easier for everyone to provide test data in a form like this):
CREATE TEMP TABLE tbl (
id int
, hierarchy int[]
, abbreviation1 text
, abbreviation2 text
);
INSERT INTO tbl VALUES
(1, '{1}', 'SB', 'GL')
,(2, '{2,1}', NULL, NULL)
,(3, '{3,2,1}', NULL, 'TC')
,(4, '{4,2,1}', NULL, NULL);
Query:
WITH RECURSIVE x AS (
SELECT id
, COALESCE(abbreviation1, abbreviation2) AS abbr
, hierarchy[2] AS parent_id
FROM tbl
UNION ALL
SELECT x.id
, COALESCE(parent.abbreviation1, parent.abbreviation2) AS abbr
, parent.hierarchy[2] AS parent_id
FROM x
JOIN tbl AS parent ON parent.id = x.parent_id
WHERE x.abbr IS NULL -- stop at non-NULL value
)
SELECT id, abbr
FROM x
WHERE abbr IS NOT NULL -- discard intermediary NULLs
ORDER BY id
Returns:
id | abbr
---+-----
1 | SB
2 | SB
3 | TC
4 | SB
This presumes that there is a non-null value on every path, or such rows will be dropped from the result.

Resources