What is wrong with my trigger? No results are inserted - sql-server

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

Related

SQL Server : AFTER INSERT trigger and INSERT INTO

I want to create a trigger that will fill up my sales history base after firing in the ORDER table.
I am creating a specific order in regular data base and after that this order automatically goes to sales_history database.
Below part works properly.
When I create a new order in regular database my sales_history database is growing with new ID_ORDERS, hooray! :)
ALTER TRIGGER [dbo].[InsertTrig]
ON [dbo].[ORDER]
AFTER INSERT
AS
IF EXISTS (SELECT *
FROM inserted i
WHERE i.ID_TYPE = 1) -- specific order type
BEGIN
INSERT INTO id.dbo.sales_history (id_order)
SELECT i.ID_ORDER FROM inserted i
END
The problem arises when I want join another table. The trigger stops working
ALTER TRIGGER [dbo].[InsertTrig]
ON [dbo].[ORDER]
AFTER INSERT
AS
IF EXISTS (SELECT *
FROM inserted i
WHERE i.ID_TYPE = 1) -- specific order type
BEGIN
INSERT INTO id.dbo.sales_history (id_order, id_item)
SELECT
inserted.ID_ORDER, ORDER_DETAILS.ID_ITEM
FROM
inserted
INNER JOIN
ORDER_DETAILS ON ORDER_DETAILS.ID_ORDER = inserted.ID_ORDER
END
I also tried this way, and still nothing :(
ALTER TRIGGER [dbo].[InsertTrig]
ON [dbo].[ORDER]
AFTER INSERT
AS
IF EXISTS (SELECT *
FROM inserted i
WHERE i.ID_TYPE = 1) -- specific order type
BEGIN
DECLARE #xyz AS numeric(18, 0)
SET #xyz = (SELECT inserted.ID_ORDER FROM inserted)
INSERT INTO id.dbo.sales_history (id_order, id_item)
SELECT
ORDER.ID_ORDER, ORDER_DETAILS.ID_ITEM
FROM
ORDER
INNER JOIN
ORDER_DETAILS ON ORDER_DETAILS.ID_ORDER = ORDER.ID_ORDER
WHERE
ORDER.ID_ORDER = #xyz
END
I want to create a trigger that will automatically fill up my sales history base after firing in ORDER table.
Your trigger is on the Order table, meaning SQL Server fires it after you insert records into the Order table. At which point, the relevant records in the Order_Details table couldn't have been inserted yet, because they have a foreign key to the Order table.
This is why an inner join between your inserted table and the Order_details table returns 0 rows.
If you want your sales_history from the order_details table, you have to populate it after you insert the records to the order_details table.
CREATE OR ALTER TRIGGER [dbo].[OrderDetails_AfterInsert]
ON [dbo].[ORDER_DETAILS]
AFTER INSERT
AS
INSERT INTO id.dbo.sales_history (id_order, id_item)
SELECT
inserted.ID_ORDER, inserted.ID_ITEM
FROM
inserted
INNER JOIN
[ORDER] ON [ORDER].ID_ORDER = inserted.ID_ORDER
WHERE [ORDER].ID_TYPE = 1 -- specific order type
As a side note: InsertTrig is bad name. Note the name of the trigger in my answer - it tells you exactly what this trigger is for, and on what table.

How to insert into another table if one of two columns has been updated in Trigger?

I am trying to insert values into another table with an after update trigger in the SQL server.
I have a table where I am storing my employees (employee_id, name, exam_results, exam_date) and I need to make a trigger that allows me to insert employee_id, exam_results, and exam_date into a table called exams_backlog whenever exam_results or exam_date is updated in table employees.
I don't know if I am able to check those fields to make the insert.
This is what I have so far:
CREATE TRIGGER updateExamBacklogTrigger
ON employees
AFTER UPDATE
AS
BEGIN
*****HERE IS WHERE I NEED TO CHECK IF ONE OF THOSE TWO FIELDS HAS BEEN UPDATED*****
INSERT INTO exams_backlog SELECT e.employee_id, e.exam_results, e.exam_date FROM employees e
SET NOCOUNT ON;
END
GO
Thanks in advance.
I have found the solution I've been looking for:
As I wanted to check if exam_results or exam_date had been updated I have used
IF(UPDATE(exam_results) OR UPDATE(exam_date ))
And then in order to only insert data for those employees that had been updated I used an
INNER JOIN inserted i ON i.employee_id = e.employee_id
So the complete trigger results as this:
CREATE TRIGGER updateExamBacklogTrigger
ON employees
AFTER UPDATE
AS
BEGIN
IF(UPDATE(exam_results) OR UPDATE(exam_date ))
INSERT INTO exams_backlog SELECT e.employee_id, e.exam_results, e.exam_date FROM employees e INNER JOIN inserted i ON i.employee_id = e.employee_id
SET NOCOUNT ON;
END
GO

Create trigger to only allow updates for certain employees

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.

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))

Instead of trigger in SQL Server loses SCOPE_IDENTITY?

I have a table where I created an INSTEAD OF trigger to enforce some business rules.
The issue is that when I insert data into this table, SCOPE_IDENTITY() returns a NULL value, rather than the actual inserted identity.
Insert + Scope code
INSERT INTO [dbo].[Payment]([DateFrom], [DateTo], [CustomerId], [AdminId])
VALUES ('2009-01-20', '2009-01-31', 6, 1)
SELECT SCOPE_IDENTITY()
Trigger:
CREATE TRIGGER [dbo].[TR_Payments_Insert]
ON [dbo].[Payment]
INSTEAD OF INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
IF NOT EXISTS(SELECT 1 FROM dbo.Payment p
INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
WHERE (i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo)
) AND NOT EXISTS (SELECT 1 FROM Inserted p
INNER JOIN Inserted i ON p.CustomerId = i.CustomerId
WHERE (i.DateFrom <> p.DateFrom AND i.DateTo <> p.DateTo) AND
((i.DateFrom >= p.DateFrom AND i.DateFrom <= p.DateTo) OR (i.DateTo >= p.DateFrom AND i.DateTo <= p.DateTo))
)
BEGIN
INSERT INTO dbo.Payment (DateFrom, DateTo, CustomerId, AdminId)
SELECT DateFrom, DateTo, CustomerId, AdminId
FROM Inserted
END
ELSE
BEGIN
ROLLBACK TRANSACTION
END
END
The code worked before the creation of this trigger. I am using LINQ to SQL in C#. I don't see a way of changing SCOPE_IDENTITY to ##IDENTITY. How do I make this work?
Use ##identity instead of scope_identity().
While scope_identity() returns the last created id in the current scope, ##identity returns the last created id in the current session.
The scope_identity() function is normally recommended over the ##identity field, as you usually don't want triggers to interfer with the id, but in this case you do.
Since you're on SQL 2008, I would highly recommend using the OUTPUT clause instead of one of the custom identity functions. SCOPE_IDENTITY currently has some issues with parallel queries that cause me to recommend against it entirely. ##Identity does not, but it's still not as explicit, and as flexible, as OUTPUT. Plus OUTPUT handles multi-row inserts. Have a look at the BOL article which has some great examples.
I was having serious reservations about using ##identity, because it can return the wrong answer.
But there is a workaround to force ##identity to have the scope_identity() value.
Just for completeness, first I'll list a couple of other workarounds for this problem I've seen on the web:
Make the trigger return a rowset. Then, in a wrapper SP that performs the insert, do INSERT Table1 EXEC sp_ExecuteSQL ... to yet another table. Then scope_identity() will work. This is messy because it requires dynamic SQL which is a pain. Also, be aware that dynamic SQL runs under the permissions of the user calling the SP rather than the permissions of the owner of the SP. If the original client could insert to the table, he should still have that permission, just know that you could run into problems if you deny permission to insert directly to the table.
If there is another candidate key, get the identity of the inserted row(s) using those keys. For example, if Name has a unique index on it, then you can insert, then select the (max for multiple rows) ID from the table you just inserted to using Name. While this may have concurrency problems if another session deletes the row you just inserted, it's no worse than in the original situation if someone deleted your row before the application could use it.
Now, here's how to definitively make your trigger safe for ##Identity to return the correct value, even if your SP or another trigger inserts to an identity-bearing table after the main insert.
Also, please put comments in your code about what you are doing and why so that future visitors to the trigger don't break things or waste time trying to figure it out.
CREATE TRIGGER TR_MyTable_I ON MyTable INSTEAD OF INSERT
AS
SET NOCOUNT ON
DECLARE #MyTableID int
INSERT MyTable (Name, SystemUser)
SELECT I.Name, System_User
FROM Inserted
SET #MyTableID = Scope_Identity()
INSERT AuditTable (SystemUser, Notes)
SELECT SystemUser, 'Added Name ' + I.Name
FROM Inserted
-- The following statement MUST be last in this trigger. It resets ##Identity
-- to be the same as the earlier Scope_Identity() value.
SELECT MyTableID INTO #Trash FROM MyTable WHERE MyTableID = #MyTableID
Normally, the extra insert to the audit table would break everything, because since it has an identity column, then ##Identity will return that value instead of the one from the insertion to MyTable. However, the final select creates a new ##Identity value that is the correct one, based on the Scope_Identity() that we saved from earlier. This also proofs it against any possible additional AFTER trigger on the MyTable table.
Update:
I just noticed that an INSTEAD OF trigger isn't necessary here. This does everything you were looking for:
CREATE TRIGGER dbo.TR_Payments_Insert ON dbo.Payment FOR INSERT
AS
SET NOCOUNT ON;
IF EXISTS (
SELECT *
FROM
Inserted I
INNER JOIN dbo.Payment P ON I.CustomerID = P.CustomerID
WHERE
I.DateFrom < P.DateTo
AND P.DateFrom < I.DateTo
) ROLLBACK TRAN;
This of course allows scope_identity() to keep working. The only drawback is that a rolled-back insert on an identity table does consume the identity values used (the identity value is still incremented by the number of rows in the insert attempt).
I've been staring at this for a few minutes and don't have absolute certainty right now, but I think this preserves the meaning of an inclusive start time and an exclusive end time. If the end time was inclusive (which would be odd to me) then the comparisons would need to use <= instead of <.
Main Problem : Trigger and Entity framework both work in diffrent scope.
The problem is, that if you generate new PK value in trigger, it is different scope. Thus this command returns zero rows and EF will throw exception.
The solution is to add the following SELECT statement at the end of your Trigger:
SELECT * FROM deleted UNION ALL
SELECT * FROM inserted;
in place of * you can mention all the column name including
SELECT IDENT_CURRENT(‘tablename’) AS <IdentityColumnname>
Like araqnid commented, the trigger seems to rollback the transaction when a condition is met. You can do that easier with an AFTER INSTERT trigger:
CREATE TRIGGER [dbo].[TR_Payments_Insert]
ON [dbo].[Payment]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF <Condition>
BEGIN
ROLLBACK TRANSACTION
END
END
Then you can use SCOPE_IDENTITY() again, because the INSERT is no longer done in the trigger.
The condition itself seems to let two identical rows past, if they're in the same insert. With the AFTER INSERT trigger, you can rewrite the condition like:
IF EXISTS(
SELECT *
FROM dbo.Payment a
LEFT JOIN dbo.Payment b
ON a.Id <> b.Id
AND a.CustomerId = b.CustomerId
AND (a.DateFrom BETWEEN b.DateFrom AND b.DateTo
OR a.DateTo BETWEEN b.DateFrom AND b.DateTo)
WHERE b.Id is NOT NULL)
And it will catch duplicate rows, because now it can differentiate them based on Id. It also works if you delete a row and replace it with another row in the same statement.
Anyway, if you want my advice, move away from triggers altogether. As you can see even for this example they are very complex. Do the insert through a stored procedure. They are simpler and faster than triggers:
create procedure dbo.InsertPayment
#DateFrom datetime, #DateTo datetime, #CustomerId int, #AdminId int
as
BEGIN TRANSACTION
IF NOT EXISTS (
SELECT *
FROM dbo.Payment
WHERE CustomerId = #CustomerId
AND (#DateFrom BETWEEN DateFrom AND DateTo
OR #DateTo BETWEEN DateFrom AND DateTo))
BEGIN
INSERT into dbo.Payment
(DateFrom, DateTo, CustomerId, AdminId)
VALUES (#DateFrom, #DateTo, #CustomerId, #AdminId)
END
COMMIT TRANSACTION
A little late to the party, but I was looking into this issue myself. A workaround is to create a temp table in the calling procedure where the insert is being performed, insert the scope identity into that temp table from inside the instead of trigger, and then read the identity value out of the temp table once the insertion is complete.
In procedure:
CREATE table #temp ( id int )
... insert statement ...
select id from #temp
-- (you can add sorting and top 1 selection for extra safety)
drop table #temp
In instead of trigger:
-- this check covers you for any inserts that don't want an identity value returned (and therefore don't provide a temp table)
IF OBJECT_ID('tempdb..#temp') is not null
begin
insert into #temp(id)
values
(SCOPE_IDENTITY())
end
You probably want to call it something other than #temp for safety sake (something long and random enough that no one else would be using it: #temp1234235234563785635).

Resources