Trigger keeps updating value in every row instead of particular one - sql

I am trying to calculate a sum for each particular order. I am using this trigger but it doesn't work properly, it updates every row with the same value instead of the only one with proper id.
done_services table
id
service_id
price
service table
id
name
payment table
id
sum
service_id
CREATE FUNCTION make_sum() RETURNS TRIGGER
AS $$
BEGIN
UPDATE payment
SET sum = (select sum(price) from done_services where service_id = new.service_id);
RETURN NULL;
END;$$ LANGUAGE plpgsql;
CREATE TRIGGER make_sum
AFTER INSERT ON basket FOR EACH ROW EXECUTE FUNCTION make_sum();
I used this command to enter an item
insert into done_services(id, service_id, price) values(uuid_generate_v4(), '76594d2f-7153-495f-9671-0ddaa331568c', 100);
But the sum changed for both rows instead of the only one with service id
Image

The immediate cause for the error message is the missing WHERE clause as instructed by Edouard. Plus, prevent expensive empty updates like:
UPDATE payment p
SET sum = ds.sum_price
FROM (
SELECT sum(d.price) AS sum_price
FROM done_services d
WHERE d.service_id = NEW.service_id
) ds
WHERE p.service_id = sum_price
AND p.sum IS DISTINCT FROM ds.sum_price;
In addition to fixing the prime error, this prevents empty updates that would not change the sum, but still write a new row version at full cost.
But the whole idea is questionable.
Keeping a sum from many rows up to date via trigger is expensive and error prone. Did you cover DELETE and INSERT accordingly? What about TRUNCATE? What about concurrent write access? Race conditions, deadlocks?
To get get the current sum for a set that can change dynamically, the superior solution is typically not to save that sum in the table at all. Use a VIEW or MATERIALIZED VIEW instead.
Or, to get the sum for a single or few payments, use a function:
CREATE OR REPLACE FUNCTION f_payment_sum(_service_id int)
RETURNS numeric
LANGUAGE sql STABLE PARALLEL SAFE AS
$func$
SELECT sum(d.price)
FROM done_services d
WHERE d.service_id = _service_id;
$func$
Related:
Updating a row based on a value from another table?

just missing something in your UPDATE statement :
UPDATE payment
SET sum = (select sum(price) from done_services where service_id = new.service_id)
WHERE service_id = new.service_id ;
Next time please create a dbfiddle with your data model, sample of data and queries.

Related

Create a trigger after insert that update another table

I'm trying to create a trigger that update another table after an insert, when the state of the swab test changes from positive to negative.
I have created this trigger, but the problem is that every time there is a user with a negative swab, the user id is copied to the table, even if this user has never been positive. Maybe, have I to compare date?
Create or replace trigger trigger_healed
After insert on swab_test
For each row
Begin
if :new.result = 'Negative' then
UPDATE illness_update
SET illness_update.state = 'healed'
WHERE illness_update.id_user = :new.id_user;
end if;
end;
This is the result that I'm trying to get.
SWAB_TEST
id_user id_swab swab_result date
1 test1 'positive' May-01-2020
1 test1 'negative' May-08-2020
2 test2 'negative' May-02-2020
ILLNESS_UPDATE
id_user state date
1 'healed' May-08-2020
What you ask for would require the trigger to look at the existing rows in the table that is being inserted on - which by default cannot be done, since a trigger cannot action the table it fires upon.
Instead of trying to work around that, I would suggest simply creating a view to generate the result that you want. This gives you an always up-to-date perspective at your data without any maintenance cost:
create view illness_update_view(id_user, state, date) as
select id_user, 'healed', date
from (
select
s.*,
lag(swab_result) over(partition by id_user order by date) lag_swab_result
from swab_test s
) s
where lag_swab_result = 'positive' and swab_result = 'negative'
The view uses window function lag() to recover the "previous" result of each row (per user). Rows that represents transitions from a positive to a negative result are retained.
As #GMB indicates you cannot do what you are asking with a standard before/after row trigger as it that cannot reference swab_test as that is the table causing the trigger to fire (that would result in an ORA-04091 mutating table error). But you can do this in a Compound Trigger (or an After statement). But before getting to that I think your data model has a fatal flaw.
You have established the capability for multiple swab tests. A logical extension for this being that each id_swab tests for a different condition, or a different test for the same condition. However, the test (id_swab) is not in your illness update table. This means if any test goes to negative result after having a prior positive result the user is healed from ALL tests. To correct this you need to a include id_swab id making the healed determination. Since GMB offers the best solution I'll expand upon that. First drop the table Illness_update. Then create Illness_update as a view. (NOTE: in answer to your question you DO NOT need a trigger for the view, everything necessary is in the swab_test; see lag windowed function.
create view illness_update(id_user, state, swab_date) as
select id_user, id_swab, 'healed' state,swab_date
from (
select
s.*
, lag(swab_result) over(partition by id_user, id_swab
order by id_user, id_swab, swab_date) as lag_swab_result
from swab_test s
) s
where lag_swab_result = 'positive'
and swab_result = 'negative';
Now, as mentioned above, if your assignment requires the use of a trigger then see fiddle. Note: I do not use date (or any data type) as a column name. Here I use swab_date in all instances.

Correct usage of Postgres JSON conversion functions

I am working on a Postgres update transaction.
Let's say I have two tables: events and ticket_books with event booking types. The ticket_books table has a foreign key pointing to the events.
I need to update an event stored in the database, including booking type records from the ticket_books table.
To deal with cascading update and delete, I decided to build a transaction, in a "pseudo-code" it looks like:
BEGIN;
DELETE FROM
ticket_books
WHERE
event_id = ${req.params.id} AND
id NOT IN (${bookingIds})
FOR booking IN json_to_recordset('${JSON.stringify(book)}') as book(id int, title varchar(200), price int, ...) LOOP
IF bookind.id THEN
UPDATE
ticket_books
SET
title = booking.title, price = booking.price
WHERE
event_id = ${req.params.id};
ELSE
INSERT INTO
ticket_books (title, price, qty_available, qty_per_sale)
VALUES
(booking.title, booking.price, booking.qty_available, booking.qty_per_sale)
RETURNING
id
END IF;
END LOOP;
UPDATE
event
SET
...
WHERE
id = ...
RETURNING
id;
COMMIT;
I currently get the error: syntax error at or near "json_to_recordset". I never used json_to_recordset or friends before, just saw from the document that from 9.3 and later those are available. Unsure how to get Postgres to understand what I need, though.
I am embedding a JSON array so the final line looks like:
FOR booking IN json_to_record('[{"id":13,"description":"Three day access to the festival","title":"Three Day General Admission","price":260,"qty_available":5000,"qty_per_sale":10},{"id":14,"description":"Single day access to the festival","title":"Single Day General Admission","price":"90.90","qty_available":2000,"qty_per_sale":2},{"title":"Free Admission","price":"0.00","qty_available":0,"qty_per_sale":0}]')
I believe that my JSON array is valid. Apparently, this is not how I should be passing it to the Postgres. What should I be doing instead? My goal is to iterate over the array entries. If there is an integer value for booking.id, I want to update the record, else insert a new one.
You need a query, and a standalone function call usually does not count as a query:
FOR booking IN select * from json_to_recordset(...
Also, you can't use BEGIN to start a transaction in plpgsql. It is only used to start a block. If you are using a procedure rather than a function, then you can COMMIT but then a new transaction starts immediately with no BEGIN token being used.
You are also missing a semicolon between the DELETE and the FOR, but from the error message that seems to be missing from only your post, and not from your actual code.

Explicitly set ROWNUM in column

I'm trying to split what was a large table update into multiple inserts into working tables. One of the queries needs uses the row number in it. On an INSERT in oracle, can I explicitly add the ROWNUM as an explicit column? This is a working table ultimately used in a reporting operation with a nasty partion over clause and having a true row number is helpful.
create table MY_TABLE(KEY number,SOMEVAL varchar2(30),EXPLICIT_ROW_NUMBER NUMBER);
INSERT /*+PARALLEL(AUTO) */ INTO MY_TABLE(KEY,SOMEVAL,EXPLICIT_ROW_NUMBER) (
SELECT /*+PARALLEL(AUTO) */ KEY,SOMEVAL,ROWNUM
FROM PREVIOUS_VERSION_OF_MY_TABLE
);
where PREVIOUS_VERSION_OF_MY_TABLE has both a KEY and SOMEVAL fields.
I'd like it to number the rows in the order that the inner select statement does it. So, the first row in the select, had it been explicitly run, would have a ROWNUM of 1, etc. I don't want it reversed, etc.
The table above has over 80MM records. Originally I used an UPDATE, and when I ran it, I got some ORA error saying that I ran out of UNDO space. I do not have the exact error message at this point anymore.
I'm trying to accomplish the same thing with multiple working tables that I would have done with one or more updates. Apparently it is either hard, impossible, etc to add UNDO space, for this query (our company DB team says), without making me a DBA, or spending about $100 on a hard drive and attaching it to the instance. So I need to write a harder query to get around this limitation. The goal is to have a session id and timestamps within that session, but for each timestamp within a session (except the last timestamp), show the next session. The original query is included below:
update sc_hub_session_activity schat
set session_time_stamp_rank = (
select /*+parallel(AUTO) */ order_number
from (
select /*+parallel(AUTO) */ schat_all.explicit_row_number as explicit_row_number,row_number() over (partition by schat_all.session_key order by schat_all.session_key,schat_all.time_stamp) as order_number
from sc_hub_session_activity schat_all
where schat_all.session_key=schat.session_key
) schat_all_group
where schat.explicit_row_number = schat_all_group.explicit_row_number
);
commit;
update sc_hub_session_activity schat
set session_next_time_stamp = (
select /*+parallel(AUTO) */ time_stamp
from sc_hub_session_activity schat2
where (schat2.session_time_stamp_rank = schat.session_time_stamp_rank+1) and (schat2.session_key = schat.session_key)
);
commit;

SQL trigger function to UPDATE daily moving average upon INSERT

I am trying to create a SQL trigger function which should UPDATE a column when data is INSERTed INTO the table. The update is based on the values present in the values being INSERTed.
I have the following table to store daily OHLC data of a stock.
CREATE TABLE daily_ohlc (
cdate date,
open numeric(8,2),
high numeric(8,2),
low numeric(8,2),
close numeric(8,2),
sma8 numeric(8,2)
);
INSERT command:
INSERT INTO daily_ohlc (cdate, open, high, low, close)
values ('SYMBOL', 101, 110, 95, 108);
When this command is executed I would like to update the 'sma8' column based on the present values being INSERTed and the values already available in the table.
As of now, I am using the following SQL query to calculate the values for every row and then use the result to update the 'sma8' column using python.
SELECT sec.date, AVG(sec.close)
OVER(ORDER BY sec.date ROWS BETWEEN 7 PRECEDING AND CURRENT ROW) AS
simple_mov_avg FROM daily_ohlc sec;
The above query calculates Simple Moving Average over the last 8 records (including the present row).
Using this procedure I update every row of data in the 'sma8' column every time I insert data. I would like to update only the last row (i.e row being INSERTed) by using a trigger. How to do this?
You may do an UPDATE FROM your select query using appropriate joins in your Trigger.
create or replace function update_sma8() RETURNS TRIGGER AS
$$
BEGIN
UPDATE daily_ohlc d SET sma8 = s.simple_mov_avg
FROM
(
SELECT sec.cdate,AVG(sec.close)
OVER(ORDER BY sec.cdate ROWS BETWEEN 7 PRECEDING AND CURRENT ROW) AS
simple_mov_avg FROM daily_ohlc sec
)s where s.cdate = NEW.cdate --The newly inserted cdate
AND d.cdate = s.cdate;
RETURN NULL;
END $$ language plpgsql;
Demo
The only caveat of using this method is that if someone deletes a row or updates close column, then the values have to be recalculated, which won't happen for existing rows. Only the inserted row will see the right re-calculated value.
Instead, you may simply create View to calculate the sma8 column from the main table for all rows when requested.
Can't you just do something along those lines?
INSERT INTO daily_ohlc
SELECT current_date, 101, 110, 95, 108, (COUNT(*)*AVG(close)+108)/(1+Count(*))
FROM daily_ohlc
WHERE cDate >= ANY (
SELECT MIN(cdate)
FROM (SELECT CDate, ROW_NUMBER() OVER (ORDER BY CDate DESC) as RowNum FROM daily_ohlc) a
WHERE RowNum <= 7
)
I know very well it could appear complicated compared to a trigger.
However, I am trying to avoid a case where you successfully create the ON INSERT trigger and next want to handle updates in the table. Updating a table within a procedure triggered by an update in the same table is not the best idea.

how to make an update trigger query in pl sql

I have 2 tables (1) product_warehouse and (2) Return_Vendor Invoice.
i have to update the quantity in product_warehouse by trigger according to the value of Return_Vendor Invoice Table.
where Item_code is unique key in both tables.
For example if the product_warehouse contain 3 quantities , and the shopkeeper returns 1 quantity to vendor then it should be 2 in the Product_warehouse. update query will also acceptable.
create or replace trigger tiuda_return_vendor after insert or update or delete
on return_vendor is
begin
update product_wherehouse
set
quantity = quantity - (:new.quantity - nvl(:old.quantity, 0)
where
item_code = nvl(:new.item_code, :old.item_code);
end;
This trigger works in most cases. You can insert update or delete the return line.
Only thing is: when updating, you cannot update the item_code itself, because the update statement doesn't take that into account. You could easily solve that, but I don't know if it's in your requirements. I usually don't change values like that, but rather remove the item and add a new line for a different item.
Updating the quantity works fine. If you update the quantity, the difference between old and new is calculated and that difference is used to modify the stock quantity in the wherehouse.
create or replace
TRIGGER "WR_RETURN_INVOICE_UPDATE_TRG"
AFTER UPDATE ON RETURN_INVOICE
FOR EACH ROW
BEGIN
UPDATE PRODUCT_WAREHOUSE
SET QUANTITY=QUANTITY-:OLD.QUANTITY
WHERE ITEM_CODE=:OLD.ITEM_CODE;
END WR_RETURN_INVOICE_UPDATE_TRG;
Try this one it will works.