How to use trigger to update column for newly inserted tuple - sql

I have a table BillRecords
bill_id buy_amt sell_amt profit
----------------------------------------------------
0 200 300 NULL
1 1000 1200 NULL
Let's say I want to insert following record
INSERT INTO BillRecords(bill_id, buy_amt, sell_amt)
VALUES (2, 2000, 2500)
I'm expecting that as soon as I insert record, trigger should only update profit column for newly inserted record (i.e. WHERE bill_id = 2) and not for all the rows in profit column. What should I do?
Note: I'm using SQL Server

No need for a trigger to do what you want. It is much simpler, and more efficient, to use a computed column:
create table billrecords (
bill_id int primary key,
buy_amt int,
sell_amt int,
profit as (buy_amt - sell_amt)
);
Or if you want to add it to an already-existing table:
alter table billrecords
add profit as (buy_amt - sell_amt);
This gives you an always up-to-date value, that you can access just like any other column. For better performance, you can use option persisted, so the value column is physically stored, and recomputed only when needed instead of every time it is accessed.

Since you only want to update the profit amount of the transaction having bill_id = 2, you can use the below after trigger this in combination with a derived column "profit" which fetches the profit amount.
CREATE TRIGGER trigger_name
ON billrecords
After INSERT
AS
IF EXISTS (SELECT * from inserted)
begin
update m
set m.profit = m.sell_amt - m.bill_amt, m.bill_amt = null, m.sell_amt = null
from billrecords m join inserted i
on i.bill_id = m.bill_id
where i.bill_id = 2
end;

Related

The subquery returned more than one value. TSQL

CREATE TABLE sales
(id INT PRIMARY KEY IDENTITY, name VARCHAR(30),
percent_part FLOAT, sales FLOAT, sum_bonus DECIMAL);
CREATE TRIGGER TRcointingBonus ON sales
AFTER UPDATE
AS BEGIN
DECLARE #sum_bonus FLOAT;
SELECT #sum_bonus = (SELECT ((sales / 100) * percent_part) FROM sales);
UPDATE sales SET sum_bonus = #sum_bonus
END;
INSERT INTO sales VALUES('staff1', 7.0, 7088, 1);
INSERT INTO sales VALUES('staff2', 3.5, 20590, 1);
INSERT INTO sales VALUES('staff3', 10.5, 6089, 1);
UPDATE sales SET sales = 7088 WHERE id=1;
I create a table and a trigger, and for each UPATE operation, there will be calculations in each row of sum_bonus.
The problem is in
DECLARE #sum_bonus FLOAT;
SELECT #sum_bonus = (SELECT ((sales / 100) * percent_part) FROM sales);
If remove FROM sales then writing to the variable will not be possible. Can explain what is the reason and how to solve this problem without abandoning the variable?
Your actual error is very clear, your subquery returns more than one value. If you take your sample data, and run your subquery on it's own:
SELECT ((sales / 100) * percent_part)
FROM sales
You get 3 values:
(No column name)
496.16
720.65
639.345
You are then trying to assign this to a single decimal variable:
DECLARE #sum_bonus FLOAT;
SELECT #sum_bonus = (SELECT ((sales / 100) * percent_part) FROM sales);
So SQL Server has no idea which of the 3 values you are expecting to be stored, so throws an error - clearly telling you that the subquery can only return one row to avoid ambiguity.
To avoid the error change your subquery to return one row.
With that being said your trigger is massively flawed, it updates the entire table every time as it has no reference to either the inserted or deleted memory resident pseudo tables, and as above the value it is trying to update the entire table with doesn't really make sense either.
What you are trying to achieve can be done very simply with a Computed Column, so your create table becomes something like:
CREATE TABLE sales
(
id INT PRIMARY KEY IDENTITY,
name VARCHAR(30),
percent_part FLOAT,
sales FLOAT,
sum_bonus AS (sales * percent_part / 100)
);
There's no need to store sum_bonus and use any kind of code to update it when underlying values change, just store the expression and calculate it as and when you need it.
Example on db<>fiddle
ADDENDUM
To answer a question you have asked in the comments as to why this works:
UPDATE sales SET sum_bonus = (SELECT (sales / 100) * procent)
But adding FROM sales breaks the query - It is because adding FROM Sales fundamentally changes the query from just superfluous use of parenthesis and SELECT, to a subquery. The former is equivalent to simply:
UPDATE sales SET sum_bonus = sales / 100 * percent
i.e. sales and percent are references to columns from the instance of the sales being updated. By adding FROM sales you introduce a further instance of the sales table, and the two columns then become references to that. With no link to the instance being updated you end up with multiple rows in the subquery, and hence your error.

Record should only be loaded to target on a scenario

I have two tables a stage table and a target table. I want my target table to hold valid CustomerScore values. Currently, we insert into staging and load to our target table. We do not want to load invalid values(-8.0000). However, if there is a customerNumber with a valid value in our target table we would like to decommission numbers by giving it a customerScore of (-8.0000). This should be the only time this value makes it into the target table, so a record for that CustomerNumber has to already be in the target for this to update that record currently in the target table. My create statement is below
CREATE TABLE stg.CustomerAppreciation (
CustomerId INT identity(1, 1)
,CustomerNumber VARCHAR(50)
,CustomerScore DECIMAL(5, 4)
);
CREATE TABLE ods.CustomerAppreciation (
CustomerId INT identity(1, 1)
,CustomerNumber VARCHAR(50)
,CustomerScore DECIMAL(5, 4)
);
Currently, my target table has two records, each value below belongs to my create table fields.
1 123 0.8468
2 143 1.0342
Now say we want to decommission CustomerID = 2 because there is a record been inserted into staging as
3 143 -8.0000
The target table should now be updated on this CustomerNumber. Making my target table look like:
1 123 0.8468
2 143 -8.0000
This should be the only time we allow -8.0000 into the table when a CustomerNumber already exists. If a customerNumber does not exists in the target table and for some reason -8.0000 is seen in staging it should not be allowed in. How would I write an update query that updates a record in my target table only if that scenario exists and prevents -8.0000 from coming in if it does not exist?
Assuming the staging table only contains one row per customer number (if not, group it to show the highest customer Id), you can use a merge to perform this function. Without checking exact syntax, something like this:
MERGE ods.CustomerAppreciation AS Target
USING (SELECT * FROM stg.CustomerAppreciation WHERE CustomerScore >= 0) AS Source ON Target.CustomerNumber = Source.CustomerNumber
WHEN MATCHED
-- choose your match criteria here
--AND Source.CustomerId > Target.CustomerId
AND NOT EXISTS (SELECT Target.* INTERSECT SELECT Source.*)
THEN UPDATE
SET Target.CustomerScore = Source.CustomerScore;
Not sure if I fully understand the specifics but here is some syntax that should help to at least get you started ...
BEGIN;
MERGE ods.CustomerAppreciation AS X
USING (SELECT CustomerNumber,CustomerScore FROM stg.CustomerAppreciation) AS Y (CustomerNumber,CustomerScore)
ON (X.CustomerNumber = Y.CustomerNumber)
WHEN MATCHED /*AND Y.CustomerNumber = '-8.0000'*/ THEN
UPDATE SET CustomerScore = Y.CustomerScore
WHEN NOT MATCHED BY X /*AND Y.CustomerNumber = '-8.0000'*/ THEN
INSERT (CustomerNumber,CustomerScore)
VALUES (Y.CustomerNumber,Y.CustomerScore)
OUTPUT $action, inserted.* INTO #MyTempTable;
END;

Creating a trigger that replaces null values in an insert with values already present in the table in SQL Server

I have a table, known as Fruit_Veg_Product_Table which is used to contain the characteristics of certain fruit and vegetable stock.
The table has the following columns:
Product_ID
Product_Type
Product_Name
Product_Colour
Product_Price
Product_In_Sale
Product_Stock_Level
Product_Height
Product_Width
Product_Depth
Product_Package_Height
Product_Package_Width
Product_Package_Depth
When a new product is inserted into the table, sometimes the product is inserted without any dimensions (the columns from Product_Height all the way to Product_Package_Depth). In this circumstance, the dimensions are entered as NULL.
I am in need of a SQL Server trigger that will replace all the NULL values from the attempted insert with the values corresponding to products that are already stored in the table which share a common Product_Type with the product that is being entered.
Any help with this problem is greatly appreciated.
Triggers have an INSERTED logical table that can be used to join the inserted row data back to the physical table. Here is an example:
CREATE TRIGGER Fruit_Veg_Product_Table_Trg
ON dbo.Fruit_Veg_Product_Table
FOR INSERT
AS
UPDATE dbo.Fruit_Veg_Product_Table
SET Product_Package_Height = ca.Product_Package_Height,
Product_Package_Width = ca.Product_Package_Width,
Product_Package_Depth = ca.Product_Package_Depth
FROM dbo.Fruit_Veg_Product_Table
CROSS APPLY
(
SELECT TOP 1
Product_Package_Height,
Product_Package_Width,
Product_Package_Depth
FROM dbo.Fruit_Veg_Product_Table AS fvpt
WHERE dbo.Fruit_Veg_Product_Table.Product_Type = fvpt.Product_Type
AND Product_Package_Height IS NOT NULL
AND Product_Package_Width IS NOT NULL
AND Product_Package_Depth IS NOT NULL
) AS ca
WHERE EXISTS
(
SELECT *
FROM INSERTED
WHERE INSERTED.Product_ID = dbo.Fruit_Veg_Product_Table.Product_ID
AND INSERTED.Product_Package_Height IS NULL
AND INSERTED.Product_Package_Width IS NULL
AND INSERTED.Product_Package_Depth IS NULL
);
GO

Inserting a Row in a Table from Select Query

I'm using two Stored Procedures and two Table in My Sql Server.
First Table Structure.
Second Table Structure.
When a Customer books a Order then the Data will be Inserted in Table 1.
I'm using a Select Query in another Page Which Selects the Details from the Second Table.
If a row with a billno from first table is not Present in Second Table I want to Insert into the Second Table with some Default Values in the Select Query. How can I do this
??
If you want to insert in the same query, you will have to create a stored procedure. There you'll query if row exists in second table, and, if not, insert a new entity in second table.
Your code should look something like this:
-- --------------------------------------------------------------------------------
-- Routine DDL
-- Note: comments before and after the routine body will not be stored by the server
-- --------------------------------------------------------------------------------
DELIMITER $$
CREATE DEFINER=`table`#`%` PROCEDURE `insertBill`(IN billNo int, val1 int, val2 int, val3 int)
BEGIN
DECLARE totalres INT DEFAULT 0;
select count(*) from SECOND_TABLE where Bill_Number = billNo INTO totalres;
IF totalres < 1 THEN
INSERT into SECOND_TABLE values(val1,val2,val3);
END IF;
END
Val1,val2 and val3 are the valuest to be inserted into second table.
Hope this helps.
What you do is to LEFT JOIN the two tables and then select only the ones where the second table had no row to join, meaning the bill number were missing.
In the example below, you can replace #default_inform_status and #default_response_status with your default values.
INSERT INTO second_table (Bill_Number, Rest_Inform_Status, Rest_Response_Status)
SELECT ft.Bill_Number, #default_inform_status, #default_response_status
FROM first_table ft
LEFT JOIN second_table st
ON st.Bill_Number = ft.Bill_number
WHERE st.Bill_Number IS NULL
If it is possible to have duplicates of the same Bill_Number in the first table, you should also add a DISTINCT after the SELECT. But considering the fact that it is a primary key, this is no issue for you.

How to keep track of historical changes to reference table?

This is a very common thing in web applications. If I have a user table and I want to keep track of all the changes made to the user table, I can use a database insert and update triggers to save those changes in the user_history table.
But what if I have user_products table, where I have user_id , product_id, cost. When I add a user in the system lets say I have two products associated with that user. So my user_products table will have two rows for that user.
user_id product_id cost
1 10 1000
2 20 2000
Now if I go to the edit user page and delete product 1 , add product 3 , change cost for product 2 from 2000 to 3000.
so normally I delete all records for user_id 1 from user_product table and then do a new insert for the new products.
So its not a regular update but a delete and then insert. Hence I am not able to keep track of history changes.
Ideally I would want to know that I deleted product 1 , added product 3 , changed cost for product 2 from 2000 to 3000.
EDIT 1:-
I am not doing a update. I am doing a delete and then insert. So I am deleting record with product id 2 and cost 2000. And then again inserting record with prod id 2 but with cost 3000. So technically its delete and insert but logically only cost is changed from 2000 to 3000. If i check while executing both queries it will say i deleted product with id 2 and and then added product with id 2 which are same. But I want to be able to see that the cost has chnaged from 2000 to 3000
One option would be to create a user_product_history table that is populated by triggers on user_product and then define a trigger that transforms the old 'delete' row in the history table into an update if the row is subsequently inserted.
CREATE TABLE user_product_history (
user_id number,
product_id number,
cost number,
operation_type varchar2(1),
operation_date date
);
CREATE TRIGGER trg_user_product_history
AFTER INSERT OR UPDATE OR DELETE ON user_product
FOR EACH ROW
DECLARE
l_cnt integer;
BEGIN
IF( deleting )
THEN
insert into user_product_history( user_id, product_id, cost, operation_type, operation_date )
values( :old.user_id, :old.product_id, :old.cost, 'D', sysdate );
ELSIF( updating )
THEN
insert into user_product_history( user_id, product_id, cost, operation_type, operation_date )
values( :new.user_id, :new.product_id, :new.cost, 'U', sysdate );
ELSIF( inserting )
THEN
select count(*)
into l_cnt
from user_product_history
where operation_type = 'D'
and user_id = :new.user_id
and product_id = :new.product_id;
if( l_cnt > 0 )
then
update user_product_history
set operation_type = 'U',
operation_date = sysdate,
cost = :new.cost
where operation_type = 'D'
and user_id = :new.user_id
and product_id = :new.product_id;
else
insert into user_product_history( user_id, product_id, cost, operation_type, operation_date )
values( :new.user_id, :new.product_id, :new.cost, 'I', sysdate );
end if;
END IF;
END;
From an efficiency standpoint, however, doing deletes and inserts rather than updates is going to mean that you're putting far more load on your database than is necessary. You'll do substantially more I/O than necessary. And you'll end up with much more complicated code for handling changes. You'll almost certainly be better served figuring out what has changed and then just updating those rows.
I don't know if there is a "standard" way or simple one but for my experience, you have to do it yourself...
Of course, you don't need to code a specific method for each table but a generic method that handles all update SQL, something smart according your tables structure.
Example:
You can define a history table that will have:
id - table name - table record id - list of fields to update - list of values updated BEFORE - list of values updated AFTER
Then, into your code, you should have an abstract class that will handle the SQL queries: on update, you call the method that will parse the SQL query and insert a record into this table, even you have to query a SELECT to retrieve the data BEFORE the updates (the overhead is minimum because the SELECT uses only few resources and you can use the DB server cache).
Finally, when you display the information relative to a record, let's say the user, you can add the code to display the history on this table AND for this user id.
Hope it will help you.