Calculate average after insert in PostgreSQL - sql

I'm trying to get the average of the field pm10_ug_m3 of all the values introduced in the last 24 hours by a sensor, but with my current code the average does not include the value of the row inserted.
Currently as the trigger is done before the insert, it is not taking into account the last value inserted in the field pm10_ug_m3. For this reason the average introduced in the field is 3 and not 6.5 obtained from (10+3)/2
1) Creation of table and addition of some dates:
(
ID bigint NOT NULL,
SensorID character(10),
pm10_ug_m3 numeric(10,2),
tense timestamp without time zone,
average float,
CONSTRAINT id_pk PRIMARY KEY (ID)
);
INSERT INTO sensor (ID,SensorID,pm10_ug_m3,tense) VALUES
(1,'S16',1,'2019-07-10 04:25:59'),
(2,'S20',3,'2017-07-10 02:25:59');
2) Creation of the trigger to calculate the average of pm10_ug_m3 of the records captured in the last 24h from the same sensor:
CREATE OR REPLACE FUNCTION calculate_avg() RETURNS TRIGGER AS $BODY$
BEGIN
NEW.average := ( SELECT AVG(pm10_ug_m3)
FROM sensor
WHERE SensorID=NEW.SensorID
AND tense>= NEW.tense - INTERVAL '24 HOURS');
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER calculate_avg_trigger BEFORE INSERT OR UPDATE
ON sensor FOR EACH ROW
EXECUTE PROCEDURE calculate_avg();
3) Insert of a new row, where it will be populated the field average:
INSERT INTO sensor (ID,SensorID,pm10_ug_m3,tense) VALUES
(3,'S20',10,'2017-07-10 04:25:59')

This does not work because the AVG() function only considers the data which is still inserted, not the new data which will be inserted.
Changing the trigger point from BEFORE to AFTER would deliver a correct result indeed, but it will not be set because the INSERT already has been done at this point.
So one way to achieve your result is to calculate the average manually in your trigger function:
SELECT (SUM(pm10_ug_M3) + NEW.pm10_ug_m3) / (COUNT(pm10_ug_m3) + 1)
FROM ...
SUM() of the current values + the new divided by the COUNT() of the current values + the new one.
demo:db<>fiddle

Related

Creating a trigger to automatically calculate the value of total tour time

I am facing some issue while creating a trigger that can automatically calculate the total time it takes for a tour to finish, below is the text question
The event holder has decided to record the shortest tour time for each event type for
all future exhibitions. Add two new attributes in the EVENTTYPE table named
eventype_record and eventtype_recordholder to store the shortest tour time for
each event type and the participant (participant number) who holds the record. You
may assume that only a single participant will hold each record and that the record
holder will only be replaced by a new single participant if their tour time is less
than the current eventtype_record. Calculate the tour time attribute added in previous task when the tour finish time is updated (ie. the participant finished the tour)
What I currently have is
CREATE OR REPLACE TRIGGER tour_time_updater
AFTER UPDATE OF tour_finishtime ON entry
FOR EACH ROW
BEGIN
UPDATE entry
SET tour_time = to_char(((:new.tour_finishtime - :old.tour_starttime) * 24 * 60), '9999.99');
END;
/
But after I try to insert a fake participant in my entry table with no finish time with this code
INSERT INTO entry (
event_id,
entry_no,
tour_starttime,
tour_finishtime,
part_no,
group_id,
tour_time
) VALUES (
9,
6,
to_date('09:05:43', 'HH:MI:SS'),
NULL,
5,
NULL,
NULL
);
and only update its tour_finishtime later
UPDATE entry SET tour_finishtime = to_date('10:05:43', 'HH:MI:SS')
where part_no = 5 and event_id = 9;
it is giving me errors like
**UPDATE entry SET tour_finishtime = to_date('10:05:43', 'HH:MI:SS')
where part_no = 5 and event_id = 9
Error report -
ORA-04091: table XXXXXXXX.ENTRY is mutating, trigger/function may not see it
ORA-06512: at "XXXXXXXX.TOUR_TIME_UPDATER", line 5
ORA-04088: error during execution of trigger 'XXXXXXXX.TOUR_TIME_UPDATER'**
Can anyone help me with that? Thank you in advance!
Don't update the table (literally), but
CREATE OR REPLACE TRIGGER tour_time_updater
BEFORE UPDATE OF tour_finishtime ON entry
FOR EACH ROW
BEGIN
:new.tour_time := to_char(((:new.tour_finishtime - :new.tour_starttime) * 24 * 60), '9999.99');
END;
/
Code you wrote is trying to update the same table whose modification fired the trigger; for the trigger, that table is "mutating" and that's an invalid condition.
You could "fix" it by writing a compound trigger (if your database version supports it) or using a package, but - why bother, if correct way to do it is as suggested in code above?
Don't use a TRIGGER, use a virtual column:
CREATE TABLE entry (
event_id NUMBER PRIMARY KEY,
entry_no NUMBER,
tour_starttime DATE,
tour_finishtime DATE,
part_no NUMBER,
group_id NUMBER
);
ALTER TABLE entry ADD tour_time INTERVAL DAY(0) TO SECOND (0)
GENERATED ALWAYS AS ((tour_finishtime - tour_starttime) DAY(0) TO SECOND(0));
or
ALTER TABLE entry ADD tour_time NUMBER(7,2)
GENERATED ALWAYS AS ((tour_finishtime - tour_starttime) *24 * 60);
If you must use a trigger (don't) then:
CREATE TABLE entry (
event_id NUMBER PRIMARY KEY,
entry_no NUMBER,
tour_starttime DATE,
tour_finishtime DATE,
part_no NUMBER,
group_id NUMBER,
tour_time INTERVAL DAY(0) TO SECOND(0)
);
CREATE OR REPLACE TRIGGER tour_time_updater
BEFORE INSERT OR UPDATE OF tour_finishtime ON entry
FOR EACH ROW
BEGIN
:NEW.tour_time := (:new.tour_finishtime - :new.tour_starttime) DAY(0) TO SECOND(0);
END;
/
db<>fiddle here

Why is my codes having error 00049 Bad Bind Variable

create or replace trigger addDate
before insert or update on Employee
for each row
begin
for EmployeeRec in
(
select * from employee
)
loop
if (EmployeeRec.DateLimit > sysdate) then
raise_application_error (-20001, 'You are only allowed to insert once a day, please try again
tomorrow');
end if;
end loop EmployeeRec;
:NEW.DateLimit := sysdate + 1;
end;
/
show errors;
Need to be able to insert a record for a day only. Any other records must wait till the next day
There is PLS 00049 ERROR at New.DateLimit
Maybe column name is not DateLimit, but Date_Limit.
If I am following you correctly, you want to allow just one record per day, as defined by column date_limit.
You don't need a trigger for this. One option uses a computed column and a unique constraint.
create table employee (
employee_id int primary key,
name varchar2(100),
date_limit date default sysdate,
day_limit date generated always as (trunc(date_limit)) unique
);
day_limit is a computed column, that contains the date portion of date_limit (with the time portion removed). A unique constraint is set on this column, so two rows cannot have date_limits that belong to the same day.
Demo on DB Fiddle

Trigger for before insert to add in x number of days to a date

I am trying to add x number of days to a variable within a table by deriving another date from the same table.
For example, in my BILLING table, it has 2 dates - BillDate and DueDate.
And so, I am trying to add a trigger before the insertion, such that it will takes in the BillDate and add 30 days to derive the DueDate.
While doing so, I got a bunch of errors, as follows:
dbfiddle
CREATE TABLE BILLING
(
BillDate DATE NOT NULL,
DueDate DATE NULL
);
-- Got ORA-24344: success with compilation error
CREATE TRIGGER duedate_trigger BEFORE INSERT ON BILLING
FOR EACH ROW
begin
set DueDate = :new.DueDate: + 30
end;
-- Got ORA-04098: trigger 'FIDDLE_FBHUOBXMWRPYBBXPIKTW.DUEDATE_TRIGGER' is invalid and failed re-validation
INSERT INTO BILLING
VALUES ((Date '2020-07-23'), NULL);
For the insertion, I have tried removing the NULL, but still I am getting a bunch of errors.
Any ideas?
Also, in the event, if the insertion statement also does includes in the due date too, will this affects the trigger? Trying to cater for 2 scenarios, generate a due date if not give, else if given, check if it is within 30 days from BillDate and update it... (likely I may have overthink/ overestimated that this is doable?)
CREATE OR REPLACE TRIGGER duedate_trigger BEFORE INSERT ON BILLING
FOR EACH ROW
begin
:new.DueDate := :new.BillDate + 30;
end;
INSERT INTO BILLING (BillDate ) values (sysdate);
CREATE OR REPLACE TRIGGER duedate_trigger
BEFORE INSERT ON billing
FOR EACH ROW
DECLARE
v_dueDate_derive NUMBER;
BEGIN
v_dueDate_derive = 30;
:new.DueDate = :new.BillDate + v_dueDate_derive;
END;
Days can be easily added by +, so it should not be the problem.
I believe there may be something wrong with INSERT itself.
Could you try to put INSERT like this?
INSERT INTO BILLING
VALUES (TO_DATE('2020-07-23'), NULL);

Trigger function to UPDATE a column up on INSERT

I would like to setup a trigger function for a postgresql table, which should update a column with the data derived from another column.
Table:
CREATE TABLE day (
symbol varchar(20) REFERENCES another_table(symbol),
date date,
open NUMERIC(8,2),
high NUMERIC(8,2),
low NUMERIC(8,2),
close NUMERIC(8,2),
sma8 NUMERIC(8,2),
PRIMARY KEY(symbol, date));
Note: composite primary key.
Sample INSERT:
INSERT INTO day VALUES('ABC', '2019-03-19', 102.3, 110.0, 125.3, 120.4, 0);
INSERT INTO day VALUES('ABC', '2019-03-20', 112.3, 109.0, 119.3, 118.4, 0);
INSERT INTO day VALUES('DEF', '2019-03-19', 1112.3, 1100.0, 1155.3, 1120.4, 0);
INSERT INTO day VALUES('DEF', '2019-03-20', 1202.3, 1180.0, 1205.3, 1190.4, 0);
and so on.
The following trigger function works fine when the 'date' column is the only primary key and the table contains data pertaining to one 'symbol' only (i.e the table contains data of one particular symbol on various unique dates).
create or replace function update_sma8() RETURNS TRIGGER AS
$$
BEGIN
UPDATE day d SET sma8 = s.simple_mov_avg
FROM
(
SELECT sec.date,AVG(sec.close)
OVER(ORDER BY sec.date ROWS BETWEEN 7 PRECEDING AND CURRENT ROW) AS
simple_mov_avg FROM day sec
)s where s.date = NEW.date --The newly inserted date
AND d.date = s.date;
RETURN NULL;
END $$ language plpgsql;
Refer: SQL trigger function to UPDATE daily moving average upon INSERT
I would like to update 'sma8' column with the value derived by averaging the current 'close' value and the last 7 'close' values of one particular symbol ('date' varies i.e past data.). Likewise for other symbols.
Kindly guide me. Thank you.
You need to know how to filter rows by "symbol".
Add WHERE clause to filter.
WHERE sec.symbol = NEW.symbol
And then, you register the trigger.
CREATE TRIGGER day_insert
AFTER INSERT ON day
FOR EACH ROW
EXECUTE PROCEDURE update_sma8();
Make sure that the "sma8" column will be updated when a row is inserted.
Here is full code.
DROP FUNCTION public.update_sma8();
CREATE FUNCTION public.update_sma8()
RETURNS trigger
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF
AS $BODY$
BEGIN
UPDATE day d SET sma8 = s.simple_mov_avg
FROM
(
SELECT sec.date,AVG(sec.close)
OVER(ORDER BY sec.date ROWS BETWEEN 7 PRECEDING AND CURRENT ROW) AS
simple_mov_avg FROM day sec WHERE symbol = NEW.symbol
)s where s.date = NEW.date --The newly inserted date
AND d.date = s.date ;
RETURN NULL;
END $BODY$;
ALTER FUNCTION public.update_sma8()
OWNER TO postgres;
You may add PARTITION BY symbol and then use it in the where clause to calculate for each symbol.
create or replace function update_sma8() RETURNS TRIGGER AS
$$
BEGIN
UPDATE day d SET sma8 = s.simple_mov_avg
FROM
(
SELECT sec.date,sec.symbol,AVG(sec.close)
OVER( partition by symbol ORDER BY sec.date ROWS BETWEEN
7 PRECEDING AND CURRENT ROW) AS
simple_mov_avg FROM day sec
)s where s.date = NEW.date --The newly inserted date
AND d.date = s.date
AND d.symbol = s.symbol;
RETURN NULL;
END $$ language plpgsql;
DEMO

SQL trigger not working

CREATE TABLE lab7.standings
(
team_name VARCHAR(100) NOT NULL PRIMARY KEY,
wins INTEGER,
losses INTEGER,
winPct NUMERIC,
CHECK(wins > 0),
CHECK(losses >0)
);
CREATE OR REPLACE FUNCTION
calc_winning_percentage()
RETURNS trigger AS $$
BEGIN
New.winPct := New.wins /(New.wins + New.losses);
RETURN NEW;
END;
$$LANGUAGE plpgsql;
CREATE TRIGGER
update_winning_percentage
AFTER INSERT OR UPDATE ON standings
FOR EACH ROW EXECUTE PROCEDURE calc_winning_percentage();
This is accurately updating the wins in my standings table, but doesn't seem to send my new calculated winning percentage.
Try this:
CREATE TRIGGER update_winning_percentage
BEFORE INSERT OR UPDATE ON standings
FOR EACH ROW EXECUTE PROCEDURE calc_winning_percentage();
In addition to changing the trigger to BEFORE like pointed out by #Grijesh:
I notice three things in your table definition:
1.integer vs. numeric
wins and losses are of type integer, but winPct is numeric.
Try the following:
SELECT 1 / 4, 2 / 4
Gives you 0 both times. The result is of type integer, fractional digits are truncated towards zero. This happens in your trigger function before the integer result is coerced to numeric in the assignment. Therefore, changes in wins and losses that only affect fractional digits are lost to the result. Fix this by:
.. either changing the column definition to numeric for all involved columns in the base table.
.. or changing the trigger function:
NEW.winPct := NEW.wins::numeric / (NEW.wins + NEW.losses);
Casting one of the numbers in the calculation to numeric (::numeric) forces the result to be numeric and preserves fractional digits.
I strongly suggest the second variant, since integer is obviously the right type for wins and losses. If your percentage doesn't have to be super-exact, I would also consider using a plain floating point type (real or double precision) for the percentage. Your trigger could then use:
NEW.winPct := NEW.wins::float8 / (NEW.wins + NEW.losses);
2.The unquoted column name winPct
It's cast to lower case and effectively just winpct. Be sure to read about identifiers in PostgreSQL.
3. Schema
Your table obviously lives in a non-standard schema: lab7.standings. Unless that is included in your search_path, the trigger creation has to use a schema-qualified name:
...
BEFORE INSERT OR UPDATE ON lab7.standings
...
P.S.
Goes to show the importance of posting your table definition with this kind of question.