I need to set up an "alert" (will send an email) if the insertion of records in, say, 1 hour, exceeds, say, 10 records.
I'm thinking about doing this in an INSERT trigger, but I'm not clear on what the best way to check that condition (or the syntax for it) is.
Thanks
I don't think a trigger and an audit table is the best way to do this. What I would do is the following:
Add a column (of type datetime) to your table called CreateDT
You can add a default value of GETDATE() to the column.
Then in an external process you can do a select like the following
SELECT COUNT(*)
FROM TABLE
WHERE CreateDT > dateadd(hour,datediff(hour,0,datediff(hour,-1,getdate())),0)
and CreateDT < dateadd(hour,datediff(hour,0,#datetime),0)
This will check the prior hour counts.
To check the last 24 hours counts you could get a list like this
SELECT HOUR(CreateDT), COUNT(*)
FROM (
SELECT CreateDt
FROM TABLE
WHERE CreateDT > dateadd(day,-1,getdate())
) T
GROUP BY HOUR(CreateDT)
Assuming you have created time on your table
Create Trigger trig_CheckRecordCount
ON TableName
For Insert
as
Begin
IF( (select COUNT(*)
from TableName
where CreatedOn >
(
Select DATEADD(HOUR,-1, CreatedOn)
from inserted))>10)
begin
//call a stored procedure to send email
end
end
Related
I have a table with few million rows. I need to transfer that data using batches to another table using SSIS. I tried with below query to get records as batches but the loop doesn't stop.
while exists (select top 1 * from dbo.test
where (date > '2018-04-25'
and date < '2018-04-27'))
Begin
select * from dbo.test
where id in ( select top (1000) id
where (date > '2018-04-25'
and date < '2018-04-27'))
order by date asc
End
This should be the only query you need:
select * from dbo.test
where id in ( select id
where (date > '2018-04-25'
and date < '2018-04-27'))
In SSIS, on the Destination Editor in your Data Flow tab for this operation, enter 1000 for the "Rows per batch:" setting. No need to try to do this with a loop and top 1000. I would highly recommend against that approach as the performance will be worse. Possibly exponentially so.
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
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))
I m sry if the title is not clear.
Here's my proble.
I created a new table which will show total, average and maximum values.
I have to insert the results into that table.
That table will have only 4 rows. No Appointment, Appointment Early, Appointment Late and Appointment Punctual.
So.. I have sth like..
insert into newTable
select
'No Appointment' as 'Col1',
avg statement,
total statement,
max statement
from orgTable
where (general conditions) and (unique condition to check NO APPOINTMENT);
I have to do that same thing for another 3 rows.. where only the unique condition is different to check early, punctual or late..
So..the statement is super long.
I wanna reduce the size.. How can I achieve that?
Complete proof-of-concept (now in Sql Server), and also we use CTE(the WITH clause) so we don't need to repeat the specific conditions on GROUP BY clause; basically the logic of the program is to tag first the rows where specific condition they belong to, then from there we group them outside of WITH clause.
-- drop table attendance;
-- drop table yourtable;
create table attendance
(
arrived datetime
);
declare #d datetime;
set #d = GETDATE();
declare #t integer;
set #t = 0;
while #t < 50 begin
insert into attendance select dateadd(hour, #t, #d);
set #t = #t + 1;
end
select * from attendance;
create table yourtable
(
segment varchar(max),
total int,
the_max datetime
);
with tagger as
(
select
-- specific conditions here
case
when DATEPART(hour,arrived) between 0 and 7 then
'First Shift'
when DATEPART(hour,arrived) between 8 and 15 then
'Second Shift'
when DATEPART(hour,arrived) between 16 and 23 then
'Third Shift'
else
'Hurry, inform the programmer of the anomaly!'
end
as segment,
*
from attendance
)
insert into yourtable(segment, total, the_max)
select segment, count(*), max(arrived)
from tagger
group by segment;
select * from yourtable
No need to reduce the size, really. SQL statements can get pretty long.
However, once it's run, it's done. The records will be there, and you can move on.
Is this something that will happen frequently, or in a loop?
Let's take your original statement:
insert into newTable
select
'No Appointment' as 'Col1',
avg statement,
total statement,
max statement
from orgTable
where (general conditions) and (unique condition to check NO APPOINTMENT);
Let's hope that unique condition to check <X> is simple. We can change your statement to:
insert into newTable
select
CASE
WHEN (unique condition to check NO APPOINTMENT) THEN 'No Appointment'
WHEN (unique condition to check APPOINTMENT EARLY) THEN 'Early'
WHEN (unique condition to check APPOINTMENT LATE) THEN 'Late'
WHEN (unique condition to check APPOINTMENT PUNCTUAL) THEN 'Punctual'
END as 'Col1',
avg statement,
total statement,
max statement
from orgTable
where (general conditions)
GROUP BY
CASE
WHEN (unique condition to check NO APPOINTMENT) THEN 'No Appointment'
WHEN (unique condition to check APPOINTMENT EARLY) THEN 'Early'
WHEN (unique condition to check APPOINTMENT LATE) THEN 'Late'
WHEN (unique condition to check APPOINTMENT PUNCTUAL) THEN 'Punctual'
END
If these conditions start to look too complex, then you may instead move this case expression into a subselect statement (i.e. your from clause would look something like:
from (select col1,col2,col3,<case expression here> as Appointment from orgTable) t
And you would then be able to refer to Appointment in your SELECT and GROUP BY clauses.
I'm assuming that all rows in orgTable fall into one of these 4 appointment categories. Otherwise, you may need to filter these rows further.
First off, I want to start by saying I am not an SQL programmer (I'm a C++/Delphi guy), so some of my questions might be really obvious. So pardon my ignorance :o)
I've been charged with writing a script that will update certain tables in a database based on the contents of a CSV file. I have it working it would seem, but I am worried about atomicity for one of the steps:
One of the tables contains only one field - an int which must be incremented each time, but from what I can see is not defined as an identity for some reason. I must create a new row in this table, and insert that row's value into another newly-created row in another table.
This is how I did it (as part of a larger script):
DECLARE #uniqueID INT,
#counter INT,
#maxCount INT
SELECT #maxCount = COUNT(*) FROM tempTable
SET #counter = 1
WHILE (#counter <= #maxCount)
BEGIN
SELECT #uniqueID = MAX(id) FROM uniqueIDTable <----Line 1
INSERT INTO uniqueIDTableVALUES (#uniqueID + 1) <----Line 2
SELECT #uniqueID = #uniqueID + 1
UPDATE TOP(1) tempTable
SET userID = #uniqueID
WHERE userID IS NULL
SET #counter = #counter + 1
END
GO
First of all, am I correct using a "WHILE" construct? I couldn't find a way to achieve this with a simple UPDATE statement.
Second of all, how can I be sure that no other operation will be carried out on the database between Lines 1 and 2 that would insert a value into the uniqueIDTable before I do? Is there a way to "synchronize" operations in SQL Server Express?
Also, keep in mind that I have no control over the database design.
Thanks a lot!
You can do the whole 9 yards in one single statement:
WITH cteUsers AS (
SELECT t.*
, ROW_NUMBER() OVER (ORDER BY userID) as rn
, COALESCE(m.id,0) as max_id
FROM tempTable t WITH(UPDLOCK)
JOIN (
SELECT MAX(id) as id
FROM uniqueIDTable WITH (UPDLOCK)
) as m ON 1=1
WHERE userID IS NULL)
UPDATE cteUsers
SET userID = rn + max_id
OUTPUT INSERTED.userID
INTO uniqueIDTable (id);
You get the MAX(id), lock the uniqueIDTable, compute sequential userIDs for users with NULL userID by using ROW_NUMBER(), update the tempTable and insert the new ids into uniqueIDTable. All in one operation.
For performance you need and index on uniqueIDTable(id) and index on tempTable(userID).
SQL is all about set oriented operations, WHILE loops are the code smell of SQL.
You need a transaction to ensure atomicity and you need to move the select and insert into one statement or do the select with an updlock to prevent two people from running the select at the same time, getting the same value and then trying to insert the same value into the table.
Basically
DECLARE #MaxValTable TABLE (MaxID int)
BEGIN TRANSACTION
BEGIN TRY
INSERT INTO uniqueIDTable VALUES (id)
OUTPUT inserted.id INTO #MaxValTable
SELECT MAX(id) + 1 FROM uniqueIDTable
UPDATE TOP(1) tempTable
SET userID = (SELECT MAXid FROM #MaxValTable)
WHERE userID IS NULL
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
RAISERROR 'Error occurred updating tempTable' -- more detail here is good
END CATCH
That said, using an identity would make things far simpler. This is a potential concurrency problem. Is there any way you can change the column to be identity?
Edit: Ensuring that only one connection at a time will be able to insert into the uniqueIDtable. Not going to scale well though.
Edit: Table variable's better than exclusive table lock. If need be, this can be used when inserting users as well.