How to create procedures with two different selects? - postgresql

I want to create a procedure that receives two ids, makes two selects in a table and returns the data, after that I want to perform an update in another table using the result that was returned to me earlier, how to do that?
This is an example of how it is at the moment
create or replace procedure transfer(
origin int,
destination int,
amount dec
)
language plpgsql
as $$
begin
select id as id_user_origin
from users
where id = origin
select id as id_user_destination
from users
where id = destination
-- subtracting the amount from the sender's account
update wallets
set balance = balance - amount
where id = id_user_origin;
-- adding the amount to the receiver's account
update wallets
set balance = balance + amount
where id = id_user_destination;
commit;
end;$$

You need to store results of the different selects into variables:
declare
id_user_origin int;
begin
select id into id_user_origin from users where id = origin ;
.....
update... ;

You can reduce the procedure to a single DML statement.
create or replace procedure transfer(
origin int
, destination int
, amount dec
)
language plpgsql
as $$
begin
update wallets
set balance = case when id = origin
then balance - amount
else balance + amount
end
where id in (origin, destination)
and exists (select null from wallets where id = destination)
and exists (select null from wallets where id = origin and balance >= amount);
commit;
end ;
$$;
The exists are not technically required, but guard against procedure receiving invalid parameters. See demo which includes a message where the update was not performed because of invalid user or insufficient balance. This, if included, would normally be written to a log table.

Related

How to optimize postgresql procedure

I have 61 million of non unique emails with statuses.
This emails need to deduplicate with logic by status.
I write stored procedure, but this procedure runs to long.
How I can optimize execution time of this procedure?
CREATE OR REPLACE FUNCTION public.load_oxy_emails() RETURNS boolean AS $$
DECLARE
row record;
rec record;
new_id int;
BEGIN
FOR row IN SELECT * FROM oxy_email ORDER BY id LOOP
SELECT * INTO rec FROM oxy_emails_clean WHERE email = row.email;
IF rec IS NOT NULL THEN
IF row.status = 3 THEN
UPDATE oxy_emails_clean SET status = 3 WHERE id = rec.id;
END IF;
ELSE
INSERT INTO oxy_emails_clean(id, email, status) VALUES(nextval('oxy_emails_clean_id_seq'), row.email, row.status);
SELECT currval('oxy_emails_clean_id_seq') INTO new_id;
INSERT INTO oxy_emails_clean_websites_relation(oxy_emails_clean_id, website_id) VALUES(new_id, row.website_id);
END IF;
END LOOP;
RETURN true;
END;
$$
LANGUAGE 'plpgsql';
How I can optimize execution time of this procedure?
Don't do it with a loop.
Doing a row-by-row processing (also known as "slow-by-slow") is almost always a lot slower then doing bulk changes where a single statement processes a lot of rows "in one go".
The change of the status can easily be done using a single statement:
update oxy_emails_clean oec
SET status = 3
from oxy_email oe
where oe.id = oec.id
and oe.status = 3;
The copying of the rows can be done using a chain of CTEs:
with to_copy as (
select *
from oxy_email
where status <> 3 --<< all those that have a different status
), clean_inserted as (
INSERT INTO oxy_emails_clean (id, email, status)
select nextval('oxy_emails_clean_id_seq'), email, status
from to_copy
returning id;
)
insert oxy_emails_clean_websites_relation (oxy_emails_clean_id, website_id)
select ci.id, tc.website_id
from clean_inserted ci
join to_copy tc on tc.id = ci.id;

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?

How to check if a user has data in certain tables

So the approach I'm taking is to create new boolean columns on the user table that if is set to true, then the table has data, if false the table is empty. Now I'm stuck because I don't know how to create the triggers, or more like the procedure that follows the trigger.
So my logic is...for each table have a trigger:
CREATE TRIGGER check_sales_trigger
AFTER INSERT OR DELETE
ON sales
FOR EACH ROW
EXECUTE PROCEDURE check_sales_table();
Then, create a procedure that updates the boolean column on the user table for each table. So basically I need help creating the procedure.
FYI, each client has his own db.
The functions (that's a plural) need to deal with a) new order for user, b) user id change for existing order and c) deleted order. Writing triggers isn't hard, it just needs some reading the manual. No ifs, no buts, no exceptions.
Since this is your first time, here's an example for the more complicated one (because it can lead to deadlocks if poorly written), to get you started:
create function check_sales_table__update() returns trigger as $$
begin
if new.user_id < old.user_id then
update users
set has_sales = true
where id = new.user_id;
update users
set has_sales = exists (select 1 from sales where user_id = old.user_id)
where id = old.user_id;
elsif old.user_id < new.user_id then
update users
set has_sales = exists (select 1 from sales where user_id = old.user_id)
where id = old.user_id;
update users
set has_sales = true
where id = new.user_id;
end if;
return null;
end;
$$ language plpgsql;
(The above assumes a not null field, of course.)

Postgres concurrency issue

I wrote the following trigger to guarantee that the field 'filesequence' on the insert receives always the maximum value + 1, for one stakeholder.
CREATE OR REPLACE FUNCTION update_filesequence()
RETURNS TRIGGER AS '
DECLARE
lastSequence file.filesequence%TYPE;
BEGIN
IF (NEW.filesequence IS NULL) THEN
PERFORM ''SELECT id FROM stakeholder WHERE id = NEW.stakeholder FOR UPDATE'';
SELECT max(filesequence) INTO lastSequence FROM file WHERE stakeholder = NEW.stakeholder;
IF (lastSequence IS NULL) THEN
lastSequence = 0;
END IF;
lastSequence = lastSequence + 1;
NEW.filesequence = lastSequence;
END IF;
RETURN NEW;
END;
' LANGUAGE 'plpgsql';
CREATE TRIGGER file_update_filesequence BEFORE INSERT
ON file FOR EACH ROW EXECUTE PROCEDURE
update_filesequence();
But I have repeated 'filesequence' on the database:
select id, filesequence, stakeholder from file where stakeholder=5273;
id filesequence stakeholder
6773 5 5273
6774 5 5273
By my undertanding, the SELECT... FOR UPDATE would LOCK two transactions on the same stakeholder, and then the second one would read the new 'filesequence'. But it is not working.
I made some tests on PgAdmin, executing the following:
BEGIN;
select id from stakeholder where id = 5273 FOR UPDATE;
And it realy LOCKED other records being inserted to the same stakeholder. Then it seems that the LOCK is working.
But when I run the application with concurrent uploads, I see then repeating.
Someone could help me in finding what is the issue with my trigger?
Thanks,
Douglas.
Your idea is right. To get an autoincrement based on another field (let's say it designate to a group) you cannot use a sequence, then you have to lock the rows of that group before incrementing it.
The logic of your trigger function does that. But you have a misunderstood about the PERFORM operation. It supposed to be put instead of the SELECT keyword, so it does not receive an string as parameter. It means that when you do:
PERFORM 'SELECT id FROM stakeholder WHERE id = NEW.stakeholder FOR UPDATE';
The PL/pgSQL is actually executing:
SELECT 'SELECT id FROM stakeholder WHERE id = NEW.stakeholder FOR UPDATE';
And ignoring the result.
What you have to do on this line is:
PERFORM id FROM stakeholder WHERE id = NEW.stakeholder FOR UPDATE;
That is it, only change this line and you are done.

Firebird 2.5 - add unique ID to each row in a table from stored procedure

I have a table which doesn't have an unique ID. I want to make a stored procedure which is adding to each row the number of the row as ID, but I don't know how to get the current row number. This is what I have done until now
CREATE OR ALTER PROCEDURE INSERTID_MYTABLE
returns (
cnt integer)
as
declare variable rnaml_count integer;
begin
/* Procedure Text */
Cnt = 1;
for select count(*) from MYTABLE r into:rnaml_count do
while (cnt <= rnaml_count) do
begin
update MYTABLE set id=:cnt
where :cnt = /*how should I get the rownumber here from select??*/
Cnt = Cnt + 1;
suspend;
end
end
I think better way will be:
Add new nullable column (let's call it ID).
Create a generator/sequence (let's call it GEN_ID).
Create a before update/insert trigger that fetches new value from sequence whenever the NEW.ID is null. Example.
Do update table set ID = ID. (This will populate the keys.)
Change the ID column to not null.
A bonus. The trigger can be left there, because it will generate the value in new inserted rows.