single-row sub query return more than one row in procedure - oracle10g

procedure below throws exception
single-row sub query return more than one row
while executing query on sql> query execute fine and shows result
as expected
create or replace procedure discount_purchase(cust_name customer1.cust_name%type)
as
amt int;
discount int;
begin
select sum(purch_amt) into amt
from orders
where customer_id=(select customer_id from customer1 where cust_name=cust_name);
dbms_output.put_line('Total amount is='||amt);
if(amt>=500) then
discount:=amt-(0.25*amt);
dbms_output.put_line('Discount amount is='||discount);
else
dbms_output.put_line('NO Discount on='||amt||'Discount only above 500');
end if;
end;
/
­
SQL> exec discount_purchase('Nick Rimando');
BEGIN discount_purchase('Nick Rimando'); END;
*
ERROR at line 1:
ORA-01427: single-row subquery returns more than one row
ORA-06512: at "ARPAN.DISCOUNT_PURCHASE", line 6
ORA-06512: at line 1

The problem is that you have multiple customers in the customer1 table with the cust_name of 'Nick Rimando'. Have a look at
select count(1)
from customer1
where cust_name = 'Nick Rimando';
I bet it will result in a number higher than 1.
The fix(es)
The fix depends on what you actually need. And you actually did not write what you need. So, a few cases come to my mind...
Case 1 - Sum of purchases of all customers with that name
create or replace procedure discount_purchase(cust_name customer1.cust_name%type)
as
amt int;
discount int;
begin
select sum(purch_amt) into amt
from orders
where customer_id in (select customer_id from customer1 where cust_name=cust_name);
dbms_output.put_line('Total amount is='||amt);
if(amt>=500) then
discount:=amt-(0.25*amt);
dbms_output.put_line('Discount amount is='||discount);
else
dbms_output.put_line('NO Discount on='||amt||'Discount only above 500');
end if;
end;
/
Case 2 - Sum of purchases of arbitrary single customer with that name
create or replace procedure discount_purchase(cust_name customer1.cust_name%type)
as
amt int;
discount int;
begin
select sum(purch_amt) into amt
from orders
where customer_id = (select customer_id from customer1 where cust_name=cust_name and rownum <= 1);
dbms_output.put_line('Total amount is='||amt);
if(amt>=500) then
discount:=amt-(0.25*amt);
dbms_output.put_line('Discount amount is='||discount);
else
dbms_output.put_line('NO Discount on='||amt||'Discount only above 500');
end if;
end;
/
Case 3 - Sum of purchases of "the first" customer with that name
create or replace procedure discount_purchase(cust_name customer1.cust_name%type)
as
amt int;
discount int;
begin
select sum(purch_amt) into amt
from orders
where customer_id = (select min(customer_id) from customer1 where cust_name=cust_name);
dbms_output.put_line('Total amount is='||amt);
if(amt>=500) then
discount:=amt-(0.25*amt);
dbms_output.put_line('Discount amount is='||discount);
else
dbms_output.put_line('NO Discount on='||amt||'Discount only above 500');
end if;
end;
/
Case 4 - you choose, you implement
I can't tell you how, that is solely up to you and your design considerations.
Case 5 - Fix your data model
Again, I can't tell you how, that is solely up to you and your design considerations.

Related

Returning the nth largest value in postgresql

I want to return the nth largest salary in the table using for-loops, and I am still having trouble with the syntax. Please help.
create function get_nth_max(n integer)
returns real
as
$$
declare
nth_max real = max(salary) from company;
pay real;
begin
for pay in select salary from company
order by salary = desc limit n
loop
if pay.salary < nth_max then
nth_max = pay.salary and
end loop;
return nth_max
end;
$$
language plpgsql;
Error message:
ERROR: syntax error at or near "desc"
LINE 10: order by salary = desc limit n loop
^
SQL state: 42601
Character: 182
First you need to define "the nth largest salary in the table" clearly.
What about duplicates? If two people earn 1000 and one earns 800, is 800 then the 2nd or 3rd highest salary? Can salary be NULL? If so, ignore NULL values? What if there are not enough rows?
Assuming ...
Duplicate entries only count once - so 800 is considered 2nd in my example.
salary can be NULL. Ignore NULL values.
Return NULL or nothing (no row) if there are not enough rows.
Proper solution
SELECT salary
FROM (
SELECT salary, dense_rank() OVER (ORDER BY salary DESC NULLS LAST) AS drnk
FROM company
) sub
WHERE drnk = 8;
That's basically the same as Tim's answer, but NULLS LAST prevents NULL from coming first in descending order. Only needed if salary can be NULL, obviously, but never wrong. Best supported with an index using the same sort order DESC NULLS LAST. See:
Sort by column ASC, but NULL values first?
FOR loop as proof of concept
If you insist on using a FOR loop for training purposes, this would be the minimal correct form:
CREATE OR REPLACE FUNCTION get_nth_max(n integer, OUT nth_max numeric)
LANGUAGE plpgsql AS
$func$
BEGIN
FOR nth_max IN
SELECT DISTINCT salary
FROM company
ORDER BY salary DESC NULLS LAST
LIMIT n
LOOP
-- do nothing
END LOOP;
END
$func$;
Working with numeric instead of real, because we don't want to use a floating-point number for monetary values.
Your version does a lot of unnecessary work. You don't need the overall max, you don't need to check anything in the loop, just run through it, the last assignment will be the result. Still inefficient, but not as much.
Notably, SELECT get_nth_max(10) returns NULL if there are only 9 rows, while the above SQL query returns no row. A subtle difference that may be relevant. (You could devise a function with RETURNS SETOF numeric to return no row for no result ...)
Using an OUT parameter to shorten the syntax. See:
Returning from a function with OUT parameter
You don't need a UDF for this, just use DENSE_RANK:
WITH cte AS (
SELECT *, DENSE_RANK() OVER (ORDER BY salary DESC) drnk
FROM company
)
SELECT salary
FROM cte
WHERE drnk = <value of n here>
So I have missing semicolons, did not end my if-statement, and a 'dangling' AND. The following code is the working version:
create or replace function get_nth_max(n integer)
returns real
as
$$
declare
nth_max real = max(salary) from company;
pay record;
begin
for pay in select salary from company
order by salary desc limit n
loop
if pay.salary < nth_max then
nth_max = pay.salary;
end if;
end loop;
return nth_max;
end;
$$
language plpgsql;

PostgreSQL read commit on different transactions

I was running some tests to better understanding read commits for postgresql.
I have two transactions running in parallel:
-- transaction 1
begin;
select id from item order by id asc FETCH FIRST 500 ROWS ONLY;
select pg_sleep(10);
commit;
--transaction 2
begin;
select id from item order by id asc FETCH FIRST 500 ROWS ONLY;
commit;
The first transaction will select first 500 ids and then hold the id by sleeping 10s
The second transaction will in the mean while querying for first 500 rows in the table.
Based my understanding of read commits, first transaction will select 1 to 500 records and second transaction will select 501 to 1000 records.
But the actual result is that both two transactions select 1 to 500 records.
I will be really appreciated if someone can point out which part is wrong. Thanks
You are misinterpreting the meaning of read committed. It means that a transaction cannot see (select) updates that are not committed. Try the following:
create table read_create_test( id integer generated always as identity
, cola text
) ;
insert into read_create_test(cola)
select 'item-' || to_char(n,'fm000')
from generate_series(1,50) gs(n);
-- transaction 1
do $$
max_val integer;
begin
insert into read_create_test(cola)
select 'item2-' || to_char(n+100,'fm000')
from generate_series(1,50) gs(n);
select max(id)
into max_val
from read_create_test;
raise notice 'Transaction 1 Max id: %',max_val;
select pg_sleep(30); -- make sure 2nd transaction has time to start
commit;
end;
$$;
-- transaction 2 (run after transaction 1 begins but before it ends)
do $$
max_val integer;
begin
select max(id)
into max_val
from read_create_test;
raise notice 'Transaction 2 Max id: %',max_val;
end;
$$;
-- transaction 3 (run after transaction 1 ends)
do $$
max_val integer;
begin
select max(id)
into max_val
from read_create_test;
raise notice 'Transaction 3 Max id: %',max_val;
end;
$$;
Analyze the results keeping in mind that A transaction cannot see uncommitted DML.

Optimizing an insert/update loop in a stored procedure

I have two tables wholesaler_catalog and wholesaler_catalog_prices. The latter has a foreign key reference to the former.
wholesaler_catalog_prices has a column called cost_type which can be either RETAIL or DISCOUNT.
Consider row Foo in wholesaler_catalog. Foo has two entries in wholesaler_catalog_prices - one for RETAIL and one for DISCOUNT. I want to split up Foo into Foo1 and Foo2, such that Foo1 points to RETAIL and Foo2 points to DISCOUNT. (The reasons for doing this are complex which I won't go into - it's part of a larger migration)
I have made a stored procedure that looks like this:
do
$$
declare
f record;
new_id int;
begin
for f in select catalog_id from
(select catalog_id, cost_type, row_number() over (partition by catalog_id) from wholesaler_catalog_prices
group by catalog_id, cost_type
order by catalog_id) as x
where row_number > 1
loop
insert into wholesaler_catalog
(item_number, name, catalog_log_id)
select item_number, name, catalog_log_id from wholesaler_catalog
where id = f.catalog_id
returning id into new_id;
-- RAISE NOTICE '% copied to %', f.catalog_id, new_id;
update wholesaler_catalog_prices set catalog_id = new_id where catalog_id = f.catalog_id and cost_type = 'RETAIL';
end loop;
end;
$$
The problem is that there are about 100k such records and it takes a very long time to run (I cancelled the run after 30 minutes). Is there anyway I can optimize the procedure to run faster?

Declare a variable of temporary table in stored procedure in PL/pgSQL

I receive this error to begin with:
ERROR: syntax error at or near "conference"
LINE 19: FOR conference IN conferenceset
Here's the function:
CREATE OR REPLACE FUNCTION due_payments_to_suppliers_previous_month()
RETURNS TABLE(supplier varchar,due_amount numeric)
AS $$
DECLARE
BEGIN
CREATE TABLE conferenceset AS -- temporary table, so I can store the result set
SELECT
conference.conference_supplier_id,
conference.id AS conferenceid,
conference.price_per_person,
0 AS participants_count,
400 AS deduction_per_participant,
0 AS total_amount
FROM Conference WHERE --- date_start has to be from the month before
date_start >= date_trunc('month', current_date - interval '1' month)
AND
date_start < date_trunc('month', current_date);
FOR conference IN conferenceset
LOOP
---fill up the count_participants column for the conference
conference.participants_count :=
SELECT COUNT(*)
FROM participant_conference JOIN conferenceset
ON participant_conference.conference_id = conferenceset.conferenceid;
---calculate the total amount for that conference
conference.total_amount := somerec.participants_count*(conference.price_per_person-conference.deduction_per_participant);
END LOOP;
----we still don't have the name of the suppliers of these conferences
CREATE TABLE finalresultset AS -- temporary table again
SELECT conference_supplier.name, conferenceset.total_amount
FROM conferenceset JOIN conference_supplier
ON conferenceset.conference_supplier_id = conference_supplier.id
----we have conference records with their amounts and suppliers' names scattered all over this set
----return the result with the suppliers' names extracted and their total amounts calculated
FOR finalrecord IN (SELECT name,SUM(total_amount) AS amount FROM finalresultset GROUP BY name)
LOOP
supplier:=finalrecord.name;
due_amount:=finalrecord.amount;
RETURN NEXT;
END LOOP;
END; $$
LANGUAGE 'plpgsql';
I don't know how and where to declare the variables that I need for the two FOR loops that I have: conference as type conferenceset and finalrecord whose type I'm not even sure of.
I guess nested blocks will be needed as well. It's my first stored procedure and I need help.
Thank you.
CREATE OR REPLACE FUNCTION due_payments_to_suppliers_previous_month()
RETURNS TABLE(supplier varchar,due_amount numeric)
AS $$
DECLARE
conference record;
finalrecord record;
BEGIN
CREATE TABLE conferenceset AS -- temporary table, so I can store the result set
SELECT
conference.conference_supplier_id,
conference.id AS conferenceid,
conference.price_per_person,
0 AS participants_count,
400 AS deduction_per_participant,
0 AS total_amount
FROM Conference WHERE --- date_start has to be from the month before
date_start >= date_trunc('month', current_date - interval '1' month)
AND
date_start < date_trunc('month', current_date);
FOR conference IN (select * from conferenceset)
LOOP
---fill up the count_participants column for the conference
conference.participants_count = (
SELECT COUNT(*)
FROM participant_conference JOIN conferenceset
ON participant_conference.conference_id = conferenceset.conferenceid
);
---calculate the total amount for that conference
conference.total_amount = somerec.participants_count*(conference.price_per_person-conference.deduction_per_participant);
END LOOP;
----we still don't have the name of the suppliers of these conferences
CREATE TABLE finalresultset AS -- temporary table again
SELECT conference_supplier.name, conferenceset.total_amount
FROM conferenceset JOIN conference_supplier
ON conferenceset.conference_supplier_id = conference_supplier.id
----we have conference records with their amounts and suppliers' names scattered all over this set
----return the result with the suppliers' names extracted and their total amounts calculated
FOR finalrecord IN (SELECT name,SUM(total_amount) AS amount FROM finalresultset GROUP BY name)
LOOP
supplier = finalrecord.name;
due_amount = finalrecord.amount;
RETURN NEXT;
END LOOP;
END; $$
LANGUAGE 'plpgsql';

Using a for loop and if statement in postgres

How come this is not working? Basically, this proc will update columns in the main buyer table to check if the user has data in other tables.
DO language plpgsql $$
DECLARE
buyer integer;
BEGIN
FOR buyer IN SELECT id FROM buyers
LOOP
IF (SELECT count(*) FROM invoice WHERE buyer_id = buyer) > 0 THEN
UPDATE buyers SET has_invoice = true WHERE id = buyer;
ELSE
UPDATE buyers SET has_invoice = false WHERE id = buyer;
END IF;
END LOOP;
RETURN;
END;
$$;
It is unclear what is "not working". Either way, use this equivalent UPDATE statement instead:
UPDATE buyers b
SET has_invoice = EXISTS (SELECT 1 id FROM invoice WHERE buyer_id = b.id);
If you don't need redundant storage for performance, you can use a VIEW or generated column for the same purpose. Then the column has_invoice is calculated on the fly and always up to date. Instructions in this closely related answer:
Store common query as column?