Writing a SQL Server trigger - error - sql-server

I have three tables:
Bus_Driver (drNo,drName,salary,StationNo,experience)
Station (StationNo,address,district,Salary_Commission)
Cleaner (Cleaner_No, Cname, StationNo)
The question is to write a trigger. It states that if a bus driver's salary is increased by 20% more than the original salary then 0.05% of the increased value will be transferred to his Station as Salary commission.
I managed to write the trigger halfway, but got stuck when I have to transfer the amount to the other table.
My code is:
CREATE TRIGGER tr_1
ON Bus_Driver
AFTER INSERT
AS
BEGIN
DECLARE #salary MONEY
SET #Salary = 0
SELECT #Salary= salary
FROM Inserted
WHERE #Salary > (120 / 100 * #Salary)
Can anyone help me how to write the next steps please

The trigger you wrote is wrong.
First, it's a trigger for insert, while the question states that the salary is raised, meaning it should be a trigger for update.
Second, your trigger assumes only a single row will be in the inserted table. However, this assumption is wrong. Triggers in SQL server are fired per statement, not per row, meaning that the inserted (and deleted) tables might contain zero, one, or many rows.
A solution for this question will be to write a trigger for update, that will, in turn, update the station table. Something like this:
CREATE TRIGGER tr_Bus_Driver_Update ON Bus_Driver
FOR UPDATE
AS
UPDATE s
SET Salary_Commission = Salary_Commission -
(0.0005 * D.Salary) + -- Remove old salary of the driver(s) from salary_commition.
(0.0005 * I.Salary) -- add new salary of the driver(s) to salary_commition
FROM Station s
INNER JOIN Inserted I ON s.StationNo = I.StationNo
INNER JOIN Deleted D ON I.drNo = D.drNo -- assuming drNo is unique in Bus_Driver table
WHERE I.Salary >= D.Salady * 1.2 -- You might need to cast to a floating point data type if the Salary is an integer data type
Note you might need to cast the salary to a floating point data type if the Salary is an integer data type whenever it's used in this trigger.

You need to stop and start again from scratch.
First of all, you need to catch the AFTER UPDATE event - not the insert - since you want to do something when the salary is updated (an existing value is replaced with a higher one).
Secondly, the trigger will be called once per UPDATE statement and if that UPDATE affects more than one row, the Deleted and Inserted pseudo tables will contain multiple rows of data - so your SELECT #Salary = salary FROM Inserted statement is doomed - it will fetch one arbitrary row and ignore all others that might be also affected.
In an UPDATE case, Inserted will have the new values (after the update), while Deleted has the old values (before the update) - so the difference between these two pseudo tables can be used to figure out if the salary increase was more than 20%:
CREATE TRIGGER trBusDriverSalaryIncrease
ON dbo.Bus_Driver
AFTER UPDATE
AS
BEGIN
-- declare a table variable to hold all revelant values
DECLARE #RelevantIncreases TABLE (drNo INT, StationNo INT, SalaryIncrease DECIMAL(18,2))
-- find those bus drivers who have had a more than 20% increase in their salary
INSERT INTO #relevantIncreases (drNo, StationNo, SalaryIncrease)
SELECT
i.drNo, i.StationNo, -- Driver and Station No
(i.Salary - d.Salary) -- Salary increase in absolute numbers
FROM
Deleted d
INNER JOIN
Inserted i ON d.drNo = i.drNo
WHERE
-- Salary increased by more than 20%
i.Salary > 1.2 * d.Salary
-- now that we have all the relevant bus drivers and their salary increase
-- insert this into the Station.Salary_Commission column
UPDATE s
SET Salary_Commission = s.Salary_Commission + ri.SalaryIncrease * 0.0005
FROM dbo.Station s
INNER JOIN #RelevantIncreases ri ON ri.StationNo = s.StationNo
END

Related

Create an Alert in SQL if a field in a table decreases (Seq number Control Table)

I have a table where we store our Sequence numbers for jobs and Invoices.
Earlier this week one of the Sequence numbers reset from 100000 odd back to 1.
I would like to create some sort of alert that would inform me if the Sequence number decreases at anytime so that I can proactively resolve the issue then and there.
Table looks like this:
Comp_id - This will always be the same eg. COMP
Seq_type - Name of the Sequence i.e Invoicing
u_version - always '#'
Seq_no - Sequence number that increments when a new invoice is created.
Seq_desc - Definition of the seq_type
SP_str1 - NULL
SP_num1 - NULL
I Need the alert to inform me when SEQ_TYPE decreases its value at anytime.
I am open any suggestions.
I would use a trigger for the alert:
Create trigger trg_u_Sequence on [Sequence]
for update
as
begin
select [existing].Seq_type as OLDSeq_type, [existing].Seq_no as OLDSeq_no, [new].Seq_type as NEWSeq_type, [new].Seq_no as NEWSeq_no
into #temp
from inserted [new]
inner join deleted [existing]
on [new].seq_type = [existing].seq_type
where [new].Seq_no <= [existing].seq_type
if ##ROWCOUNT > 0
begin
--Alert using sp_send_dbmail or write to a table with the contents of #temp
end
end

Archiving data from Table1 to Table2 upon condition | Sql Server 2017 Express

I have a database with two tables:
Table Student with the following
columns:
StudentID int identity,
StudentFN,
StudentLN,
Active bit,
EnrollmentDate
Table ArchivedStudent with the following columns:
ArvchivedStudentID int identity,
StudentID int,
StudentFN,
StudentLN,
WithdrawalDate getdate(),
ReasonDropped
In the long run, I'd like to schedule automatic updates for the table AcrchivedStudent and move the data from columns StudentID, StudentFN and StudentLN from table Student to table ArchnivedStudent when column Active changes from 1 (true) to 0 (false).
Here's my start up script that is not working:
update [as]
set [as].StudentID = s.StudentID,
[as].StudentFN = s.StudentFN,
[as].StudentLN = s.StudentLN
from ArchivedStudent [as]
inner join Student s
on [as].StudentID = s.StudentID
where s.Active = 0
go
The issue is that it does not return any results.
Once I'll be able to update table ArchivedStudent, I'd like to delete data of the students whose Active status changed to 0 in the Student table.
Your question still isn't very clear on the process. For example, do you want to allow the student to be deactivated for a certain period of time before they are moved to the archive table or do you want the student to be immediately moved to the archived table once the student is deactivated?
If the latter, this is much easier:
INSERT INTO ArchivedStudent (StudentId, StudentFn, StudentLn, WithdrawalDate)
SELECT S.StudentId, S.StudentFn, S.StudentLn, GETDATE()
FROM Student S
WHERE StudentId = ?
DELETE FROM Student WHERE StudentId = ?
If the former, then that is more challenging and we will require more detail.
Update 1:
To set the Withdrawal date based off a calculated value, use the following:
INSERT INTO ArchivedStudent (StudentId, StudentFn, StudentLn, WithdrawalDate)
SELECT S.StudentId, S.StudentFn, S.StudentLn, CAST(DATEADD(D,14,GETDATE()) AS DATE)
FROM Student S
WHERE StudentId = ?
Note 1: In DATEADD(), use a positive value for future dates and use a negative value for past dates. You can remove the DATE CAST if you need the actual time in addition to the date.
Note 2: The DELETE script posted in the original answer still stands.
You need a trigger to do it :
CREATE TRIGGER ArchiveStudent ON Student
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO ArchivedStudent (StudentID, StudentFN, StudentLN)
SELECT
StudentID
, StudentFN
, StudentLN
FROM
Student
WHERE
Active = 0
DELETE FROM Student
WHERE
Active = 0
END
However, your approach it's simple, and risky at the same time. For instance, if someone made a student inactive by mistake, then the trigger will immediately insert that student into archive table then deleted. Surely you can retrieve it by many ways such as deleted, inserted tables or even get the max id of archive table, but why you put yourself in this situation in the first place?. This is one of many general issues could be experienced by the current approach. A better approach is to actually add more versioning or historian methods for the tables, and make the archives run either from SQL Job or a store procedure on a fixed dates rather than triggers. this would give you a scheduled and controlled data archiving.
You can even add a historian columns which will store the value of active column and the date of the change. Then, use trigger or store procedure to do it for you (or even a computed column with a generic scalar function that will be reused on multiple tables). for instance, if the student is inactive for 5 business days, then archive it and delete it from the table.
You could use a TRIGGER AFTER UPDATE on the Student table.
This trigger would:
- react only on UPDATE,
- transfer Student to ArchiveStudent, when Active is set to 0
- and set WithdrawalDate to 2 weeks from today.
Trigger:
CREATE TRIGGER [Student_Changed]
ON Student
AFTER UPDATE
AS
BEGIN
-- Archive
INSERT INTO ArchiveStudent
(StudentID, StudentFN, StudentLN, WithdrawalDate)
SELECT
DELETED.StudentID
,DELETED.[StudentFN]
,DELETED.[StudentLN]
,DATEADD(day, 14, GETDATE()) -- 2 Weeks from today
FROM DELETED
WHERE DELETED.Active = 1
-- Delete archived
DELETE FROM Student
WHERE StudentID = (SELECT DELETED.[StudentID] FROM DELETED)
AND Active = 0
END;
DEMO:
You can take a look at the SQL Fiddle solution here.
There are a number of solutions here that all appear partially correct, but with some issues. Your initial update of your archive table will not insert into the archive table, only update an existing row. And since you are trying to join between the live table and the archive table, you will get no results - well, no updates since an update statement doesn't produce "results" as such anyway.
So as other have said you would use two statements - one an insert statement and one a delete. I would tend to be on the careful side and make sure that I a) dont get duplicates in my archive table and b) dont delete from live before I am sure its made it into the archive. So the two statements would be:
insert archivestudent(...fieldlist...)
select * from student
where active=0
and not exists(select * from archivestudent where archivestudent.studentid=student.studentid)
delete student
where active=0
and exists(select * from archivestudent where archivedstudent.studentid=student.studentid)
You can then run this code whenever you wish, schedule it as a job to run each night, whatever makes sense in your app.
If, on the other hand you want to immediately run then a trigger is the way to go. Be aware though that triggers are set-based operations, meaning that the trigger runs once for all rows affected by an update. This means that the solution proposed by #Milan will fail if the triggering update affects more than one row, because the clause WHERE StudentID = (SELECT DELETED.[StudentID] FROM DELETED) will return more one value. An example might be update student set active=0 where enrolmentdate<'2017-01-01'
You should always join to the internal tables exposed inside a trigger, in this case the DELETED table
delete student
from deleted
join student on student.studentid=deleted.studentid
where active=0
I'd still be tempted to add the where exists/not exists clauses inside the trigger as well just to make it more error-proof.
You need two queries:
Insert
Insert into archivedstudent (studentid, student, studentln) select studentid, studentfn, studentln from student where active=0 and studentid not in (select studentid from archivedstudent);
And the delete
Delete from student where studentid in (select studentid from archivedstudent);
What you need is a trigger.SQL Server Trigger After Update for a Specific Value However you should be careful with triggers on large amounts of data, they can hurt performance.

How to store existing column value in new table using trigger

I have two tables
Customer
CustomerUpdate
Structure of both tables are like this
Customer table's structure
CustomerName | CustomerId
CustomerUpdate table's structure
NewCustomerName | NewCustomerId | OldCustomerName
I have few values inserted in the Customer table. Whenever I should update the data in this table I want that the existing as well as new data should be triggered into new table CustomerUpdate.
For this I created a trigger but this is only pulling the updated data, it's not pulling the existing data..
CREATE TRIGGER trgAfterUpdate
ON [dbo].Customer
FOR UPDATE
AS
SET NOCOUNT ON
declare #NewCustomerName nchar(20);
declare #NewCustomerId nchar(20);
declare #OldCustomerName nchar(20);
declare #audit_action varchar(100);
select #NewCustomerName = i.CustomerName from inserted i;
select #NewCustomerId = i.CustomerId from inserted i;
select #OldCustomerName = c.CustomerName
from Customer c
where CustomerId = #NewCustomerId;
if update(CustomerName)
set #audit_action='Updated Record -- After Update Trigger.';
if update(CustomerId)
set #audit_action='Updated Record -- After Update Trigger.';
insert into CustomerUpdate(NewCustomerName, NewCustomerId, OldCustomername)
values(#NewCustomerName, #NewCustomerId, #OldCustomerName);
PRINT 'AFTER UPDATE Trigger fired.'
GO
Please help me out
First, selecting from the table being modified when an update trigger is executing will get the new value. These are AFTER triggers (rather than INSTEAD triggers) and therefore the update has already happened by the time the trigger fires (although it can be rolled back). If you need the old value, you should select from the DELETED pseudo-table.
Second, as pointed out by #marc_s in comments, your trigger has the hidden assumption that only one row is affected by each update. This may very well be a valid assumption for your environment, if your application only ever updates one row at a time, but in the general case, every trigger should be ready to handle the case where many rows are affected by a single update. Writing your triggers to handle multiple rows is good practice.
Third, all of your sequentially executing code is pretty much unnecessary. The old value and the new value can be retrieved and inserted all at once:
CREATE TRIGGER trgAfterUpdate
ON [dbo].Customer
FOR UPDATE
AS
BEGIN
SET NOCOUNT ON
insert into CustomerUpdate(NewCustomerName, NewCustomerId, OldCustomername)
-- case 1: ID unchanged
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
JOIN Deleted D on I.CustomerID=D.CustomerID
UNION ALL
-- case 2: ID changed, Name unchanged
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
JOIN Deleted D on I.CustomerName=D.CustomerName
WHERE I.CustomerID<>D.CustomerID
UNION ALL
--case 3: ID changed, Name changed
SELECT I.CustomerName, I.CustomerID, D.CustomerName
FROM Inserted I
LEFT JOIN Deleted D on I.CustomerID=D.CustomerID OR I.CustomerName=D.CustomerName
WHERE D.CustomerID IS NULL;
END

target table `RECEIPT` of the DML statement cannot have any enabled triggers if the statement contains an OUTPUT clause without INTO clause

I have a table called INVOICE which stores bill information about an order/orders, one of the columns in this table is a column named paid which is a type of bit. As its name indicates, this column indicates whether the specific order/orders bill is paid or not.
I have another table named RECEIPT, this table stores information about any payment processes for a specific invoice.
So every time user pay an amount for the specified invoice, a new receipt record is created.
Now What I'm trying to do is to create a trigger that updates the paid column in the INVOICE table and set it to 1. This update process should be triggered in case of that the sum of receipts that belong to the invoice is equal to the amount_due in the INVOICE table.
In other words, if invoice due amount= 100$
and the user paid 50$
then, late he paid the other 50$
The paid column in the INVOICE table should be set to 1 as the total payments are equal to the invoice due amount
This is the trigger I've created to achieve the above
CREATE TRIGGER tg_invoice_payment ON RECEIPT
AFTER INSERT
AS
BEGIN
UPDATE INVOICE
SET paid = 1
WHERE INVOICE.invoice_id = (SELECT inserted.invoice_id FROM inserted)
AND (SELECT SUM(RECEIPT.amount_paid)
FROM RECEIPT
JOIN inserted ON RECEIPT.receipt_id = inserted.receipt_id
WHERE RECEIPT.invoice_id = inserted.invoice_id) = (SELECT INVOICE.amount_due
FROM INVOICE
JOIN inserted ON INVOICE.invoice_id = inserted.invoice_id
WHERE INVOICE.invoice_id = inserted.invoice_id)
END;
it compiled successfully but at run time I've get the below error:
The target table 'RECEIPT' of the DML statement cannot have any enabled triggers if the statement contains an OUTPUT clause without INTO clause
I think personally that you should update the paid status outside the scope of triggers. If you perform an INSERT into RECEIPT, you can execute the UPDATE INVOICE ... statement right after that (inside a TRANSACTION of course). A lot cleaner and predictable that way.
As to the error you are getting it's hard to say what is causing that based on the information you gave us. Perhaps the TRIGGER is triggering other TRIGGERs that produce the error you are getting? The statement you provided simply doesn't have an OUTPUT statement.
In any case, the statement you provided is not written correctly (as Damien pointed out) because the inserted table can have multipe rows. This is a rewrite to correct at least that part:
CREATE TRIGGER tg_invoice_payment ON RECEIPT
AFTER INSERT
AS
BEGIN
UPDATE
INVOICE
SET
paid = 1
FROM
inserted AS ins
INNER JOIN INVOICE AS inv ON
inv.invoice_id=ins.invoice_id
WHERE
inv.amount_due=(
SELECT
SUM(r.amount_paid)
FROM
RECEIPT AS r
WHERE
r.receipt_id=ins.invoice_id
);
END;
But as I mentioned earlier you probably not be doing this from a TRIGGER. Execute this statement from your program right after any INSERT/UPDATE. Alternatively, write a Stored Procedure for inserting into RECEIPT and execute the UPDATE statement right after the INSERT.

SQL Server triggers to check credit card balance

I've been trying to find a solution to a very simple problem, but I just cant find out how to do it. I have two tables Transactions and Credit_Card.
Transactions
transid (PK), ccid (FK: to credit_card > ccid), amount, timestamp
Credit_Card
ccid (PK), Balance, creditlimit
I want to create a trigger so before someone inserts a transaction it checks that the transaction amount + the balance of the credit card does not go over the creditlimit and if it is, it rejects the insert.
"EDIT" The following code fixed my issue, big thanks to Dan Guzman for his contribution!
CREATE TRIGGER TR_transactions
ON transactions FOR INSERT, UPDATE
AS
IF EXISTS(
SELECT 1
FROM (
SELECT t.ccid, SUM(t.amount) AS amount
FROM inserted AS t
GROUP BY t.ccid) AS t
JOIN Credit_Card AS cc ON
cc.ccid = t.ccid
WHERE cc.creditlimit <= (t.amount + cc.balance)
)
BEGIN
RAISERROR('Credit limit exceeded', 16, 1);
ROLLBACK;
END;
If I understand correctly, you just need to check the credit limit against newly inserted/updated transactions. Keep in mind that a SQL Server trigger fires once per statement and a statement may affect multiple rows. The virtual inserted will have images of the affected rows. You can use this to limit the credit check to only the credit cards affected by the related transactions.
CREATE TRIGGER TR_transactions
ON transactions FOR INSERT, UPDATE
AS
IF EXISTS(
SELECT 1
FROM (
SELECT inserted.ccid, SUM(inserted.amount) AS amount
FROM inserted
GROUP BY inserted.ccid) AS t
JOIN Credit_Card AS cc ON
cc.ccid = t.ccid
WHERE cc.creditlimit <= (t.amount + cc.balance)
)
BEGIN
RAISERROR('Credit limit exceeded', 16, 1);
ROLLBACK;
END;
EDIT
I removed the t alias from the inserted table and qualified the columns with inserted instead to better indicate the source of the data. It's generally a good practice to qualify column names with the table name or alias in multi-table queries to avoid ambiguity.
The integers 16 and 1 in the RAISERROR statement specify the severity and state of the raised error. See the SQL Server Books Online reference for details. Severity 11 and greater raise an error, with severities in the 11 through 16 range indicating a user-correctable error.
you can try this.
ALTER trigger [dbo].[TrigerOnInsertPonches]
On [dbo].[CHECKINOUT]
After Insert
As
BEGIN
DECLARE #ccid int
,#amount money
you have to tell sql that the trigger is ofter insert,
then you can declare the using variable. declare
select #ccid=o.ccid from inserted o;
I think is the correct whey to catch the id.
then you can make the select filtiering from this value.
I hope this can be usefull

Resources