Create trigger to only allow updates for certain employees - sql-server

We have a database with one table for all of our Employee information, (name, pay, etc). One of the columns in the table is "commission." I am trying to write a trigger that will only allow the "commission" column be updated or inserted into if it is for a Sales Representative. If an update is tried on any other employee then it should print out an error. I would like this trigger to also print all of the information from each update, failed or not, to a separate table. What is the best way for me to go about doing this?
I am relatively new to SQL Server so any help here would be greatly appreciated!
Thanks,
EDIT:
Here is what i have so far:
CREATE TRIGGER CommissionUpdate ON Employees
FOR UPDATE
AS IF UPDATE(Commission)
Declare
#Old_Comm money
, #New_Comm money
, #EmpID int
Select #EmpID = (Select EmployeeID From Deleted)
Select #Old_Comm = (Select Commission From Deleted)
Select #New_Comm = (Select Commission From Inserted)
BEGIN
INSERT INTO ChangeLog (EmpID, [User], [Date], OldComm, NewComm)
VALUES (#EmpID, User_Name(), GetDate(), #Old_Comm, #New_Comm)
END
Basically all this does is add entries to the ChangeLog table when the commission table is updated. Im still having trouble adding the constraints to only allow "commission" to be updated for Sales Reps.

Related

What is wrong with my trigger? No results are inserted

The trigger below select ID's from one table (employeeInOut), sums int's in a column in that table matching all ID's, and is supposed to insert these in another table (monthlyHours). I can't figure out if this is a syntax problem (nothing shows up in intellisense), and all it says is trigger executed successfully - nothing is inserted.
Trigger ->
GO
CREATE TRIGGER empTotalsHoursWorked
ON employeeInOut
FOR INSERT, DELETE, UPDATE
AS
BEGIN
INSERT INTO monthlyHours(employeeID, monthlyHours)
SELECT (SELECT employeeID FROM employeeInOut),
SUM(dailyHours) AS monthlyHours
FROM employeeInOut
WHERE employeeInOut.employeeID=(SELECT employeeID FROM monthlyHours)
END
GO
I have re-worked this trigger many times and this is the one with no errors, however nothing is inserted, and results seem to be nothing. Any advice, answers please appreciated.
Going with a couple of assumptions here one being that monthlyHours table contains employeeID and monthlyhours.
With that being said I think you are going to need multiple triggers depending on the action. Below is an example based on insert into the employeeInOut table
GO
CREATE TRIGGER empTotalsHoursWorked
ON employeeInOut
AFTER INSERT
AS
BEGIN
DECLARE #employeeID INT
DECLARE #monthlyHours INT
SELECT #employeeID = INSERTED.employeeID
FROM INSERTED
SELECT #monthlyHours = SUM(dailyHours)
FROM employeeInOut
WHERE employeeInOut.employeeID = #employeeID
INSERT INTO monthlyHours(employeeID,monthlyHours)
values (#employeeID, #monthlyHours)
END
GO
This will insert a new row of course. You may want to modify this to update the row if the row already exists in the monthlyHours table for that employee.
I would really advise against a trigger for a simple running total like this, your best option would be to create a view. Something like:
CREATE VIEW dbo.MonthlyHours
AS
SELECT EmployeeID,
monthlyHours = SUM(dailyHours)
FROM dbo.employeeInOut
GROUP BY EmployeeID;
GO
Then you can access it in the same way as your table:
SELECT *
FROM dbo.MonthlyHours;
If you are particularly worried about performance, then you can always index the view:
CREATE VIEW dbo.MonthlyHours
WITH SCHEMABINDING
AS
SELECT EmployeeID,
monthlyHours = SUM(dailyHours),
RecordCount = COUNT_BIG(*)
FROM dbo.employeeInOut
GROUP BY EmployeeID;
GO
CREATE UNIQUE CLUSTERED INDEX UQ_MonthlyHours__EmployeeID ON dbo.MonthlyHours(EmployeeID);
Now whenever you add or remove records from employeeInOut SQL Server will automatically update the clustered index for the view, you just need to use the WITH (NOEXPAND) query hint to ensure that you aren't running the query behind the view:
SELECT *
FROM dbo.MonthlyHours WITH (NOEXPAND);
Finally, based on the fact the table is called monthly hours, I am guessing it should be by month, as such I assume you also have a date field in employeeInOut, in which case your view might be more like:
CREATE VIEW dbo.MonthlyHours
WITH SCHEMABINDING
AS
SELECT EmployeeID,
FirstDayOfMonth = DATEADD(MONTH, DATEDIFF(MONTH, 0, [YourDateField]), 0),
monthlyHours = SUM(dailyHours),
RecordCount = COUNT_BIG(*)
FROM dbo.employeeInOut
GROUP BY EmployeeID, DATEADD(MONTH, DATEDIFF(MONTH, 0, [YourDateField]), 0);
GO
CREATE UNIQUE CLUSTERED INDEX UQ_MonthlyHours__EmployeeID_FirstDayOfMonth
ON dbo.MonthlyHours(EmployeeID, FirstDayOfMonth);
And you can use the view in the same way described above.
ADDENDUM
For what it is worth, for your trigger to work properly you need to consider all cases:
Inserting a record where that employee already exists in MonthlyHours (Update existing).
Inserting a record where that employee does not exist in MonthlyHours (insert new).
Updating a record (update existing)
Deleting a record (update existing, or delete)
To handle all of these cases you can use MERGE:
CREATE TRIGGER empTotalsHoursWorked
ON employeeInOut
FOR INSERT, DELETE, UPDATE
AS
BEGIN
WITH ChangesToMake AS
( SELECT EmployeeID, SUM(dailyHours) AS MonthlyHours
FROM ( SELECT EmployeeID, dailyHours
FROM Inserted
UNION ALL
SELECT EmployeeID, -dailyHours
FROM deleted
) AS t
GROUP BY EmployeeID
)
MERGE INTO monthlyHours AS m
USING ChangesToMake AS c
ON c.EmployeeID = m.EmployeeID
WHEN MATCHED THEN UPDATE
SET MonthlyHours = c.MonthlyHours
WHEN NOT MATCHED BY TARGET THEN
INSERT (EmployeeID, MonthlyHours)
VALUES (c.EmployeeID, c.MonthlyHours)
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
END
GO

Dynamic INSERT script in SQL

Is there a way to create dynamic INSERT scripts from a stored procedure in SQL?
I was asked to create an archive functionality where it is supposed to select a date range and create INSERT statements of all the rows of that range and navigate through all the foreign keys generating cascade INSERT scripts for those "dependency" tables too.
Is this near possible? I was researching about this and couldn't find anything.
If you are trying to do inserts from other tables dynamically, you could do this with combination of INSERT and SELECT statements. I am not sure if this is what you are after.
E.g.:
INSERT INTO Person_Archive(Id,Name, CountryId)
SELECT Id, Name, CountryId FROM Person
WHERE CreatedAt BETWEEN '2016-01-25 00:00:00' AND '2016-12-25 23:59:59'
This is the harder one. You only want to add Countries that dont exist into the Archive. Of course more ways to skin a cat here, this is just articulating the approach.
INSERT INTO Country_Archive(Id, Name) -- INSERT INTO THE archive
SELECT Country.CountryId, Country.Name FROM Person -- SELECT rows
JOIN Country ON CountryID.Id = Person.CountryId -- Join on Country to get Country Name
WHERE CreatedAt BETWEEN '2016-01-25 00:00:00' AND '2016-12-25 23:59:59'
-- Criteria for query
AND (SELECT COUNT(*) FROM Country_Archive WHERE Country_Archive.Id = Person.CountryID) = 0)
-- This checks to see if CountryId hasn't already been added
GROUP BY Country.CountryId, Country.Name -- This get unique records only, i.e. if multiple users have same country.

Microsoft sql trigger update multiple lines

I have a two tables, a Customers and a Sales table.
I am trying to create a trigger to update the amount of sales in the customer table when the Sales table is updated.
CREATE TRIGGER salesUPDATE
ON SALES
AFTER INSERT
AS
UPDATE Customers
SET salesAmount = Sales.Amount
GO
But I get that sales does not exist. Should I be using a join?
Will this trigger update all columns or do I need to specify which column to update?
CREATE TRIGGER salesUpdate ON SALES
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
;WITH cteAffectedCustomers AS (
SELECT DISTINCT CustomerId
FROM
inserted
UNION
SELECT DISTINCT CustomerId
FROM
deleted
)
, cteAggregations AS (
SELECT
ca.CustomerId
,SUM(ISNULL(s.Amount,0)) as SalesAmount
,COUNT(s.SalesId) as NumOfSales
FROM
cteAffectedCustomers ca
INNER JOIN Customers c
ON ca.CustomerId = c.CustomerId
LEFT JOIN Sales s
ON ca.CustomerId = s.CustomerId
GROUP BY
ca.CustomerId
)
UPDATE c
SET SalesAmount = ca.SalesAmount
,NumOfSales = ca.NumOfSales
FROM
Customers c
INNER JOIN cteAggregations ca
ON c.CustomerId = ca.CustomerId
END
Here is an example of the type of logic you would need to create to maintain a pre-agregated value. If you want to SUM an Amount in the Sales table you will need an AFTER INSERT, UPDATE, and DELETE. Then you would need to:
determine all of the affected customers So you don't update the entire customer table
do the aggregation
update with an inner join to the aggregated data
A note about triggers, they are a set based operation NOT a scalar. That means they fire once for x# of rows NOT x# of times for x# of rows. So you have to account for multiple records during updates and do joins just like would outside of a trigger when updating one table with another.
This has a performance impact to write operations but does expedite your reads, however if you are not in an extremely extremely high read volume operation you would do better to use a view/query and optimize your indexes. There is less likely hood of the synchronization of the aggregate data getting messed up. If you do go the trigger route I suggest you also have a SQL job set up on some reasonable increment (nightly) that checks and rectifies any inconsistencies that may occur.
Use magic table inserted
CREATE TRIGGER salesUPDATE
ON SALES
AFTER INSERT
AS
BEGIN
Declare #Amount varchar(50) = (Select top 1 Amount from inserted)
UPDATE Customers
SET salesAmount = #Amount
END
GO
Note : top 1 for insertion of multiple records
Use the inserted table name which contains the new value:
CREATE TRIGGER salesUPDATE
ON SALES
AFTER INSERT
AS
UPDATE Customers
SET salesAmount = inserted.Amount
GO

I would like to know on how to convert Oracle triggers into SQL Server triggers

As I understand, SQL SERVER Triggers does not support FOR EACH ROW. Also I am aware that you have to use inserted tables and deleted tables. Other than that, I have no clue how to write SQL Server triggers. They look so different. Can some help please?
Below is the code for Oracle Triggers
create or replace TRIGGER Ten_Percent_Discount
BEFORE INSERT OR UPDATE ON Bookings
FOR EACH ROW
DECLARE CURSOR C_Passengers IS
SELECT StatusName
FROM Passengers
WHERE PassengerNumber = :NEW.Passengers_PassengerNumber;
l_status_name Passengers.StatusName%TYPE;
BEGIN
OPEN C_Passengers;
FETCH C_Passengers INTO l_status_name;
CLOSE C_Passengers;
Below is what I have written so far. I know I am using the inserted tables wrong
IF l_status_name = 'Regular'
THEN
:New.TotalCost := 0.90 * :New.TotalCost;
END IF;
END;
create TRIGGER Ten_Percent_Discount
ON Customer
FOR INSERT ,UPDATE
AS
DECLARE C_Passengers CURSOR FOR
SELECT StatusLevel
FROM Customer
WHERE CustomerID = inserted.CustomerID
Thanks for all the help in advance.
Table structure for customer
Table structure for Order
Below answer is only for reference purpose that you can use to build gradually towards final solution:
create table dbo.customer
(
customerid varchar(10),
firstname nvarchar(50),
statuslevel varchar(50)
)
go
create table dbo.customerorder
(
orderid varchar(10),
totalprice numeric(5,2),
productid varchar(10),
customerid varchar(10)
)
go
go
create trigger dbo.tr_customer on dbo.customer for insert,update
as
begin
update co
set co.totalprice = .9*co.totalprice
from dbo.customerorder co
inner join inserted i
on co.customerid = i.customerid
where i.statuslevel = 'Standard'
end
go
--test for above code
insert into dbo.customer values (1,'jayesh','')
insert into dbo.customerorder values (1,500.25,1,1)
insert into dbo.customerorder values (1,600.25,2,1)
select * from dbo.customer
select * from dbo.customerorder
update dbo.customer set statuslevel = 'Standard' where customerid = 1
select * from dbo.customer
select * from dbo.customerorder
But what I am pretty sure is that when customer is created for the first time, there will not be any orders to apply discounts on, so you will certainly need UPDATE Trigger as well.

SQL trigger for audit table getting out of sync

I recently created a SQL trigger to replace a very expensive query I used to run to reduce the amount of updates my database does each day.
Before I preform an update I check to see how many updates have already occurred for the day, this used to be done by querying:
SELECT COUNT(*) FROM Movies WHERE DateAdded = Date.Now
Well my database has over 1 million records and this query is run about 1-2k a minute so you can see why I wanted to take a new approach for this.
So I created an audit table and setup a SQL Trigger to update this table when any INSERT or UPDATE happens on the Movie table. However I'm noticing the audit table is getting out of sync by a few hundred everyday (the audit table count is higher than the actual updates in the movie table). As this does not pose a huge issue I'm just curious what could be causing this or how to go about debugging it?
SQL Trigger:
ALTER TRIGGER [dbo].[trg_Audit]
ON [dbo].[Movies]
AFTER UPDATE, INSERT
AS
BEGIN
UPDATE Audit SET [count] = [count] + 1 WHERE [date] = CONVERT (date, GETDATE())
IF ##ROWCOUNT=0
INSERT INTO audit ([date], [count]) VALUES (GETDATE(), 1)
END
The above trigger only happens after an UPDATE or INSERT on the Movie table and tries to update the count + 1 in the Audit table and if it doesn't exists (IF ##ROWCOUNT=0) it then creates it. Any help would be much appreciated! Thanks.
Something like this should work:
create table dbo.Movies (
A int not null,
B int not null,
DateAdded datetime not null
)
go
create view dbo.audit
with schemabinding
as
select CONVERT(date,DateAdded) as dt,COUNT_BIG(*) as cnt
from dbo.Movies
group by CONVERT(date,DateAdded)
go
create unique clustered index IX_MovieCounts on dbo.audit (dt)
This is called an indexed view. The advantage is that SQL Server takes responsibility for maintaining the data stored in this view, and it's always right.
Unless you're on Enterprise/Developer edition, you'd query the audit view using the NOEXPAND hint:
SELECT * from audit with (noexpand)
This has the advantages that
a) You don't have to write the triggers yourself now (SQL Server does actually have something quite similar to triggers behind the scenes),
b) It can now cope with multi-row inserts, updates and deletes, and
c) You don't have to write the logic to cope with an update that changes the DateAdded value.
Rather than incrementing the count by 1 you should probably be incrementing it by the number of records that have changed e.g.
UPDATE Audit
SET [count] = [count] + (SELECT COUNT(*) FROM INSERTED)
WHERE [date] = CONVERT (date, GETDATE())
IF ##ROWCOUNT=0
INSERT INTO audit ([date], [count])
VALUES (GETDATE(), (SELECT COUNT(*) FROM INSERTED))

Resources