Optimising SQL Procedure in SQL Server 2008 - sql-server

I have two table Old_Table and New_Table. My old table has 13 million records in it currently and I need to process the data in old table and dump it into new. Data will be continuously inserted into my old table. So, I wrote a stored procedure and created a sql job to run nightly and process the data that is inserted into the old table for that day.
My procedure is taking hell lot of a time. Like, my procedure need to process 13 million records per day and the sql job is running never endingly since it started. How can I optimise my below procedure to make it faster
select #From = Max(InsertTime) From [New_Table];
set #To = GETDATE();
declare #ID as int;
set #ID = 0;
SET #ID = (SELECT MIN(Id) FROM Old_Table where TimeStamp > #From and TimeStamp < #To)
WHILE #ID IS NOT NULL
BEGIN
--Get the row data
SELECT #col1 = COLUMN1,
#col2 = case when CHARINDEX('?', COLUMN2) > 0
THEN SUBSTRING(COLUMN2, 1,CHARINDEX('?', COLUMN2)-1)
else COLUMN2
END,
#col3 = [dbo].[ModifyString] (COLUMN3)
from Old_Table with(nolock)
where Id = #ID;
--Few if conditions
select #rowID = ID from [dbo].[New_Table] with(nolock)
where [COL1] = #col1 and [COL2] = #col2 and [COL3] = #col3
--If exists update the row else insert as new row
If #rowID > 0
Begin
-- Update my New_Table
End
Else
Begin
-- Insert into my new table
End
--delete from Old_Table
delete from [dbo].[Old_Table] where id = #ID
--Fetch next record
SET #ID = (SELECT MIN(Id) FROM Old_Table where TimeStamp > #From and TimeStamp < #To);
END
My function [dbo].[ModifyString] has 5 IF conditions which uses CHARINDEX and STUFF functions with in it.
Adding Table defenitions:
[New_Table](
[ID] [int] IDENTITY(1,1) NOT NULL, --Primary Key
[COL1] [varchar](max) NULL,
[COL2] [varchar](max) NULL,
[COL3] [varchar](max) NULL,
[Count] [int] NULL,
[TimeImported] [datetime] NULL, -- Non-Clustered Index
[COL5_NEW] [bit] NULL,
[COL6_NEW] [bit] NULL,
[COL7_NEW] [bit] NULL,
[Old_Table](
[id] [int] IDENTITY(1,1) NOT NULL, -- Primary Key
[TimeStamp] [datetime] NOT NULL, -- Non-Clustered Index
[COL1] [varchar](max) NULL,
[COL2] [varchar](max) NULL,
[COL3] [varchar](max) NULL,
Edit: My old table has duplicate records and [Count] in the new table needs to be incremented for each duplicate record in the old table.
Thanks.

Convert the query to set-based operations. I see nothing preventing that from working. Iteration-based code is not recommended in T-SQL for performance and maintainability reasons.
You can either use the MERGE statement to execute all changes at once, or run one insert, update and delete statement.
I see no reason why you cannot update all 13 million records in one (or three) statements. This will realize huge efficiency gains (orders of magnitude).

Loops are no bueno in SQL. Avoid them if you can... This psudocode should get you started down the right road:
--Insert new
INSERT INTO NewTable
SELECT * FROM OldTable
EXCEPT
SELECT * FROM NewTable
--Update existing
UPDATE NewTable as nt
INNER JOIN OldTable as ot
ON nt.id = ot.id
SET nt.value = ot.value
Most modern DBMSs are designed to handle set based operations so if you see a loop you can expect performance problems.

As well as the RBAR others have mentioned (which needs addressing), you're also searching New_Table for a match against 3 varchar(MAX) fields, which aren't indexable.
I'd suggest adding OldTableID as an indexed int column to New_Table, then amending the script to use it as the link (populating it as part of the MERGE or INSERT used) instead of the three varchar columns (as well as sorting the RBAR). An index on Old_Table.TimeStamp would also help.

This is a template for the insert
insert into New_Table
SELECT Old_Table.ID, Old_Table.COLUMN1,
case when CHARINDEX('?', Old_Table.COLUMN2) > 0
THEN SUBSTRING(Old_Table.COLUMN2, 1,CHARINDEX('?', COLUMN2)-1)
else Old_Table.COLUMN2
END,
Old_Table.[ModifyString] (COLUMN3)
from Old_Table
left join New_Table
on Old_Table.[COL1] = New_Table.[COL1]
and Old_Table.[COL2] = New_Table.[COL2]
and Old_Table.[COL3] = New_Table.[COL3]
where Old_Table,TimeStamp > #From and Old_Table.TimeStamp < #To
and New_Table.ID is null
See this for combining the delete and insert
enter link description here
Then the update is a join (no left) and no New_Table.ID is null

Related

SQL Server parameterized query does not use Non Clustered Filtered

I defined a non clustered index with Include and Filter on Students table. The SQL Server version is 2017.
Students table definition:
CREATE TABLE [dbo].[Students]
(
[Id] [INT] IDENTITY(1,1) NOT NULL,
[Name] [NVARCHAR](50) NOT NULL,
[CreatedOn] [DATETIME2](7) NOT NULL,
[Active] [BIT] NOT NULL,
[Deleted] [BIT] NOT NULL,
CONSTRAINT [PK_Students]
PRIMARY KEY CLUSTERED ([Id] ASC)
) ON [PRIMARY]
Non-clustered index with include and filter:
CREATE NONCLUSTERED INDEX [NonClusteredIndex-20200508-225254]
ON [dbo].[Students] ([CreatedOn] ASC)
INCLUDE([Name])
WHERE ([Active] = (1) AND [Deleted] = (0))
ON [PRIMARY]
GO
This query uses NonClusteredIndex-20200508-225254
SELECT Name, CreatedOn FROM dbo.Students
WHERE Active = 1
AND Deleted = 0
ORDER BY CreatedOn
Actual execution plan
But when I use the parameterized query as following, it doesn't use the NonClusteredIndex-20200508-225254. Why does this happen? Where am I wrong?
DECLARE #Active BIT = 1
DECLARE #Deleted BIT = 0
SELECT Name, CreatedOn
FROM dbo.Students
WHERE Active = #Active
AND Deleted = #Deleted
ORDER BY CreatedOn
Actual execution plan
This is entirely expected.
When you compile a plan with parameters or variables it needs to produce a plan that will work for any possible value they may have.
You can add OPTION (RECOMPILE) to the statement so the runtime values of these are taken into account (basically they are replaced by literals with the runtime value) but that will mean a recompile every execution.
You are probably best having two separate queries, one for the case handled by the filtered index and one for the other cases.
You might have been hoping that SQL Server would do something like the below and dynamically switch between the clustered index scan + sort vs filtered index scan and no sort (the filters have startup predicates so only at most one branch is executed)
But getting this plan required a change to the filtered index to move Name into the key columns as below...
CREATE NONCLUSTERED INDEX [NonClusteredIndex-20200508-225254]
ON [dbo].[Students] ([CreatedOn] ASC, [Name] asc)
WHERE ([Active] = (1) AND [Deleted] = (0))
... and rewriting the query to
DECLARE #Active BIT = 1
DECLARE #Deleted BIT = 0
SELECT NAME,
CreatedOn
FROM dbo.Students WITH (INDEX =[NonClusteredIndex-20200508-225254])
WHERE Active = 1
AND Deleted = 0
AND 1 = 1 /*Prevent auto parameterisation*/
AND ( #Active = 1 AND #Deleted = 0 )
UNION ALL
SELECT NAME,
CreatedOn
FROM dbo.Students
WHERE Active = #Active
AND Deleted = #Deleted
AND NOT ( #Active = 1
AND #Deleted = 0 )
ORDER BY CreatedOn
OPTION (MERGE UNION)

How can I insert a newly generated IDENTITY value into a related table with other fields?

I have a data table that contains a name and a social security number. I want to insert the name into a table with an identity field, then insert the ssn with that new identity field value into another table.
Below are the tables:
CREATE TABLE [data_table]
(
[name] [varchar](50) NOT NULL,
[ssn] [varchar](9) NOT NULL,
)
CREATE TABLE [entity_key_table]
(
[entity_key] [int] IDENTITY(1000000,1) NOT NULL,
[name] [varchar](50) NOT NULL,
)
CREATE TABLE [entity_identifier_table]
(
[entity_identifier_key] [int] IDENTITY(1000000,1) NOT NULL,
[entity_key] [int] NOT NULL,
[ssn] [int] NOT NULL,
)
This query works but doesn't link entity_key in [entity_key_table] TO ssn in [entity_identifier_table]:
INSERT INTO entity_key_table (name)
OUTPUT [INSERTED].[entity_key]
INTO [entity_identifier_table] (entity_key)
SELECT [name]
FROM [data_table]
This is what I want to do, but it doesn't work.
INSERT INTO entity (name)
OUTPUT [INSERTED].[entity_key], [data_table].[ssn]
INTO [entity_identifier] (entity_key,ssn)
SELECT [name]
FROM [data_table]
Rewriting my answer based on your requirements and the articles you linked. I think you can get that behavior doing something like this. I admit, I have never seen a merge on something like 1 != 1 like the article suggests, so I would be very cautious with this and test the bajeezes out out of it.
FWIW, it looks like during an INSERT, you can't access data that's not in the inserted virtual table, but updates (and apparently MERGE statements) can.
if object_id('tempdb.dbo.#data_table') is not null drop table #data_table
create table #data_table
(
[name] [varchar](50) NOT NULL,
[ssn] [varchar](9) NOT NULL,
)
if object_id('tempdb.dbo.#entity_key_table') is not null drop table #entity_key_table
create table #entity_key_table
(
[entity_key] [int] IDENTITY(1000000,1) NOT NULL,
name varchar(50)
)
if object_id('tempdb.dbo.#entity_identifier_table') is not null drop table #entity_identifier_table
create table #entity_identifier_table
(
[entity_identifier_key] [int] IDENTITY(2000000,1) NOT NULL,
[entity_key] [int] NOT NULL,
[ssn] varchar(9) NOT NULL,
)
insert into #Data_table (Name, SSN)
select 'John', '123456789' union all
select 'John', '001100110' union all
select 'Jill', '987654321'
merge into #entity_key_table t
using #data_table s
on 1 != 1
when not matched then insert
(
name
)
values
(
s.name
)
output inserted.entity_key, s.ssn
into #entity_identifier_table
(
entity_key,
ssn
);
select top 1000 *
from #data_table
select top 1000 *
from #entity_key_table
select top 1000 *
from #entity_identifier_table
The problem with your code is that you output data only from inserted or deleted.
Assuming your name column only relates to one SSN, the following would work:
DECLARE #output TABLE (entity_key INT,ssn VARCHAR (11))
INSERT INTO entity (entity_key, name)
OUTPUT [INSERTED].[entity_key], [inserted].[name]
INTO #output
SELECT D.Entity_key, d.name
FROM datatable
INSERT INTO entity_identifier (entity_key, ssn)
Select o.entity_key, d.snn
from #output o
join datatable d on o.name = d.name
However, the problem of multiple duplicated names having different Social Security Numbers is extremely high. In this case, your current structure simply does not work because there is no way to know which identity belongs to which name record. (The Merge solution in another post may also have this problem, before you put that to production be sure to test the scenario of duplicated names. The chances of duplicated names in a set of records is extremely high in any reasonable large data set of names and this should be one of your unit test for any potential solution.)
Here is a potential workaround. First, insert the SSN as the name in the first insert, then return output as shown but join on the #output Name column to the SSN column. After doing the other insert, then update the name in the orginal table to the correct name again joining on the SSN data.
DECLARE #output TABLE (entity_key INT,ssn VARCHAR (11))
INSERT INTO entity (entity_key, name)
OUTPUT [INSERTED].[entity_key], [inserted].[ssn]
INTO #output
SELECT D.Entity_key, d.name
FROM datatable
INSERT INTO entity_identifier (entity_key, ssn)
Select o.entity_key, d.output
from #output o
update e
set name = d.name
FROM entity e
join #output o on e.entity_key = o.entity_key
join datatable d on o.name = d.ssn

Exception when updating row with rowversion?

I have a table that looks like this :
CREATE TABLE [dbo].[akut_prioritering]
(
[behandling_id] [int] NOT NULL,
[akutstatus] [int] NOT NULL,
[nasta_dag] [bit] NOT NULL,
[sort_order] [bigint] NOT NULL,
[rowversion] [timestamp] NOT NULL,
CONSTRAINT [XPKakut_prioritering]
PRIMARY KEY CLUSTERED ([behandling_id] ASC)
) ON [PRIMARY]
And then I have this stored procedure that tries to update rows in this table :
ALTER PROCEDURE [dbo].[akutlistaSave]
#behandlingSortOrder dbo.akutlista_sortorder_tabletype READONLY
AS
BEGIN
SET NOCOUNT ON;
DECLARE #behandlingId INT;
DECLARE #sortOrder BIGINT;
DECLARE #rowversion ROWVERSION;
DECLARE sortOrderCursor CURSOR LOCAL SCROLL STATIC FOR
SELECT behandling_id, sort_order FROM #behandlingSortOrder
OPEN sortOrderCursor
BEGIN TRAN
FETCH NEXT FROM sortOrderCursor INTO #behandlingId, #sortOrder, #rowversion
WHILE ##FETCH_STATUS = 0
BEGIN
IF EXISTS(SELECT *
FROM akut_prioritering ap
WHERE ap.behandling_id = #behandlingId
AND ap.rowversion = #rowversion)
BEGIN
UPDATE akut_prioritering
SET sort_order = #sortOrder
WHERE behandling_id = #behandlingId;
END
ELSE
BEGIN
RAISERROR ('Rowversion not correct.', 16, 1);
END
FETCH NEXT FROM sortOrderCursor INTO #behandlingId, #sortOrder, #rowversion
END
CLOSE sortOrderCursor
SELECT
ap.behandling_id, ap.rowversion
FROM
akut_prioritering ap
INNER JOIN
#behandlingSortOrder bso ON ap.behandling_id = bso.behandling_id;
DEALLOCATE sortOrderCursor
END
The inparameter type looks like this :
CREATE TYPE [dbo].[akutlista_sortorder_tabletype] AS TABLE
(
[behandling_id] [int] NULL,
[sort_order] [bigint] NULL,
[rowversion] [timestamp] NULL
)
When running this I get a SqlException :
Cannot insert an explicit value into a timestamp column. Use INSERT with a column list to exclude the timestamp column, or insert a DEFAULT into the timestamp column.
From what I understand the rowversion column should be updated with a new value automatically, there is no reason in my case to set it manual.
You can't set the rowversion value in dbo.akutlista_sortorder_tabletype because it is not updateable: it is auto generated
However, rowversion (a.k.a deprecated timestamp) is simply a (var)binary(8) with some special rules. You can define and set a (var)binary(8) in dbo.akutlista_sortorder_tabletype and compare on that in the UPDATE
From the first link
A nonnullable rowversion column is semantically equivalent to a binary(8) column. A nullable rowversion column is semantically equivalent to a varbinary(8) column.
It looks like you are trying to insert a timestamp value in a custom table type and then passing that to your stored procedure. As your error suggests, you cannot insert explicit timestamp values into timestamp columns.
You will need to find a different way of passing you table values to this stored procedure to work.

How can I avoid or minimize deadlocks in this situation?

I have a relatively small table (for now). It works as a fancy queue. Jobs that execute every /second/, ask this table for more work and whenever work completes, they tell the table that work is completed.
Table has ~1000 entries or so entries and long-term will hopefully have 100k+ rows
Each entry signifies a job that needs to be executed once per minute. Table is hosted in SQL Azure (S2 plan)
Job Starter executes a stored proc that requests work from this table. Basically, the proc looks at the table, sees which tasks are not in progress and are overdue, marks them as "in progress" and returns them to job starter.
When task completes, a simple update is executed to tell that task completed and will be available for another cycle of work in a minute (field called Frequency controls this)
PROBLEM: I get deadlocks quiet frequently when asking this table for more work, or trying to mark entries as completed. Looks like ROWLOCK hint is not working. Do I need an indexing structure on this table?
Here is a Stored Procedure that retrieves records (usually up to 20 at a time, governed by #count parameter
CREATE PROCEDURE [dbo].[sp_GetScheduledItems]
#activity NVARCHAR (50), #count INT, #timeout INT=300, #dataCenter NVARCHAR (50)
AS
BEGIN
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
DECLARE #batchId uniqueidentifier
SELECT #batchId = NEWID()
DECLARE #result int;
DECLARE #process nvarchar(255);
BEGIN TRAN
-- Update rows
UPDATE Schedule
WITH (ROWLOCK)
SET
LastBatchId = #batchId,
LastStartedProcessingId = NEWID(),
LastStartedProcessingTime = GETUTCDATE()
WHERE
ActivityType = #activity AND
IsEnabled = 1 AND
ItemId IN (
SELECT TOP (#count) ItemId
FROM Schedule
WHERE
(LastStartedProcessingId = LastCompletedProcessingId OR LastCompletedProcessingId IS NULL OR DATEDIFF(SECOND, LastStartedProcessingTime, GETUTCDATE()) > #timeout) AND
IsEnabled = 1 AND ActivityType = #activity AND DataCenter = #dataCenter AND
(LastStartedProcessingTime IS NULL OR DATEDIFF(SECOND, LastStartedProcessingTime, GETUTCDATE()) > Frequency)
ORDER BY (DATEDIFF(SECOND, ISNULL(LastStartedProcessingTime, '1/1/2000'), GETUTCDATE()) - Frequency) DESC
)
COMMIT TRAN
-- Return the updated rows
SELECT ItemId, ParentItemId, ItemName, ParentItemName, DataCenter, LastStartedProcessingId, Frequency, LastProcessTime, ActivityType
FROM Schedule
WHERE LastBatchId = #batchId
END
GO
Here is a stored procedure that marks entries as completed (it does so one-at-a-time)
CREATE PROCEDURE [dbo].[sp_CompleteScheduledItem]
#activity NVARCHAR (50), #itemId UNIQUEIDENTIFIER, #processingId UNIQUEIDENTIFIER, #status NVARCHAR (50), #lastProcessTime DATETIME, #dataCenter NVARCHAR (50)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE Schedule WITH (ROWLOCK)
SET
LastCompletedProcessingId = LastStartedProcessingId,
LastCompletedProcessingTime = GETUTCDATE(),
LastCompletedStatus = #status,
LastProcessTime = #lastProcessTime
WHERE
ItemId = #itemId AND
LastStartedProcessingId = #processingId AND
DataCenter = #dataCenter AND
ActivityType = #activity
END
GO
Here is the table itself
CREATE TABLE [dbo].[Schedule](
[ItemId] [uniqueidentifier] NOT NULL,
[ParentItemId] [uniqueidentifier] NOT NULL,
[ActivityType] [nvarchar](50) NOT NULL,
[Frequency] [int] NOT NULL,
[LastBatchId] [uniqueidentifier] NULL,
[LastStartedProcessingId] [uniqueidentifier] NULL,
[LastStartedProcessingTime] [datetime] NULL,
[LastCompletedProcessingId] [uniqueidentifier] NULL,
[LastCompletedProcessingTime] [datetime] NULL,
[LastCompletedStatus] [nvarchar](50) NULL,
[IsEnabled] [bit] NOT NULL,
[LastProcessTime] [datetime] NULL,
[DataCenter] [nvarchar](50) NOT NULL,
[ItemName] [nvarchar](255) NOT NULL,
[ParentItemName] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_Schedule] PRIMARY KEY CLUSTERED
(
[DataCenter] ASC,
[ItemId] ASC,
[ActivityType] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)
This is a good question :-) As usual you can do many things but in your case I think we can simplify your query quite a bit. Note that the suggestion below doesn't use SERIALIZABLE isolation level which in your case in all likelihood is causing table level locks to prevent phantom read from occurring (and also makes all write access to your table, well, serialized. You also don't actually need to specify BEGIN & COMMIT TRAN as you are only issuing one statement within your transaction (although it doesn't hurt either in your case). In this example we leverage the fact that we can actually issue your update directly against the sub query (in this case in the shape of a CTE) and we can also remove your last SELECT as we can return the result set directly from the UPDATE statement.
HTH,
-Tobias
SQL Server Team
CREATE PROCEDURE [dbo].[sp_GetScheduledItems]
#activity NVARCHAR (50), #count INT, #timeout INT=300, #dataCenter NVARCHAR (50)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #batchId uniqueidentifier
SELECT #batchId = NEWID()
DECLARE #result int;
DECLARE #process nvarchar(255);
-- Update rows
WITH a AS (
SELECT TOP (#count)
*
FROM Schedule
WHERE
(LastStartedProcessingId = LastCompletedProcessingId OR LastCompletedProcessingId IS NULL OR DATEDIFF(SECOND, LastStartedProcessingTime, GETUTCDATE()) > #timeout) AND
IsEnabled = 1 AND ActivityType = #activity AND DataCenter = #dataCenter AND
(LastStartedProcessingTime IS NULL OR DATEDIFF(SECOND, LastStartedProcessingTime, GETUTCDATE()) > Frequency)
ORDER BY (DATEDIFF(SECOND, ISNULL(LastStartedProcessingTime, '1/1/2000'), GETUTCDATE()) - Frequency) DESC
)
UPDATE a SET
LastBatchId = #batchId,
LastStartedProcessingId = NEWID(),
LastStartedProcessingTime = GETUTCDATE()
OUTPUT INSERTED.ItemId, INSERTED.ParentItemId, INSERTED.ItemName, INSERTED.ParentItemName, INSERTED.DataCenter, INSERTED.LastStartedProcessingId, INSERTED.Frequency, INSERTED.LastProcessTime, INSERTED.ActivityType
END

SQL - Trigger for auto-incrementing number of signed in people

I am having a little bit of trouble with making a trigger in my SQL. I have two tables:
This one
Create table [user]
(
[id_user] Integer Identity(1,1) NOT NULL,
[id_event] Integer NULL,
[name] Nvarchar(15) NOT NULL,
[lastname] Nvarchar(25) NOT NULL,
[email] Nvarchar(50) NOT NULL, UNIQUE ([email]),
[phone] Integer NULL, UNIQUE ([phone]),
[pass] Nvarchar(50) NOT NULL,
[nick] Nvarchar(20) NOT NULL, UNIQUE ([nick]),
Primary Key ([id_user])
)
go
and this one
Create table [event]
(
[id_event] Integer Identity(1,1) NOT NULL,
[id_creator] Integer NOT NULL,
[name] Nvarchar(50) NOT NULL,
[date] Datetime NOT NULL, UNIQUE ([date]),
[city] Nvarchar(50) NOT NULL,
[street] Nvarchar(50) NOT NULL,
[zip] Integer NOT NULL,
[building_number] Integer NOT NULL,
[n_signed_people] Integer Default 0 NOT NULL Constraint [n_signed_people] Check (n_signed_people <= 20),
Primary Key ([id_akce])
)
Now I need a trigger for when I insert a new user with and id_event, or update existing one with one, to take the id_event I inserted, look in the table of events and increment the n_signed_people in a line with a coresponding id_event, until it is 20. When it is 20, it should say that the event is full. I made something like this, it is working when I add a new user with id, but now I need it to stop at 20 and say its full and also I am not sure if it will work, when I'll try to update existing user, by adding an id_event (I assume it was NULL before update).
CREATE TRIGGER TR_userSigning
ON user
AFTER INSERT
AS
BEGIN
DECLARE #idevent int;
IF (SELECT id_event FROM Inserted) IS NOT NULL --if the id_event is not empty
BEGIN
SELECT #idevent=id_event FROM Inserted; --the inserted id_event will be save in a local variable
UPDATE event SET n_signed_people = n_signed_people+1 WHERE #idevent = id_event;
END
END
go
Good evening,
I did notice some issues with your schema. I want to list the fixes I made in order.
1 - Do not use reserved words. Both user and event are reserved.
2 - Name your constraints. You will be glad they are not some random word when you want to drop one.
3 - I added a foreign key to make sure there is integrity in the relationship.
All this work was done in tempdb. Now, lets get to the fun stuff, the trigger.
-- Just playing around
use tempdb;
go
-- attendee table
if object_id('attendees') > 0
drop table attendees
go
create table attendees
(
id int identity (1,1) NOT NULL constraint pk_attendees primary key,
firstname nvarchar(15) NOT NULL,
lastname nvarchar(25) NOT NULL,
email nvarchar(50) NOT NULL constraint uc_email unique,
phone int NULL constraint uc_phone unique,
pass nvarchar(50) NOT NULL,
nick nvarchar(20) NOT NULL constraint uc_nick unique,
event_id int NOT NULL
)
go
-- events table
if object_id('events') > 0
drop table events
go
create table events
(
id int identity (1,1) NOT NULL constraint pk_events primary key,
creator int NOT NULL,
name nvarchar(50) NOT NULL,
planed_date datetime NOT NULL constraint uc_planed_date unique,
street nvarchar(50) NOT NULL,
city nvarchar(50) NOT NULL,
zip nvarchar(9) NOT NULL,
building_num int NOT NULL,
registered int
constraint df_registered default (0) NOT NULL
constraint chk_registered check (registered <= 20),
);
go
-- add some data
insert into events (creator, name, planed_date, street, city, zip, building_num)
values (1, 'new years eve', '20131231 20:00:00', 'Promenade Street', 'Providence', '02908', 99);
-- make sure their is integrity
alter table attendees add constraint [fk_event_id]
foreign key (event_id) references events (id);
I usually add all three options (insert, update, & delete). You coded for insert in the example above. But you did not code for delete.
Also, both the inserted and deleted tables can contain multiple rows. For instance, if two attendees decide to drop out, you want to minus 2 from the table.
-- create the new trigger.
CREATE TRIGGER [dbo].[trg_attendees_cnt] on [dbo].[attendees]
FOR INSERT, UPDATE, DELETE
AS
BEGIN
-- declare local variable
DECLARE #MYMSG VARCHAR(250);
-- nothing to do?
IF (##rowcount = 0) RETURN;
-- do not count rows
SET NOCOUNT ON;
-- deleted data
IF NOT EXISTS (SELECT * FROM inserted)
BEGIN
UPDATE e
SET e.registered = e.registered - c.total
FROM
[dbo].[events] e
INNER JOIN
(SELECT [event_id], count(*) as total
FROM deleted group by [event_id]) c
ON e.id = c.event_id;
RETURN;
END
-- inserted data
ELSE IF NOT EXISTS (SELECT * FROM deleted)
BEGIN
UPDATE e
SET e.registered = e.registered + c.total
FROM
[dbo].[events] e
INNER JOIN
(SELECT [event_id], count(*) as total
FROM inserted group by [event_id]) c
ON e.id = c.event_id;
RETURN;
END;
-- updated data (no counting involved)
END
GO
Like any good programmer, I need to test my work to make sure it is sound.
Lets add 21 new attendees. The check constraint should fire. This only works since the error generated by the UPDATE rollback the insert.
-- Add 21 attendees
declare #var_cnt int = 0;
declare #var_num char(2);
while (#var_cnt < 22)
begin
set #var_num = str(#var_cnt, 2, 0);
insert into attendees (firstname, lastname, email, phone, pass, nick, event_id)
values ('first-' + #var_num,
'last-' + #var_num,
'email-'+ #var_num,
5554400 + (#var_cnt),
'pass-' + #var_num,
'nick-' + #var_num, 1);
set #var_cnt = #var_cnt + 1
end
go
Last but not least, we need to test a DELETE action.
-- Delete the last row
delete from [dbo].[attendees] where id = 20;
go

Resources