Using a for loop and if statement in postgres - postgresql

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?

Related

How to create procedures with two different selects?

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.

how to have a trigger work only for certain columns (they are chosen by their id) after updating

I have two tables.Tickets which has the columns: ticketid,startdate , enddate
and transactions which has the columns: transactionid, ticketid (fk to tickets), ticketcost.I want to create a trigger on tickets that makes a discount to ticketcost of transactions(of the table transactions) whenever the enddate of a ticket is updated.Multiple transactions might have the same ticket.
I was able to make a trigger that did what i described however not only at one ticket, the one that the date was changed, but every ticket of the tickets table.
first attempt:
create or replace function changeDate() returns trigger as $changeDate$
BEGIN
IF new.enddate != old.enddate THEN
update transactions
set ticketcost = ticketcost - ticketcost*0.1
from tickets
where tickets.ticketid= transactions.ticketid;
END IF;
return new;
END
$changeDate$ LANGUAGE plpgsql;
CREATE TRIGGER changeDate after UPDATE ON tickets
FOR EACH ROW EXECUTE FUNCTION changeDate();
This obviously failed because it is done for every row so every ticket is updated.
What i have now is this:
create or replace function changeDate() returns trigger as $changeDate$
Declare
arg1 integer;
BEGIN
IF new.enddate != old.enddate THEN
update transactions
set ticketcost = ticketcost - ticketcost*0.1
from tickets
where arg1 = transactions.ticketid;
END IF;
return new;
END
$changeDate$ LANGUAGE plpgsql;
CREATE TRIGGER changeDate after UPDATE ON tickets
FOR EACH ROW
WHEN (new.enddate != old.enddate)
EXECUTE FUNCTION changeDate(tickets.ticketid);
I have been trying to pass only the id of a ticket that has different new and old dates.The query works but nothing is changed.Basically i m trying to find a way to pass the id of the ticket that has had its enddate field changed.In the above example i m trying to pass it as a variable when the condition i described occurs.Any help would be appreciated as i cant really find a solution.
You can restrict the trigger to fire only when the date has been updated by specifying the column name and by ensuring the updated value is not the same as the old one:
CREATE TRIGGER changeDate after UPDATE OF enddate ON tickets
FOR EACH ROW
WHEN (OLD.enddateIS DISTINCT FROM NEW.enddate )
EXECUTE FUNCTION changeDate();
Inside the trigger function, you can refer to the NEW.ticketID directly
create or replace function changeDate() returns trigger as $changeDate$
BEGIN
update transactions
set ticketcost = ticketcost - ticketcost*0.1
where transactions.ticketid = NEW.ticketid;
return new;
END
$changeDate$ LANGUAGE plpgsql;
PS: since the function is not changing the date, a better name would be setDiscount

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.

Is there a similar function in postgresql for mysql's SQL_CALC_FOUND_ROWS?

everybody using mysql knows:
SELECT SQL_CALC_FOUND_ROWS ..... FROM table WHERE ... LIMIT 5, 10;
and right after run this :
SELECT FOUND_ROWS();
how do i do this in postrgesql? so far, i found only ways where i have to send the query twice...
No, there is not (at least not as of July 2007). I'm afraid you'll have to resort to:
BEGIN ISOLATION LEVEL SERIALIZABLE;
SELECT id, username, title, date FROM posts ORDER BY date DESC LIMIT 20;
SELECT count(id, username, title, date) AS total FROM posts;
END;
The isolation level needs to be SERIALIZABLE to ensure that the query does not see concurrent updates between the SELECT statements.
Another option you have, though, is to use a trigger to count rows as they're INSERTed or DELETEd. Suppose you have the following table:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
poster TEXT,
title TEXT,
time TIMESTAMPTZ DEFAULT now()
);
INSERT INTO posts (poster, title) VALUES ('Alice', 'Post 1');
INSERT INTO posts (poster, title) VALUES ('Bob', 'Post 2');
INSERT INTO posts (poster, title) VALUES ('Charlie', 'Post 3');
Then, perform the following to create a table called post_count that contains a running count of the number of rows in posts:
-- Don't let any new posts be added while we're setting up the counter.
BEGIN;
LOCK TABLE posts;
-- Create and initialize our post_count table.
SELECT count(*) INTO TABLE post_count FROM posts;
-- Create the trigger function.
CREATE FUNCTION post_added_or_removed() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
UPDATE post_count SET count = count - 1;
ELSIF TG_OP = 'INSERT' THEN
UPDATE post_count SET count = count + 1;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Call the trigger function any time a row is inserted.
CREATE TRIGGER post_added_or_removed_tgr
AFTER INSERT OR DELETE
ON posts
FOR EACH ROW
EXECUTE PROCEDURE post_added_or_removed();
COMMIT;
Note that this maintains a running count of all of the rows in posts. To keep a running count of certain rows, you'll have to tweak it:
SELECT count(*) INTO TABLE post_count FROM posts WHERE poster <> 'Bob';
CREATE FUNCTION post_added_or_removed() RETURNS TRIGGER AS $$
BEGIN
-- The IF statements are nested because OR does not short circuit.
IF TG_OP = 'DELETE' THEN
IF OLD.poster <> 'Bob' THEN
UPDATE post_count SET count = count - 1;
END IF;
ELSIF TG_OP = 'INSERT' THEN
IF NEW.poster <> 'Bob' THEN
UPDATE post_count SET count = count + 1;
END IF;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
There is a simple way, but keep in mind, that following COUNT(*) aggr function will be applied to all rows returned after where and before limit/offset (may be costy)
SELECT
id,
"count" (*) OVER () AS cnt
FROM
objects
WHERE
id > 2
OFFSET 50
LIMIT 5
No, PostgreSQL doesn't try to count all relevant results when you only need 10 results. You need a seperate COUNT to count all results.