SQL using a function in a trigger - sql-server

I am creating a a trigger in SQL that will insert into another table after Insert on it. However I need to fetch a Value from the table to increment to be used in the insert.
I have a AirVisionSiteLog table. On insert on the table I would like for it to insert into another SiteLog table. However in order to do this I need to fetch the last Entry Number of the Site from the SiteLog table. Then on its insert take that result and increase by one for the new Entry Number. I am new to Triggers and Functions so I am not sure how to use them correctly. I believe I have a function to retrieve and increment the Entry Number however I am not sure how to use it in the Trigger.
My Function -
CREATE FUNCTION AQB_RMS.F_GetLogEntryNumber
(#LocationID int)
RETURNS INTEGER
AS
BEGIN
DECLARE
#MaxEntry Integer,
#EntryNumber Integer
Set #MaxEntry = (Select Max(SL.EntryNumber) FROM AQB_MON.AQB_RMS.SiteLog SL
WHERE SL.LocationID = #LocationID)
SET #EntryNumber = #MaxEntry + 1
RETURN #EntryNumber
END
My Trigger and attempt to use the Function -
CREATE TRIGGER [AQB_RMS].[SiteLogCreate] on [AQB_MON].[AQB_RMS].[AirVisionSiteLog]
AFTER INSERT
AS
BEGIN
declare #entrynumber int
declare #corrected int
set #corrected = 0
INSERT INTO [AQB_MON].[AQB_RMS].[SiteLog]
([SiteLogTypeID],[LocationID],[EntryNumber],[SiteLogEntry]
,[EntryDate],[Corrected],[DATE_CREATED],[CREATED_BY])
SELECT st.SiteLogTypeID, l.LocationID,
(select AQB_RMS.F_GetLogEntryNumber from [AQB_MON].[AQB_RMS].[SiteLog] sl
where sl.LocationID = l.LocationID)
, i.SiteLogEntry, i.EntryDate, #corrected, i.DATE_CREATED, i.CREATED_BY
from inserted i
left join AQB_MON.[AQB_RMS].[SiteLogType] st on st.SiteLogType = i.SiteLogType
left join AQB_MON.AQB_RMS.Location l on l.SourceSiteID = i.SourceSiteID
END
GO

I believe that you are close.
At this part of the query in the trigger: (I set the columns vertically so that the difference is more noticable)
SELECT st.SiteLogTypeID,
l.LocationID,
(select AQB_RMS.F_GetLogEntryNumber from [AQB_MON].[AQB_RMS].[SiteLog] sl where sl.LocationID = l.LocationID),
i.SiteLogEntry,
i.EntryDate,
#corrected,
i.DATE_CREATED,
i.CREATED_BY
...should be:
SELECT st.SiteLogTypeID,
l.LocationID,
AQB_RMS.F_GetLogEntryNumber(select l.LocationID from [AQB_MON].[AQB_RMS].[SiteLog] sl where sl.LocationID = l.LocationID),
i.SiteLogEntry,
i.EntryDate,
#corrected,
i.DATE_CREATED,
i.CREATED_BY
So basically, you would call the function name with the query as the parameter, which the results thereof should only be one row with a value.
Note that in my modified example, I added the l.LocationID after the select in the function call, so I'm not sure if this is what you need, but change that to match your needs. Because I'm not sure of the exact column that you need, add a comment should there be other issues.

Related

IIF with Multiple Case Statements for Computed Column

I'm trying to add this as a Formula (Computed Column) but I'm getting an error message saying it is not valid.
Can anyone see what is wrong with the below formula?
IIF
(
select * from Config where Property = 'AutomaticExpiry' and Value = 1,
case when [ExpiryDate] IS NULL OR sysdatetimeoffset()<[ExpiryDate] then 1 else 0 end,
case when [ExpiryDate] IS NULL then 1 else 0 end
)
From BOL: ALTER TABLE computed_column_definition
computed_column_expression Is an expression that defines the value of
a computed column. A computed column is a virtual column that is not
physically stored in the table but is computed from an expression that
uses other columns in the same table. For example, a computed column
could have the definition: cost AS price * qty. The expression can be
a noncomputed column name, constant, function, variable, and any
combination of these connected by one or more operators. The
expression cannot be a subquery or include an alias data type.
Wrap the login in function. Something like this:
CREATE FUNCTION [dbo].[fn_CustomFunction]
(
#ExpireDate DATETIME2
)
RETURNS BIT
AS
BEGIN;
DECLARE #Value BIT = 0;
IF EXISTS(select * from Config where Property = 'AutomaticExpiry' and Value = 1)
BEGIN;
SET #Value = IIF (sysdatetimeoffset()< #ExpireDate, 1, 0)
RETURN #value;
END;
RETURN IIF(#ExpireDate IS NULL, 1, 0);
END;
GO
--DROP TABLE IF EXISTS dbo.TEST;
CREATE TABLE dbo.TEST
(
[ID] INT IDENTITY(1,1)
,[ExpireDate] DATETIME2
,ComputeColumn AS [dbo].[fn_CustomFunction] ([ExpireDate])
)
GO
INSERT INTO dbo.TEst (ExpireDate)
VALUES ('2019-01-01')
,('2018-01-01')
,(NULL);
SELECT *
FROM dbo.Test;
Youre trying to do something, what we're not quite sure - you've made a classic XY problem mistake.. You have some task, like "implement auto login expiry if it's on in the prefs table" and you've devised this broken solution (use a computed column/IIF) and have sought help to know why it's broken.. It's not solving the actual core problem.
In transitioning from your current state to one where you're solving the problem, you can consider:
As a view:
CREATE VIEW yourtable_withexpiry AS
SELECT
*,
CASE WHEN [ExpiryDate] IS NULL OR config.[Value] = 1 AND SysDateTimeOffset() < [ExpiryDate] THEN 1 ELSE 0 END AS IsValid
FROM
yourtable
LEFT JOIN
config
ON config.property = 'AutomaticExpiry'
As a trigger:
CREATE TRIGGER trg_withexpiry ON yourtable
AFTER INSERT OR UPDATE
AS
IF NOT EXISTS(select * from Config where Property = 'AutomaticExpiry' and Value = 1)
RETURN;
UPDATE yourtable SET [ExpiryDate] = DATE_ADD(..some current time and suitable offset here..)
FROM yourtable y INNER JOIN inserted i ON y.pk = i.pk;
END;
But honestly, you should be doing this in your front end app. It should be responsible for reading/writing session data and keeping things up to date and kicking users out if they're over time etc.. Using the database for this is, to a large extent, putting business logic/decision processing into a system that shouldn't be concerned with it..
Have your front end language implement a code that looks up user info upon some regular event (like page navigation or other activity) and refreshes the expiry date as a consequence of the activity, only if the expiry date isn't passed. For sure too keep the thing valid if the expiry is set to null if you want a way to have people active forever (or whatever)

trying to do multiple inserts in one trigger using field dependent on first insert

I am trying to manipulate a bunch of table triggers that start with an insert into one event table (TB A). This insert fires a trigger (T1) that does an insert into a secondary table (TB B). The secondary table has an insert trigger (T2) that does an update on the first table (TB A).
Pardon the confusion but basically I wanted to ensure that for the first trigger, do a second insert in the same table using the values of the first insert.
BEGIN
SET NOCOUNT ON
declare #Time int
declare #DeleteLinger int
select #Time = convert(integer,value) from Systemproperty
where [name] = 'KeepStoreLingerTimeInMinutes'
select #DeleteLinger = convert(integer,value) from Systemproperty
where [name] = 'KeepDeleteLingerTimeInMinutes'
IF (#DeleteLinger >= #Time) SET #Time=#DeleteLinger+1
insert StorageQueue
(TimeToExecute,Operation,Parameter,RuleID,GFlags)
select DateAdd(mi,#Time,getutcdate()), 1, I.ID, r.ID, r.GFlags
from inserted I, StorageRule r
where r.Active=1 and I.Active=0 and (I.OnlineCount > 0 OR
I.OnlineScreenCount > 0)
-- try and get the value that was just inserted into StorageQueue
select #SFlags=S.GFlags FROM StorageQueue S, StorageRule r, inserted I
WHERE r.ID = S.RuleID and I.ID = S.parameter
-- if a certain value do another insert into StorageQueue
If (#SFlags = 10)
INSERT INTO StorageQueue
(TimeToExecute,Operation,Parameter,RuleID,StoreFlags)
VALUES(DateAdd(mi,#Time,getutcdate()), 1, (SELECT parameter
FROM StorageQueue),2, #SFlags)
END
The problem is that there seems to be either an issue that the record is not yet inserted because the variable #SFlags is null or some other trigger accesses the values and makes changes. My question is whether this is a good way to do it. Is it possible to retrieve a value into the variable from within a trigger because it seems whichever way I try it, it doesnt work.

SQL Server trigger for multiple rows

I have a trigger:
ALTER TRIGGER [dbo].[tg_trs_uharian] ON [dbo].[master_st]
AFTER INSERT AS
BEGIN
SET NOCOUNT ON
declare #tgl_mulai varchar(10),
#tgl_selesai varchar(10),
#kdlokasi int,
#thn_harian int,
#date_diff int
declare #tugasID int;
declare #uangharian20 decimal(15,2);
declare #uangharian80 decimal(15,2);
declare #uangharian100 decimal(15,2);
select #tugasID=tugasID
from inserted
SET #thn_harian=CAST(YEAR(CONVERT(datetime, #tgl_mulai, 103)) AS INT);
SET #date_diff=((SELECT datediff(day,CONVERT([datetime],#tgl_mulai,(103)),CONVERT([datetime],#tgl_selesai,(103))))+1);
SET #uangharian100 = (
SELECT k.uh_nominal
FROM master_st m
LEFT OUTER JOIN ref_uharian AS k
ON k.uh_kdlokasi=m.kdlokasi AND k.uh_tahun=#thn_harian);
insert into trs_uangharian (tugasID, uangharian100) values
(#tugasID, #uangharian100);
END
How to make select #tugasID=tugasID from inserted applicable for multiple row inserted row table with different tugasID? It seems that my code is applicable for single row only.
It seems that #date_diff is not used
You use #thn_harian so we need #tgl_mulai, but it is NULL by default
So your INSERT statement has some problems.
I assumed that #tgl_mulai is a column of the original table master_st so I treat it as a column of "inserted" trigger internal table
ALTER TRIGGER [dbo].[tg_trs_uharian] ON [dbo].[master_st]
AFTER INSERT AS
BEGIN
SET NOCOUNT ON
insert into trs_uangharian(tugasID, uangharian100)
select
i.tugasID,
k.uh_nominal
from inserted i
left join ref_uharian AS k
ON k.uh_kdlokasi = i.kdlokasi AND
k.uh_tahun = CAST(YEAR(CONVERT(datetime, i.tgl_mulai, 103)) AS INT)
END
Please, this is a common problem among new SQL developers
SQL triggers work set-based.
Do not calculate any value using variables.
These can only store the last row's calculations in general.
Instead use Inserted and Deleted internal tables.
Your query is messed up a bit, so I can provide only general solution. Change INSERT part on something like this:
INSERT INTO trs_uangharian (tugasID, uangharian100)
SELECT i.tugasID,
k.uh_nominal
FROM inserted i
LEFT JOIN ref_uharian AS k
ON k.uh_kdlokasi=i.kdlokasi AND k.uh_tahun=#thn_harian
You should be able to replace the INSERT statement with this:
INSERT INTO trs_uangharian (tugasID, uangharian100)
SELECT
tugasID,
#uangharian100
FROM
inserted
However it looks like you also have an issue with #tgl_mulai and #tgl_selesai not being set to anything.

Inserting with Triggers

I am writing a trigger. Whenever I insert multiple values into my table, NFL.Widereceivers, I want it to automatically insert these values into another table, AFC.North. I have written a trigger, and it works to an extent:
begin
declare
#name varchar(30),
#team varchar(3),
#receptions int,
#yards int,
#touchdowns int
select #name = Name from inserted
select #team = Team from inserted
select #receptions = Receptions from inserted
select #yards = Yards from inserted
select #touchdowns = Touchdowns from inserted
if (#team = 'PIT' or #team = 'BAL' or #team = 'CIN' or #team = 'CLE')
begin
insert into AFC.North (Name, Team, Receptions, Yards, Touchdowns)
values (#name, #team, #receptions, #yards, #touchdowns);
end
end
However, this trigger does not work if I insert multiple values into NFL.Widereceivers, only the first row is inserted into AFC.North.
How can I make the trigger insert multiple rows of data?
Your trigger makes a common but unfortunate mistaken assumption that all statements that fire them will affect exactly one row. Unlike some other platforms, a trigger fires per statement, not per row. So, you need to treat inserted like a set, and therefore stop assigning individual values to variables.
INSERT AFC.North(Name,Team,Receptions,Yards,Touchdowns)
SELECT Name,Team,Receptions,Yards,Touchdowns
FROM inserted WHERE Team IN ('BAL','CIN','CLE','PIT');
You also need to decide what to do for the rows that are in inserted for the other divisions (hint: you will need an INSERT statement per division, I suspect). Of course a better design would have the division as a column, rather than have each division with its own table.
Why are you assigning values in variables in trigger, you can insert it directly in table like below. If you assign values in variables then it will store values for one row at a time. It will work fine if you insert records one by one, but not work in multiple.
insert into AFC.North (Name, Team, Receptions, Yards, Touchdowns)
select Name, Team, Receptions, Yards, Touchdowns
from inserted
where Team IN ('PIT','BAL','CIN','CLE')
Even your variable code also can be optimized in single query like
select #name = Name, #team = Team, #receptions = Receptions, #yards = Yards, #touchdowns = Touchdowns from inserted

tsql bulk update

MyTableA has several million records. On regular occasions every row in MyTableA needs to be updated with values from TheirTableA.
Unfortunately I have no control over TheirTableA and there is no field to indicate if anything in TheirTableA has changed so I either just update everything or I update based on comparing every field which could be different (not really feasible as this is a long and wide table).
Unfortunately the transaction log is ballooning doing a straight update so I wanted to chunk it by using UPDATE TOP, however, as I understand it I need some field to determine if the records in MyTableA have been updated yet or not otherwise I'll end up in an infinite loop:
declare #again as bit;
set #again = 1;
while #again = 1
begin
update top (10000) MyTableA
set my.A1 = their.A1, my.A2 = their.A2, my.A3 = their.A3
from MyTableA my
join TheirTableA their on my.Id = their.Id
if ##ROWCOUNT > 0
set #again = 1
else
set #again = 0
end
is the only way this will work if I add in a
where my.A1 <> their.A1 and my.A2 <> their.A2 and my.A3 <> their.A3
this seems like it will be horribly inefficient with many columns to compare
I'm sure I'm missing an obvious alternative?
Assuming both tables are the same structure, you can get a resultset of rows that are different using
SELECT * into #different_rows from MyTable EXCEPT select * from TheirTable and then update from that using whatever key fields are available.
Well, the first, and simplest solution, would obviously be if you could change the schema to include a timestamp for last update - and then only update the rows with a timestamp newer than your last change.
But if that is not possible, another way to go could be to use the HashBytes function, perhaps by concatenating the fields into an xml that you then compare. The caveat here is an 8kb limit (https://connect.microsoft.com/SQLServer/feedback/details/273429/hashbytes-function-should-support-large-data-types) EDIT: Once again, I have stolen code, this time from:
http://sqlblogcasts.com/blogs/tonyrogerson/archive/2009/10/21/detecting-changed-rows-in-a-trigger-using-hashbytes-and-without-eventdata-and-or-s.aspx
His example is:
select batch_id
from (
select distinct batch_id, hash_combined = hashbytes( 'sha1', combined )
from ( select batch_id,
combined =( select batch_id, batch_name, some_parm, some_parm2
from deleted c -- need old values
where c.batch_id = d.batch_id
for xml path( '' ) )
from deleted d
union all
select batch_id,
combined =( select batch_id, batch_name, some_parm, some_parm2
from some_base_table c -- need current values (could use inserted here)
where c.batch_id = d.batch_id
for xml path( '' ) )
from deleted d
) as r
) as c
group by batch_id
having count(*) > 1
A last resort (and my original suggestion) is to try Binary_Checksum? As noted in the comment, this does open the risk for a rather high collision rate.
http://msdn.microsoft.com/en-us/library/ms173784.aspx
I have stolen the following example from lessthandot.com - link to the full SQL (and other cool functions) is below.
--Data Mismatch
SELECT 'Data Mismatch', t1.au_id
FROM( SELECT BINARY_CHECKSUM(*) AS CheckSum1 ,au_id FROM pubs..authors) t1
JOIN(SELECT BINARY_CHECKSUM(*) AS CheckSum2,au_id FROM tempdb..authors2) t2 ON t1.au_id =t2.au_id
WHERE CheckSum1 <> CheckSum2
Example taken from http://wiki.lessthandot.com/index.php/Ten_SQL_Server_Functions_That_You_Have_Ignored_Until_Now
I don't know if this is better than adding where my.A1 <> their.A1 and my.A2 <> their.A2 and my.A3 <> their.A3, but I would definitely give it a try (assuming SQL Server 2005+):
declare #again as bit;
set #again = 1;
declare #idlist table (Id int);
while #again = 1
begin
update top (10000) MyTableA
set my.A1 = their.A1, my.A2 = their.A2, my.A3 = their.A3
output inserted.Id into #idlist (Id)
from MyTableA my
join TheirTableA their on my.Id = their.Id
left join #idlist i on my.Id = i.Id
where i.Id is null
/* alternatively (instead of left join + where):
where not exists (select * from #idlist where Id = my.Id) */
if ##ROWCOUNT > 0
set #again = 1
else
set #again = 0
end
That is, declare a table variable for collecting the IDs of the rows being updated and use that table for looking up (and omitting) IDs that have already been updated.
A slight variation on the method would be to use a local temporary table instead of a table variable. That way you would be able to create an index on the ID lookup table, which might result in better performance.
If schema change is not possible. How about using trigger to save off the Ids that have changed. And only import/export those rows.
Or use trigger to export it immediately.

Resources