Trigger function to UPDATE a column up on INSERT - sql

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

Related

Getting unexpected values for number of days between two dates in oracle

I am writing a SQL code which fetches two dates from the database and calculates the number of days between them. Here is the code:
create table borrower(
roll_no number,
date_of_issue date,
name_of_book varchar(20),
status varchar(10)
);
insert into borrower values(1,to_date('02-JAN-2022'),'dbms','issued');
insert into borrower values(2,to_date('10-JAN-2022'),'cns','issued');
insert into borrower values(3,to_date('17-JAN-2022'),'spos','issued');
insert into borrower values(4,to_date('26-JAN-2022'),'toc','issued');
create table fine(
roll_no number,
current_date date,
amount number
);
insert into fine values(1,to_date('14-FEB-2022'),null);
insert into fine values(2,to_date('14-FEB-2022'),null);
insert into fine values(3,to_date('14-FEB-2022'),null);
insert into fine values(4,to_date('14-FEB-2022'),null);
DECLARE
roll_counter number:=1;
initial_date date;
final_date date;
date_calc number;
BEGIN
loop
select date_of_issue into initial_date from borrower where roll_no=roll_counter;
select current_date into final_date from fine where roll_no=roll_counter;
date_calc:=final_date-initial_date;
dbms_output.put_line(date_calc);
roll_counter:=roll_counter+1;
exit when roll_counter>4;
end loop;
END;
/
drop table borrower;
drop table fine;
I am not getting any error, but instead getting unexpected values for the number of days. Here is the output:
Statement processed.
246.4165625
238.4165625
231.4165625
222.4165625
I was expecting the number of days between the two dates(check the table). Can someone help me sort this out.
CURRENT_DATE is an Oracle keyword that returns the current date. Name your column something that is not an Oracle keyword.
https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CURRENT_DATE.html
As #Matthew McPeak pointed out, CURRENT_DATE is a built-in function and that function is being called rather than returning your column value.
If you want the column value then you need to prefix the column name with the table name/alias and use fine.current_date:
DECLARE
roll_counter number:=1;
initial_date date;
final_date date;
date_calc number;
BEGIN
FOR roll_counter IN 1 .. 4 LOOP
select date_of_issue
into initial_date
from borrower
where roll_no=roll_counter;
select fine.current_date
into final_date
from fine
where roll_no=roll_counter;
date_calc:=final_date-initial_date;
dbms_output.put_line(date_calc);
END LOOP;
END;
/
Which, for your sample data, outputs:
43
35
28
19
Or you can use a single query (rather than multiple queries that are called in each loop iteration):
BEGIN
FOR r IN (
SELECT f.current_date - b.date_of_issue AS diff
FROM borrower b
FULL OUTER JOIN fine f
ON (b.roll_no = f.roll_no)
WHERE COALESCE(b.roll_no, f.roll_no) BETWEEN 1 AND 4
ORDER BY COALESCE(b.roll_no, f.roll_no)
) LOOP
dbms_output.put_line(r.diff);
END LOOP;
END;
/
db<>fiddle here

What are the different ways of adding a constraint so that only items that are available on the order date can be inserted?

order.date must be between item.date_from and item.date_to... what are the different ways of doing that?
CREATE TABLE "item" (
"id" SERIAL PRIMARY KEY,
"date_from" DATE NOT NULL,
"date_to" DATE NOT NULL
);
CREATE TABLE "order" (
"id" SERIAL PRIMARY KEY,
"date" DATE NOT NULL
);
CREATE TABLE "order_item" (
"order" INTEGER NOT NULL REFERENCES "order",
"item" INTEGER NOT NULL REFERENCES "item"
);
Check constraints work on simple expressions. For example, a simple sanity check on the order: check( date > '2010-01-01'). There's also exclusion constraints which check no two rows have the same value as defined by the exclusion. But, with the exception of foreign key constraints, constraints don't query other tables.
You can solve this with a trigger on insert and update, and I'll go into that below, but its better to solve this sort of problem with referential integrity. However, I can't think of a way to do that.
You can make a view of available items for the order. Here $1 is the date of the order.
create temporary view items_available_to_order
select *
-- pluralize table names to avoid conflicting with keywords and columns
from items
-- date_from and date_to has become a single daterange when_available
where items.when_available #> $1
Then only insert items from that view.
If you want to go the trigger route (you can do both) write a function which checks whether an order's item is valid. It either raises an exception or returns a trigger. new is the inserted row, or the row after an update.
I changed some of the table and column names and types to avoid common pitfalls.
create function check_item_order_is_valid()
returns trigger
language 'plpgsql'
as $body$
declare
item_is_available boolean;
begin
select
items.when_available #> orders.ordered_on into item_is_available
from item_orders
join items on items.id = new.order_id
join orders on orders.id = new.item_id;
if( not item_is_available) then
raise exception 'Item #% is not available for order #%',
new.item_id, new.order_id;
end if;
return new;
end
$body$
Then define a trigger to call the function when rows are inserted or updated in the item/order table.
create trigger check_item_orders
before insert or update
on item_orders
for each row
execute function check_item_order_is_valid();
Demonstration.
What if the valid range of an item changes? You need an update trigger on item to check that its orders are still valid. Maybe. Depends on your business logic.
A test example:
CREATE OR REPLACE FUNCTION public.item_date()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
order_date date;
from_date date;
to_date date;
BEGIN
select into order_date "date" from "order" where id = new.order;
select into from_date, to_date date_from, date_to from item where id = new.item;
--Use date range to test whether order date is in item date range.
if order_date <# daterange(from_date, to_date, '[]') then
return new;
else
return null;
end if;
END;
$function$
create trigger item_date_check before insert or update on order_item for each row execute function item_date();
insert into item values (1, '09/01/2021', '10/31/2021');
insert into item values (2, '07/01/2021', '08/31/2021');
insert into "order" values (1, '09/05/2021');
insert into order_item values (1, 1);
NOTICE: Order date 2021-09-05, from_date 2021-09-01, to_date 2021-10-31
INSERT 0 1
--Returning NULL causes the INSERT not to happen.
insert into order_item values (1, 2);
NOTICE: Order date 2021-09-05, from_date 2021-07-01, to_date 2021-08-31
INSERT 0 0
Note that I had to quote "order" as that is a reserved word also. You might to take a look at Key(reserved) Words. For range functions/operators see Range Function. For general information on range(s) see Range Types

Updating column with sequence numbers

I have a table for patients and this table has an empty column (varying character) and I want to fill it with sequence numbers and the sequence should start from 1 and end with the number of rows
I also have an existing column that takes holds the date when the row was created, I want the numbers that will be filled in the empty column to be ordered by giving 1 to the oldest date and so on.
You should just use ROW_NUMBER here, at the time you query, rather than updating your table:
SELECT *,
ROW_NUMBER() OVER (ORDER BY date_col DESC) rn
FROM yourTable;
The reason for not attempting an update here is that as soon as new data gets inserted into the table, you might be forced to run the update again, and that could get messy after a while.
I would use this for update:
Let's say your table is called test and columns are id(the one that is empty) and date_col(the column with date when the row was created).
And update your table with this statement:
update test t1
set id = t2.row_num
from (select ROW_NUMBER() OVER (ORDER BY date_col desc) row_num, date_col
from test) t2
where t1.date_col = t2.date_col;
Then to prepare everything for the future inserts I would create a function and a trigger. Function returns the next number that needs to be inserted in the id column and a trigger makes sure that that number will be inserted every next time the insert is made.
CREATE OR REPLACE FUNCTION sequence_test_up()
RETURNS "trigger" AS
$BODY$
BEGIN
New.id:= (select max(id)+1 from test);
Return NEW;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
CREATE TRIGGER Test_Tr
BEFORE INSERT
ON test
FOR EACH ROW
EXECUTE PROCEDURE sequence_test_up();
And then the next insert would insert ID with the value of the next number in line.
Here is the DEMO with step by step example.

Calculate average after insert in PostgreSQL

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

PLSQL condition statement in trigger involving 2 tables

I've two tables purchases and customers, I need to update visits_made (number) in customers if time of purchase ptime (date) in purchases table is different from the already existing last_visit (date) in customers table.
Here is the trigger that I'm trying and I'm doing something terribly and shamefully wrong.
create or replace trigger update_visits_made
after insert on purchases
for each row
declare new_date purchases.ptime%type;
begin
select ptime into new_date
from purchases
where purchases.ptime = :new.ptime;
if new_date = customers.last_visit then
new.last_visit=old.last_visit;
else
update customers
set visits_made=visits_made+1
where purchases.ptime=:new.ptime;
end if;
end;
/
show errors
Can anybody please tell me where I'm going wrong?
I get following errors
LINE/COL ERROR
10/15 PLS-00103: Encountered the symbol "=" when expecting one of the
following:
:= . ( # % ;
11/1 PLS-00103: Encountered the symbol "ELSE"
16/1 PLS-00103: Encountered the symbol "END"
This is a scalar assignment in PL/SQL:
new.last_visit = old.last_visit;
Not only should this be done with := but new and old should have colons before their names:
:new.last_visit := :old.last_visit;
Once you have fixed that problem, then the update will pose an issue:
update customers
set visits_made=visits_made+1
where purchases.ptime = :new.ptime;
It is unclear to me what this is supposed to be doing, so I can't suggest anything, other than pointing out that purchases is not defined.
I think somehow i get your requirement. Basically its a ticker which count the vists of user based on Login dates. I have written a snippet below which replicates the same scenario as mentioned Above. Let me know if this helps.
-- Table creation and insertion script
CREATE TABLE PURCHASES
(
P_ID NUMBER,
P_TIME DATE
);
INSERT INTO PURCHASES
SELECT LEVEL,SYSDATE+LEVEL FROM DUAL
CONNECT BY LEVEL < 10;
CREATE TABLE CUSTOMERS
(
P_ID NUMBER,
P_VISITS NUMBER
);
INSERT INTO CUSTOMERS
SELECT LEVEL,NULL FROM DUAL
CONNECT BY LEVEL < 10;
-- Table creation and insertion script
--Trigger Script
CREATE OR REPLACE TRIGGER update_purchase BEFORE
INSERT ON purchases FOR EACH row
DECLARE
new_date purchases.p_time%type;
BEGIN
BEGIN
SELECT A.P_TIME
INTO new_date
FROM
(SELECT p_time,
ROW_NUMBER() OVER(PARTITION BY P_ID ORDER BY P_TIME DESC) RNK
-- INTO new_date
FROM purchases
WHERE purchases.p_id = :new.p_id
)a
WHERE A.RNK =1;
EXCEPTION WHEN OTHERS THEN
RETURN;
END;
IF :NEW.P_TIME <> new_date THEN
UPDATE customers
SET P_VISITS =NVL(P_VISITS,0)+1
WHERE p_id=:new.p_id;
END IF;
END;
--Trigger Script
--Testing Script
INSERT INTO PURCHASES VALUES
(9,TO_DATE('12/11/2015','MM/DD/YYYY'));
--Testing Script