PostgreSQL select sum inside procedure - sql

I want a procedure that checks, which buyer spent more than 1600$ and than I want to print out their info, so I could send them a gift card.
I get error: SQL Error [42601]: ERROR: query has no destination for result data
when I call this procedure:
create or replace procedure bookstore.procedure1 ()
language plpgsql
as $$
declare
i integer := 0;
temp_price numeric;
temp_sum numeric;
temp_foreign_key integer;
number_of_buyers integer := (select count(*) from bookstore.buyer);
begin
while i < number_of_buyers loop
select sum (price) as temp_sum from bookstore.receipt where id_buyer = i;
if temp_sum > 1600 then
select id_buyer, name, surname, adress from bookstore.buyer where id_buyer = i;
end if;
i := i+1;
end loop;
end
$$;
The problem is in select sum (price) as temp_sum... row. I have the similar procedure in MySQL and it works. I tried running that row with id_buyer = 20 for example and it worked. How should I change it?
That's my first question on stackoverflow, I hope it's understandable.

Let SQL do all the work of serving the data. This can be done in a single statement.
select buy.buy_id
, buy.name
, buy.address
, buy.city
, buy.postal_code
, pur.purchases
from buyers buy
join ( select inv.buy_id
, sum(inv.amount) purchases
from invoices inv
group by buy_id
having sum(inv.amount) >= 1600.00
) pur
on (pur.buy_id = buy.buy_id) ;
If you must have a stored program, then wrap the above query in a SQL function that returns a table. then Select from the function.
create or replace
function preferred_buyers()
returns table ( buy_id integer
, name text
, address text
, city text
, postal_code text
, purchases numeric(7,2)
)
language sql
as $$
select buy.buy_id
, buy.name
, buy.address
, buy.city
, buy.postal_code
, pur.purchases
from buyers buy
join ( select inv.buy_id
, sum(inv.amount) purchases
from invoices inv
group by buy_id
having sum(inv.amount) >= 1600.00
) pur
on (pur.buy_id = buy.buy_id) ;
$$;
select * from preferred_buyers();
Neither of the above handles printing task. This is not something SQL very good at. Actually it cannot do so. Printing requires a programming language extension; for Postgres it is plpgsql. Also realize any printing done is not available at least in a production environment; it would be on the DB server. Handle your printing in the presentation manager (app).

You probably need to add the return statement
In the Function creation definition SQL, there must be a row written as RETURNS integer/text
Try-
language plpgsql
as $$
declare
i integer := 0;
temp_price numeric;
temp_sum numeric;
temp_foreign_key integer;
number_of_buyers integer := (select count(*) from bookstore.buyer);
begin
while i < number_of_buyers loop
select sum (price) as temp_sum from bookstore.receipt where id_buyer = i;
if temp_sum > 1600 then
select id_buyer, name, surname, adress from bookstore.buyer where id_buyer = i;
end if;
i := i+1;
end loop;
return 1;
end
$$;
Or else you can remove the initial statement of RETURNS text/integer by using CREATE OR REPLACE FUNCTION your_function ...

In plpgsql SELECT statement must have a receiver.
You must redirect the SELECT output with INTO or use direct affectation
--Fist version
SELECT SUM(price) FROM bookstore.receipt WHERE id_buyer = i INTO temp_sum;
--Second version, keep surrounding parenthesis
temp_sum = (SELECT SUM(price) FROM bookstore.receipt WHERE id_buyer = i);
Regarding your loop, you should use something more efficient and safe
DECLARE
temp_sum NUMERIC;
v_buyer bookstore.BUYER;
BEGIN
FOR v_buyer IN SELECT * FROM bookstore.buyer LOOP
--Use coalesce to manage empty result
temp_sum = (SELECT coalesce(SUM(price), 0) FROM bookstore.receipt WHERE id_buyer = v_buyer.id);
IF temp_sum > 1600 THEN
-- Your business logic
END IF;
END LOOP;
END
If your dataset is large it's better to use a cursor
DECLARE
temp_sum NUMERIC;
v_buyer_cursor CURSOR FOR SELECT * FROM bookstore.buyer;
v_buyer bookstore.BUYER;
BEGIN
FOR v_buyer IN v_buyer_cursor LOOP
temp_sum = (SELECT COALESCE(SUM(price), 0) FROM bookstore.receipt WHERE id_buyer = v_buyer.id);
IF temp_sum > 1600 THEN
-- Your business logic
END IF;
END LOOP;
END

None of the answers helped. I had to replace this row:
SELECT id_buyer, name, surname, adress FROM bookstore.buyer WHERE id_buyer = i;
with this row:
INSERT INTO bookstore.temp SELECT id_buyer, name, surname, adress FROM bookstore.buyer WHERE id_buyer = i;
Now it works.

Related

How to optimize insert from select in For loop in Oracle PL/SQL

I am trying to compute cash balances bunch of tables and insert them into another table. I have a loop which computes the balance for each date of the year and inserts it into the balance table.
The problem is that this sql takes almost half an hour:
declare
d_i number :=1
begin
loop
insert into my_cash_table
select
portfolio ,
book_name, position_type, currency, cash_balance_cad
from
cash_balance cb
-- plus bunch of code here
...
where bal_date in &sdate + d_i
group by book_name, position_type, currency;
d_i := d_i + 1;
if d_i > 366 then
exit;
end if;
end loop;
end;
Is there anyway for me to optimize it so that I can grab all the data in a loop and then just bulk insert? Also can I run simply select statement in a loop, i.e. without doing an insert?
Thank you
Maybe you need optimization of your query.
In order to exclude context switching between sql and plsql you may go two ways.
Use subquery or with statement to generate numbers from 1 to 366 and multiply inserting rows by joining to it:
begin
insert into my_cash_table
with days_num_q as
(select level num
from dual
connect by level <= 366)
select portfolio,
book_name,
position_type,
currency,
cash_balance_cad
from cash_balance cb
join cnt_days
on bal_date = &sdate + days_num_q.num
/* -- plus bunch of code here
.....
*/;
end;
Create nested collection of numbers from 1 to 366 and use forall statement:
declare
type days_num_tbl_type is table of number;
days_num_tbl days_num_tbl_type;
d_i number;
begin
d_i := 1:
days_num_tbl := days_num_tbl_type();
days_num_tbl.extend(366);
while d_i <= 366 loop
days_num_tbl(d_i) := d_i;
d_i := d_i + 1;
end loop;
forall i in days_num_tbl.FIRST..days_num_tbl.LAST
insert into my_cash_table
select portfolio,
book_name,
position_type,
currency,
cash_balance_cad
from cash_balance cb
/* -- plus bunch of code here
.....
*/
where bal_date = &sdate + days_num_tbl(i);
end;

Oracle sql to pgsql with table() function issue

I am doing the migration from Oracle to pgsql and I've the the oracle sql like below:
select code,product_no,qty qty from t_ma_tb_inventory
where code is not null and status=2
and update_type in (select * from table(splitstr(:types,',')))
and tb_shop_id=:shopId
order by update_time
and the splitstr function like below:
CREATE OR REPLACE FUNCTION splitstr (p_string text, p_delimiter text) RETURNS SETOF STR_SPLIT AS $body$
DECLARE
v_length bigint := LENGTH(p_string);
v_start bigint := 1;
v_index bigint;
BEGIN
WHILE(v_start <= v_length)
LOOP
v_index := INSTR(p_string, p_delimiter, v_start);
IF v_index = 0
THEN
RETURN NEXT SUBSTR(p_string, v_start);
v_start := v_length + 1;
ELSE
RETURN NEXT SUBSTR(p_string, v_start, v_index - v_start);
v_start := v_index + 1;
END IF;
END LOOP;
RETURN;
END
$body$
LANGUAGE PLPGSQL
SECURITY DEFINER
;
-- REVOKE ALL ON FUNCTION splitstr (p_string text, p_delimiter text) FROM PUBLIC;
can someone help me to write the equivalent code in pgsql?Thank you very much
You don't need to write your own function - Postgres already has a built-in function for that: string_to_array
select code, product_no, qty qty
from t_ma_tb_inventory
where code is not null and status=2
and update_type = any ( string_to_array(:types,',') )
and tb_shop_id = :shopId
order by update_time;
If you are passing a text, but you need to compare that to an integer, you need to cast the resulting array:
select code, product_no, qty qty
from t_ma_tb_inventory
where code is not null and status=2
and update_type = any ( string_to_array(:types,',')::int[] )
and tb_shop_id = :shopId
order by update_time;
However: that is a pretty ugly workaround that is only necessary in Oracle because it has no support for real arrays in SQL (only in PL/SQL).
In Postgres it would be much better to pass an array of integers directly (e.g. make :types an int[]). Then no parsing or casting would be necessary:
select code, product_no, qty qty
from t_ma_tb_inventory
where code is not null and status=2
and update_type = any ( :types )
and tb_shop_id = :shopId
order by update_time;

SQL QUERY: How to assign random different value of column in a table, to differents row in another table?

I'm developing a desktop application that works like Risiko!'s game, based on some database procedures.
I need to assign to each player a tot of rows which are not equal to each other assigned to other players.
I've tried for something like this, but doesn't work:
`create or replace PROCEDURE "giveterritory"
(idpartita NUMBER,nr_partecipanti number )
is
begin
DECLARE
CURSOR cur_file IS
SELECT *
FROM Player
WHERE idgame = Player.idgame ;
var_cur cur_file%ROWTYPE;
cont number ;
nr_territories_gioc_prec number ;
G1_Terr int ;
G2_Terr int ;
G3_Terr int ;
G1_ID number;
G2_ID number;
G3_ID number;
begin
cont:=0 ;
for var_cur in cur_file
loop
cont := cont + 1 ;
update territory
set Player_owner = var_cur.id_Player
where ROWNUM = MOD( MOD ( number_RANDOM(),42 ) ,ROWNUM );
G1_Terr := var_cur.number_territories_tot ; -- mi salvo i territories del primo Player
G1_ID := var_cur.id_Player ;
end if ;
if (cont=2 ) then
update territory
set Player_owner = var_cur.id_Player
where ROWNUM = MOD( MOD ( number_RANDOM(),42 -G1_Terr ) , ROWNUM ) and id_territory NOT IN ( select Player_owner from territory where ;
G2_Terr := var_cur.number_territories_tot ;
end if ;
if (cont=3 ) then
update territory
set Player_owner = var_cur.id_Player
where ROWNUM <= 42 - G1_Terr - G2_Terr;
--where ROWNUM <= 42 - var_cur.number_territories_tot * 2 ;
G3_Terr := var_cur.number_territories_tot ;
end if ;
end loop;
end;
end;`
number_random code:
create or replace FUNCTION "NUMBER_RANDOM"
return NUMBER
is
numbrndm int;
begin
select dbms_random.value(1,99999999) into numbrndm
from dual;
return numbrndm;
end;
If I understand correctly, you're trying to assign each player a random set of territories?
If so, sort the territories in random order and assign them to players round robin. Here's a MERGE statement that should do that. I apologize for any minor syntax errors: I am not in front of an Oracle database right now.
MERGE INTO territory t
USING (
WITH players AS ( SELECT rownum player#, id_player
FROM player
WHERE idgame=:idgame ),
players_count AS ( SELECT count(*) players_count FROM players )
-- Get the id_player for each assignment
SELECT assignedt.id_territory, p.id_player
FROM
(
-- Assign the randomly-ordered territories to players in round-robin format
SELECT randt.id_territory,
mod(rownum, pc.players_count)+1 player#
FROM (
-- Query the territories in a random order
SELECT t.id_territory
FROM territory t
ORDER BY dbms_random.random ) randt, players_count pc
-- Optional:
-- ... add a where clause to limit rownum <= some number to make
-- ... sure each player receives an even number of territories.
-- ... You'd need the count of territories to do that.
) assignedt inner join players p on p.player# = assignedt.player#
) u
ON ( u.id_territory = t.id_territory )
WHEN MATCHED THEN UPDATE SET t.id_player = u.id_player;
I'd put a random number column in your SQL selects for each row. Then you can order your record set by the random number directly in SQL.
Do this for both players and territory lists.
This example should work for any number of territories and players. If the territories don't divide up equally, it will be at random which players get one extra territory.
We are going to loop through the territories list once, in random order. We are going to step through the player list until we reach the end, then reload the player list again. This happens as many times as needed.
Might need to fix this for syntax or logic. I don't have an Oracle instance to test it.
CREATE OR REPLACE PROCEDURE giveterritory (pID_GAME PLS_INTEGER) IS
CURSOR c_player IS
SELECT id_player, dbms_random.value() rnd
FROM players
WHERE id_game = pID_GAME
ORDER BY 2 desc;
--
CURSOR c_territory IS
SELECT id_territory, dbms_random.value() rnd
FROM territories
WHERE id_game = pID_GAME
FOR UPDATE of id_player
ORDER BY 2 desc;
--
l_player_row c_player%ROWTYPE;
l_territory_row c_territory%ROWTYPE;
BEGIN
OPEN c_player;
FETCH c_player into l_player_row;
--
FOR r in c_territory
LOOP
UPDATE territories
SET id_player = l_player_row.id_player
WHERE CURRENT OF c_territory;
--
FETCH c_player INTO l_player_row;
IF c_player%NOTFOUND THEN
CLOSE c_player;
OPEN c_player;
FETCH c_player INTO l_player_row;
END IF;
END LOOP;
END;

Calling a function from an explicit cursor

I need to call the stored function findtotalcarmodels from this PL/SQL block. The way this code is written is not the way I would do it in production, however it is an exercise in 'lateral' thinking.
SET SERVEROUTPUT ON FORMAT WRAP SIZE 12000
Declare
v_model VARCHAR2(40);
v_cost NUMBER;
v_reg VARCHAR2(10);
v_carcategory VARCHAR2(40);
v_totalcars NUMBER;
v_count DATE;
v_maxcount DATE;
v_maxdept VARCHAR2(20);
cursor carcur IS
SELECT * FROM i_car;
v_car carcur%ROWTYPE;
Cursor c_date (p_reg i_booking.registration%TYPE) IS
SELECT date_reserved
FROM i_booking
WHERE registration = p_reg;
v_date c_date%ROWTYPE;
Begin
v_totalcars := findtotalcarmodels();
FOR v_car IN carcur LOOP
If v_cost <=50000 THEN v_carcategory := 'Budget Car';
End IF;
If v_cost BETWEEN 50000 AND 100000 THEN v_carcategory := 'Standard Car';
End IF;
If v_cost >100000 THEN v_carcategory := 'Premium Car';
End If;
FOR v_date IN c_date(v_car.registration) LOOP
v_count := v_count + 1;
END LOOP;
IF v_count > v_maxcount THEN
v_maxcount := v_count;
v_maxdept := v_car.registration;
END IF;
DBMS_OUTPUT.PUT_LINE('Registration:'|| ' '|| v_car.registration);
DBMS_OUTPUT.PUT_LINE('Cost:'|| '$' ||v_car.Cost);
DBMS_OUTPUT.PUT_LINE('Model Name:'|| ' '||v_car.model_name);
DBMS_OUTPUT.PUT_LINE('Car Category:'|| ' '||v_carcategory);
DBMS_OUTPUT.PUT_LINE('Total number of Cars:'|| ' '||v_totalcars);
DBMS_OUTPUT.PUT_LINE('Most Recent Rental Date: '|| ' '||v_maxcount);
DBMS_OUTPUT.NEW_LINE;
END LOOP;
END;
I am getting the error:
v_totalcars := findtotalcarmodels();
*
ERROR at line 19:
ORA-06550: line 19, column 16:
PLS-00306: wrong number or types of arguments in call to 'FINDTOTALCARMODELS'
ORA-06550: line 19, column 1:
PL/SQL: Statement ignored
Am I calling my function correctly in the right position?
This is the function:
CREATE OR REPLACE Function findtotalcarmodels
(model_name_in IN varchar2)
RETURN NUMBER
IS
counter INTEGER := 0;
CURSOR car_count_cur IS
SELECT model_name FROM i_car WHERE model_name = model_name_in;
Rec_car_details car_count_cur%ROWTYPE;
BEGIN
OPEN car_count_cur;
LOOP
FETCH car_count_cur INTO Rec_car_details;
EXIT WHEN car_count_cur%NOTFOUND;
counter := counter + 1;
END LOOP;
CLOSE car_count_cur;
RETURN counter;
END;
Okay, so I have no idea why you're getting that error with that function. The error indicates that you're not giving the function the correct number of arguments. Judging by the function that's clearly not what's happening, or it's not the same function.
You've just changed the function call; the function requires an argument so the "incorrect" code you had in your first revision was actually correct.
However, let's put that to one side for a second and look again at what you're doing.
Your function is a count on a table. There's no need for a cursor or looping, incrementing variables or anything. You can simplify it to
select count(*) from i_car where model_name = :model_name
You never assign the variables v_count or v_maxcount a value so incrementing them will still result in a NULL. They're dates anyway, so it's a little strange to be incrementing them.
Your cursor c_date is just another count; once again no need for a loop.
The model_name variable is never initialised so your function will not return a result.
There are a lot of ways to simplify this; though I'm going to guess a few things here. Change your carcur cursor to the following:
select i.*
, case cost
when <= 50000 then 'Budget Car'
when <= 100000 then 'Standard Car'
else 'Premium Car'
end as category
, count(*) over ( partition by model_name ) as total_cars
from i_cars
This appears to enable you to remove your IF statements and your function. You can then remove your second loop by performing an outer join on i_booking (you need to add the primary key in yourself):
select i.*
, case c.cost
when <= 50000 then 'Budget Car'
when <= 100000 then 'Standard Car'
else 'Premium Car'
end as category
, count(distinct c.primary_key)
over ( partition by c.model_name ) as total_cars
, count(b.date_reserved)
over ( partition by b.registration ) as reserved_ct
from i_cars c
left outer join i_booking b
on c.registration = b.registration
I'm not 100% certain on the max stuff as it's not clear at all where it's assigned (it's not) but it looks as though you might be wanting to find the maximum count by model etc, in which case you can use a sub-query on the above cursor:
select sub.*
, max(total_cars) over () as max_cars
, max(reserved_ct) over () as max_reserved
from ( select i.*
, case c.cost
when <= 50000 then 'Budget Car'
when <= 100000 then 'Standard Car'
else 'Premium Car'
end as category
, count(distinct c.primary_key)
over ( partition by c.model_name ) as total_cars
, count(b.date_reserved)
over ( partition by b.registration ) as reserved_ct
from i_cars c
left outer join i_booking b
on c.registration = b.registration
) sub
If you then need to output it (there's rarely a need) you can loop through this single SQL statement, which gives you everything in one go:
declare
c_cursor is
select sub.*
, max(total_cars) over () as max_cars
, max(reserved_ct) over () as max_reserved
from ( select i.*
, case c.cost
when <= 50000 then 'Budget Car'
when <= 100000 then 'Standard Car'
else 'Premium Car'
end as category
, count(distinct c.primary_key)
over ( partition by c.model_name ) as total_cars
, count(b.date_reserved)
over ( partition by b.registration ) as reserved_ct
from i_cars c
left outer join i_booking b
on c.registration = b.registration
) sub
;
begin
for i in c_cursor loop
dbms_output.put_line(i.registration);
dbms_output.put_line(i.cost);
...
end loop;
end;
I've reduced a PL/SQL block and a function to a single SQL statement; it may not be spot on because there's so many unknowns but it's worth trying for yourself. Simple is almost always better.

Getting an error in sql, when executing code below.How to declare a table type in plsql. Am a beginner . Please suggest

create or replace procedure BAS_NUM_UPD is
cursor cur is
select distinct o.oi_b,mpr.pa_ke_i,ltrim(substr(convert_171_to_711(cp.p_t_num),1,7),'0') bs_nbr
from t_obj o, mat_pa_rel mp, cor_pa cp
where o.ob_t = 'something'
and o.oi_b = mp.oi_b
and mp.pa_ke_i = cp.pa_ke_i;
l_ba_num_at_i number(10) := get_attribute_id('Ba timber');
flag1 VARCHAR2(10);
type t1 is table of varchar2(10);
par_k t1;
BEGIN
for x in cur loop
BEGIN
select pa_ke_i into par_k from mat_pa_rel where oi_b=x.oi_b ;
if par_k.count=null then
insert into cs_val (oi_b, at_i, value, flag, ) values (x.oi_b, l_ba_num_at_i, null, 1);
end if;
select flag into flag1 from cs_val where at_i = l_ba_num_at_i and oi_b = x.oi_b
and value = x.bs_nbr;
EXCEPTION
when NO_DATA_FOUND THEN
insert into cs_val (oi_b, at_i, value, flag, )
values (x.oi_b, l_ba_num_at_i, x.bs_nbr, 1);
flag1 :='Nothing';
when OTHERS
then
raise_application_error(-20011,'Unknown Exception in PROCEDURE');
END;
end loop;
end BAS_NUM_UPD;
error:
PLS-00642: local collection types not allowed in SQL statements
You should get it running if you do a bulk collect
select pa_ke_i bulk collect into par_k from mat_pa_rel where oi_b=x.oi_b ;
Then I think the if is not right. I think you need to do
if par_k.count = 0 then
But to be honest you might just make a count
select count(*) into l_cnt from mat_pa_rel where oi_b=x.oi_b;
If l_cnt = 0 then ...
Of course l_cnt has to be defined.
You should create type t1 in the schema and not in the pl/sql block.