SQL Assign datediff to table column in insert trigger - sql

Table
CREATE TABLE CurrentApplication
(
StartDate datetime NOT NULL,
EndDate datetime NOT NULL,
NoOfDays integer,
StaffID integer NOT NULL,
AppStatus varchar(30) NOT NULL DEFAULT 'PENDING'
)
Insert Trigger
CREATE TRIGGER InsertNoOfDays ON CurrentApplication
AFTER INSERT
BEGIN
INSERT INTO CurrentApplication
NoOfDays AS Datediff(day, StartDate, EndDate)
END
I have NoOfDays column which should hold the DateDiff between StartDate and EndDate and the value should be inserted whenever a new record is inserted into the table. How do I write the trigger and do it? I've tried but my trigger doesn't work. Thanks!

One method is to use an INSTEAD OF trigger:
CREATE TRIGGER InsertNoOfDays ON CurrentApplication
INSTEAD OF INSERT AS
BEGIN
INSERT INTO CurrentApplication( . . ., NoOfDays)
SELECT . . .,
Datediff(day, StartDate, EndDate)
FROM inserted;
END;

One thing you can do is add a computed column instead of actual column as NoOfdays.
There are benefits and drawbacks in adding a computed column.
You don't have have a trigger and calculate it. Saves on writes.
This also means sometimes you cannot index on that column and reads may have an impact.
Here is the documentation from Microsoft on computed columns (documentation says it is only on SQL 2016 onwards):
Specify Computed Columns in Tables
But, here is sample in SQL 2014 that worked for me (I think it works in SQL 2012 also)
use tempdb
GO
/*
Wors in version:
Microsoft SQL Server 2014 - 12.0.2000.8 (X64)
Standard Edition (64-bit
*/
IF OBJECT_ID('tempdb.dbo.CurrentApplication') IS NULL
CREATE TABLE dbo.CurrentApplication
(
StartDate datetime NOT NULL,
EndDate datetime NOT NULL,
NoOfDays AS (datediff(dd, StartDate, EndDate)) PERSISTED,
StaffID integer NOT NULL,
)
INSERT INTO dbo.CurrentApplication(StartDate, EndDate, StaffId)
SELECT TOP 10
StartDate = DATEADD(dd, object_id, '1/1/2017')
,EndDate = DATEADD(dd, object_id*object_id, '1/1/2017')
,StaffId = object_id
FROM sys.objects
WHERE object_id < 300
order by Object_id
SELECT * FROM CurrentApplication
IF OBJECT_ID('tempdb.dbo.CurrentApplication') IS NOT NULL
DROP TABLE dbo.CurrentApplication

Since not eligible to add comments, adding as a new answer.
Adding to #Gordon Linoff's answer, just substituted column names instead of periods to see if that caused the error you getting. I didnt get that error. Not sure if there is anything with SQL 2012.
But, this works (in SQL 2014):
CREATE TRIGGER InsertNoOfDays ON dbo.CurrentApplication
INSTEAD OF INSERT AS
BEGIN
INSERT INTO CurrentApplication(StartDate, EndDate, NoOfDays, StaffID)
SELECT StartDate
,EndDate
,Datediff(dd, StartDate, EndDate)
,StaffId
FROM inserted as i;
END;

Related

Checking if date in table B is between date in Table A before inserting SQLite

I have a table called Project with start and end dates. I also have a table called Plan which have its own start and end dates column. But I some way to validate that the Plans start/end date is between the matching Project start/end date.
I dont know if its appropriate to add a check when I create the table or when I insert rows to the Plan table. So far I have tried both with no luck.
The following code gives me an error message no such column. Does anyone know how to fix this problem? Thanks in advance.
https://i.stack.imgur.com/UC5Ai.png
%%sql
DROP TABLE IF EXISTS Plan;
CREATE TABLE Plan (
pID varchar(255) NOT NULL UNIQUE,
projectID varchar(255) NOT NULL UNIQUE,
name varchar(255) NOT NULL DEFAULT ' ',
startDate DATE NOT NULL DEFAULT '2000-12-31',
endDate DARE NOT NULL DEFAULT '2000-12-31'
CHECK (JulianDay(startDate) <= JulianDay(endDate) AND (startDate >= Project.startDate) AND
(endDate <= Project.endDate)),
PRIMARY KEY (pID, projectID),
FOREIGN KEY (projectID) REFERENCES Project(projectID)
);
You need a BEFORE INSERT trigger:
CREATE TRIGGER trg_ins_plan BEFORE INSERT ON Plan
BEGIN
SELECT
CASE
WHEN NOT EXISTS (
SELECT 1
FROM Project p
WHERE p.projectID = NEW.projectID AND p.startDate <= NEW.startDate AND p.endDate >= NEW.endDate
)
THEN RAISE(ABORT, 'Invalid dates')
END;
END;
and a BEFORE UPDATE trigger:
CREATE TRIGGER trg_upd_plan BEFORE UPDATE ON Plan
BEGIN
SELECT
CASE
WHEN NOT EXISTS (
SELECT 1
FROM Project p
WHERE p.projectID = NEW.projectID AND p.startDate <= NEW.startDate AND p.endDate >= NEW.endDate
)
THEN RAISE(ABORT, 'Invalid dates')
END;
END;
Also, change the CHECK constraint for the dates to:
CHECK (JulianDay(startDate) <= JulianDay(endDate))
or just:
CHECK (startDate <= endDate)
See the demo.

SCD Type 2 - Handling Intraday changes?

I have a merge statement that builds my SCD type 2 table each night. This table must house all historical changes made in the source system and create a new row with the date from/date to columns populated along with the "islatest" flag. I have come across an issue today that I am not really sure how to handle.
There looks to have been multiple changes to the source table within a 24 hour period.
ID Code PAN EnterDate Cost Created
16155 1012401593331 ENRD 2015-11-05 7706.3 2021-08-17 14:34
16155 1012401593331 ENRD 2015-11-05 8584.4 2021-08-17 16:33
I use a basic merge statement to identify my changes however what would be the best approach to ensure all changes get picked up correctly? The above is giving me an error as it's trying to insert/update multiple rows with the same value
DECLARE #DateNow DATETIME = Getdate()
IF Object_id('tempdb..#meteridinsert') IS NOT NULL
DROP TABLE #meteridinsert;
CREATE TABLE #meteridinsert
(
meterid INT,
change VARCHAR(10)
);
MERGE
INTO [DIM].[Meters] AS target
using stg_meters AS source
ON target.[ID] = source.[ID]
AND target.latest=1
WHEN matched THEN
UPDATE
SET target.islatest = 0,
target.todate = #Datenow
WHEN NOT matched BY target THEN
INSERT
(
id,
code,
pan,
enterdate,
cost,
created,
[FromDate] ,
[ToDate] ,
[IsLatest]
)
VALUES
(
source.id,
source.code ,
source.pan ,
source.enterdate ,
source.cost ,
source.created ,
#Datenow ,
NULL ,
1
)
output source.id,
$action
INTO #meteridinsert;INSERT INTO [DIM].[Meters]
(
[id] ,
[code] ,
[pan] ,
[enterdate] ,
[cost] ,
[created] ,
[FromDate] ,
[ToDate] ,
[IsLatest]
)
SELECT ([id] ,[code] ,[pan] ,[enterdate] ,[cost] ,[created] , #DateNow ,NULL ,1 FROM stg_meters a
INNER JOIN #meteridinsert cid
ON a.id = cid.meterid
AND cid.change = 'UPDATE'
Maybe you can do it using merge statement, but I would prefer to use typicall update and insert approach in order to make it easier to understand (also I am not sure that merge allows you to use the same source record for update and insert...)
First of all I create the table dimscd2 to represent your dimension table
create table dimscd2
(naturalkey int, descr varchar(100), startdate datetime, enddate datetime)
And then I insert some records...
insert into dimscd2 values
(1,'A','2019-01-12 00:00:00.000', '2020-01-01 00:00:00.000'),
(1,'B','2020-01-01 00:00:00.000', NULL)
As you can see, the "current" is the one with descr='B' because it has an enddate NULL (I do recommend you to use surrogate keys for each record... This is just an incremental key for each record of your dimension, and the fact table must be linked with this surrogate key in order to reflect the status of the fact in the moment when happened).
Then, I have created some dummy data to represent the source data with the changes for the same natural key
-- new data (src_data)
select 1 as naturalkey,'C' as descr, cast('2020-01-02 00:00:00.000' as datetime) as dt into src_data
union all
select 1 as naturalkey,'D' as descr, cast('2020-01-03 00:00:00.000' as datetime) as dt
After that, I have created a temp table (##tmp) with this query to set the enddate for each record:
-- tmp table
select naturalkey, descr, dt,
lead(dt,1,0) over (partition by naturalkey order by dt) enddate,
row_number() over (partition by naturalkey order by dt) rn
into ##tmp
from src_data
The LEAD function takes the next start date for the same natural key, ordered by date (dt).
The ROW_NUMBER marks with 1 the oldest record in the source data for the natural key in the dimension.
Then, I proceed to close the "current" record using update
update d
set enddate = t.dt
from dimscd2 d
join ##tmp t
on d.naturalkey = t.naturalkey
and d.enddate is null
and t.rn = 1
And finally I add the new source data to the dimension with insert
insert into dimscd2
select naturalkey, descr, dt,
case enddate when '1900-00-00' then null else enddate end
from ##tmp
Final result is obtained with the query:
select * from dimscd2
You can test on this db<>fiddle

Optimize this query without using not exist repeatably, is there a better way to write this query?

For example I have three table where say DataTable1, DataTable2 and DataTable3
and need to filter it from DataRange table, every time I have used NOT exist as shown below,
Is there a better way to write this.
Temp table to hold some daterange which is used for fiter:
Declare #DateRangeTable as Table(
StartDate datetime,
EndDate datetime
)
Some temp table which will hold data on which we need to apply date range filter
INSERT INTO #DateRangeTable values
('07/01/2020','07/04/2020'),
('07/06/2020','07/08/2020');
/*Table 1 which will hold some data*/
Declare #DataTable1 as Table(
Id numeric,
Date datetime
)
INSERT INTO #DataTable1 values
(1,'07/09/2020'),
(2,'07/06/2020');
Declare #DataTable2 as Table(
Id numeric,
Date datetime
)
INSERT INTO #DataTable2 values
(1,'07/10/2020'),
(2,'07/06/2020');
Declare #DataTable3 as Table(
Id numeric,
Date datetime
)
INSERT INTO #DataTable3 values
(1,'07/11/2020'),
(2,'07/06/2020');
Now I want to filter data based on DateRange table, here I need some optimized way so that i don't have to use not exists mutiple times, In real senario, I have mutiple tables where I have to filter based on the daterange table.
Select * from #DataTable1
where NOT EXISTS(
Select 1 from #DateRangeTable
where [Date] between StartDate and EndDate
)
Select * from #DataTable2
where NOT EXISTS(
Select 1 from #DateRangeTable
where [Date] between StartDate and EndDate
)
Select * from #DataTable3
where NOT EXISTS(
Select 1 from #DateRangeTable
where [Date] between StartDate and EndDate
)
Instead of using NOT EXISTS you could join the date range table:
SELECT dt.*
FROM #DataTable1 dt
LEFT JOIN #DateRangeTable dr ON dt.[Date] BETWEEN dr.StartDate and dr.EndDate
WHERE dr.StartDate IS NULL
It may perform better on large tables but you would have to compare the execution plans and make sure you have indexes on the date columns.
I would write the same query... but if you can change table structure I would try to improve performance adding two columns to specify the month as an integer (I suppose is the first couple of figures).
Obviously you have to test with your data and compare the timings.
Declare #DateRangeTable as Table(
StartDate datetime,
EndDate datetime,
StartMonth tinyint,
EndMonth tinyint
)
INSERT INTO #DateRangeTable values
('07/01/2020','07/04/2020', 7, 7),
('07/06/2020','07/08/2020', 7, 7),
('07/25/2020','08/02/2020', 7, 8); // (another record with different months)
Now your queries can use the new column to try to reduce comparisons (is a tinyint, sql server can partition records if you define a secondary index for StartMonth and EndMonth):
Select * from #DataTable1
where NOT EXISTS(
Select 1 from #DateRangeTable
where (DATEPART('month', [Date]) between StartMonth and EndMonth)
and ([Date] between StartDate and EndDate)
)

Trigger which counts datediff

Hello for hobby purposes i am trying to create a C# application with a MS SQL Server database which reassembles a hotel system. I am now trying to create a SQL trigger which calculates a datedifference. A reservation may not be longer than 6 weeks(42 days). However, my trigger goes off even when placing reservations which have a datedifference lower than 42 days, even if the difference is 1 day. so I am not sure what I am doing wrong.
My trigger:
create trigger trigger_reservation
on reservation
after update, insert
as
if exists
(
select reservationid, DATEDIFF(dd,Startdate,Enddate)
from reservation
group by reservationid, enddate, startdate
having DATEDIFF(dd,Startdate,Enddate) > 42
)
begin
raiserror('Error: Reservation may not be longer than 6 weeks',16, 1)
rollback transaction
end
Triggers are expensive to run and maintain. This type of check can be accomplished by a simple CHECK CONSTRAINT
CREATE TABLE reservation (
reservationid INT,
startdate DATE,
enddate DATE,
-- ...
CONSTRAINT reservation_dates_ck
CHECK(DATEDIFF(dd, startdate, enddate) < 43)
)
Here is a dbfiddle demo
And here's how you go about doing it with a trigger
CREATE TRIGGER trigger_reservation
ON reservation AFTER UPDATE, INSERT
AS
IF EXISTS (
SELECT *
FROM inserted
WHERE DATEDIFF(dd, startdate, enddate) > 42
)
BEGIN
RAISERROR ('Error: Reservation may not be longer than 6 weeks', 16, 1);
ROLLBACK TRANSACTION;
RETURN
END;
Here is a dbfiddle demo

SQL Server 2012 Insert DATEDIFF into column trigger whenever a new record is inserted

Table
CREATE TABLE CurrentApplication
(
StartDate datetime NOT NULL,
EndDate datetime NOT NULL,
NoOfDays integer,
StaffID integer NOT NULL,
AppStatus varchar(30) NOT NULL DEFAULT 'PENDING'
)
Trigger
CREATE TRIGGER InsertNoOfDays ON CurrentApplication
AFTER INSERT
AS
BEGIN
DECLARE #temp INT
SELECT #temp = DATEDIFF(day, EndDate, StartDate)
FROM inserted
INSERT INTO CurrentApplication(NoOfDays) VALUES (#temp)
--SELECT StaffID = inserted.StaffID
--FROM inserted
-- INSERT INTO CurrentApplication(NoOfDays)
-- SELECT Datediff(day, EndDate, StartDate)
-- FROM inserted;
END
Error message:
Msg 515, Level 16, State 2, Procedure InsertNoOfDays, Line 10
Cannot insert the value NULL into column 'StartDate', table
'StaffPortalDB.dbo.CurrentApplication'; column does not allow nulls.
INSERT fails. The statement has been terminated.
What I'm trying to do is I have a table CurrentApplication and I want the NoOfDays column to automatically be populated whenever a user inserts a new row, with the date difference of start day and end day.
IF Sql server
Try inserting some default or dummy values,since its not null column
Some thing like this:
CREATE TRIGGER InsertNoOfDays ON CurrentApplication
AFTER INSERT
AS
BEGIN
DECLARE #temp INT
SELECT #temp = coalesce(DATEDIFF(day, EndDate, StartDate),0) --Default 0
FROM inserted
INSERT INTO CurrentApplication(NoOfDays) VALUES (#temp)
--SELECT StaffID = inserted.StaffID
--FROM inserted
-- INSERT INTO CurrentApplication(NoOfDays)
-- SELECT Datediff(day, EndDate, StartDate)
-- FROM inserted;
END
It's because your Insert statement is attempting to insert a record but isn't inserting any values into the columns that cannot be empty (StartDate, EndDate, StaffID, AppStatus). For this insert to succeed you need to either change the INSERT statement to insert a value into these columns or change the table schema to allow NULL values.