Slowly changing dimension by date only - sql

I have a database table using the concept of data warehousing of slowly changing dimension to keep track of old versions.
So, I implemented it with the Log Trigger mechanism.
My table is like this:
CREATE TABLE "T_MyTable" (
"Id" INT NOT NULL DEFAULT NULL,
"Description" NVARCHAR(255) NULL DEFAULT NULL )
and I created an hystory table
CREATE TABLE "T_MyTableHistory" (
"Id" INT NOT NULL DEFAULT NULL,
"Description" NVARCHAR(255) NULL DEFAULT NULL,
StartDate DATETIME,
EndDate DATETIME )
Then, with a trigger like this, I get the history:
CREATE TRIGGER TableTrigger ON T_MyTable FOR DELETE, INSERT, UPDATE AS
DECLARE #NOW DATETIME
SET #NOW = CURRENT_TIMESTAMP
UPDATE T_MyTableHistory
SET EndDate = #now
FROM T_MyTableHistory, DELETED
WHERE T_MyTableHistory.Id = DELETED.Id
AND T_MyTableHistory.EndDate IS NULL
INSERT INTO T_MyTableHistory (Id, Description, StartDate, EndDate)
SELECT Id, Description, #NOW, NULL
FROM INSERTED
And, to query the history table, I use
SELECT Id, Description
FROM T_MyTableHistory
WHERE #DATE >= StartDate
AND (#DATE < EndDate OR EndDate IS NULL)
Now, my question is this: my customer will actually query the history table by date only (i.e. without the time of the day), so I need to get the record version at that date.
I thought about two options:
change the trigger (how?) to record only one "history" record per date.
keep the trigger as-is, recording all the changes in the database (including date and time), but then query the history table to get the latest version of a particular date (how?)
My feeling is that the second option is easier to implement, otherwise the trigger could become complicated (INSERT or UPDATE, depending on the presence of the history record for current date).
I'd need some help in choosing the right direction, and I'd like to have an example of the SQL query needed, in the chosen option.

I agree with your second opinion.
It is good to save date along with time. While filtering data based on date use
CONVERT() function to make sure that DATE only got compared. Also, When client enter a single date, If records have same start and end date
they will not be in your filter so use Date >= StartDate and Date <= EndDate not (>= ,<)
DECLARE #Date AS DATETIME
SET #Date = '2013-07-30'
SELECT TOP 1 Id, Description
FROM T_MyTableHistory
WHERE CONVERT(VARCHAR(20), #DATE, 103)
>= CONVERT(VARCHAR(20), StartDate, 103)
AND (CONVERT(VARCHAR(20), #DATE, 103)
< CONVERT(VARCHAR(20), EndDate, 103) OR EndDate IS NULL)
ORDER BY StartDate DESC

At the end, I came up with this query:
SELECT Id, Description
FROM T_MyTableHistory
WHERE ( DateAdd(day, datediff(day,0, #MyDate), 0) >= StartDate ) AND
(( DateAdd(day, datediff(day,0, #MyDate), 0) < EndDate ) OR ( EndDate IS NULL ))
This should be faster than varchar<->datetime conversion, and it should also be locale-independent.
By the way, this query should not need the TOP 1 and the ORDER BY clauses, since the function
DateAdd(day, datediff(day,0, #MyDate)
automatically returns the selected date, with "midnight" time (e.g. 20141215 00:00:00), so records with the same date are automatically cut out of the results.
References:
How to return the date part only from a SQL Server datetime datatype
Best approach to remove time part of datetime in SQL Server

Related

Moving T-SQL datatime column for X days overlapping UNIQUE constraint

The problem is to use DATEADD function on column with unique value constraint taking into consideration the fact that new values will overlap existing values and in fact there will be violation of constraint, because we can not have two rows with the same date.
e.g. I have table with column [SomeDate] which is of type DateTime and has constraint to be unique. I have dates starting from 2017-01-01 to 2018-01-01 and want to update records by adding 7 days to each of them.
If you update all rows, there should be no problem with a unique constraint.
Here is a quick example:
CREATE TABLE T
(
SomeDate date NOT NULL,
CONSTRAINT uc UNIQUE (SomeDate)
)
;WITH CTE AS
(
SELECT CAST(GETDATE() As Date) As TheDate
UNION ALL
SELECT CAST(DateADD(DAY, 1, TheDate) As Date)
FROM CTE
WHERE TheDate < DATEADD(DAY, 10, GETDATE())
)
INSERT INTO T(SomeDate)
SELECT TheDate
FROM CTE
UPDATE T
SET SomeDate = DATEADD(DAY, 3, SomeDate)
You can see it in action on rextester
One of the possible way is to move the dates ahead to go out of current min-max range and then bring them back taking into account how many days we want to add. Here is ready and working solution:
--Number of days we want to add to existing dates
DECLARE #daysToMoveAhead int = 7;
DECLARE #minDate datetime = (SELECT MIN([SomeDate]) from dbo.MyTable)
DECLARE #maxDate datetime = (SELECT MAX([SomeDate]) from dbo.MyTable)
DECLARE #diff int = DATEDIFF(DAY,#minDate,#maxDate)
--temporary move the dates out of existing min-max range
update dbo.MyTable set [SomeDate] = DATEADD(DAY, #diff,[SomeDate]);
--bring dates back and add as many days as we wanted
update dbo.MyTable set [SomeDate] = DATEADD(DAY, #daysToMoveAhead - #diff,[SomeDate]);

Given Date parameter is considered as Date Time parameter

I'm writing a stored procedure in sql!
I have to get records in the particular date.
I am using this query:
Declare #FromDate datetime
set #FromDate = '06/02/2014'
select * from Table where Date = #FromDate
Actually, in the Database there are 10 records in that date, but it is showing only two records because the #FromDate is taking like this 06/02/2014 00:00:00.000
If I write the query like this it means it works correctly!
select * from Table
where Date between '2014-08-28 00:00:00.000' and '2014-08-28 23:59:59.999'
How to solve this? I need to get all the records in that particular date.
Please help me !
If #FromDate is of data type datetime and Table.Date is also of data type datetime then:
Declare #FromDate datetime = '2014-06-02';
Select Table.Date
From Table
Where Table.Date >= #FromDate And Date < DateAdd(day, 1, Table.Date)
Above, we create an inclusive lower boundary (anything equal to or later than 2014-06-02) and an exclusive upper boundary (anything earlier than 2014-06-03), but with a variable defined just once. So, effectively the query checks 2014-06-02 <= FromDate < 2014-06-03.
If you convert DateTime to Nvarchar your issue would be solved.
Try this query:
Declare #Date datetime='2014-08-28 00:00:00.000'
select * from Table
where CONVERT(nvarchar(20),Date,105) = CONVERT(nvarchar(20),#Date,105)

sql subtract dates

Some of the dates in a column of dates were recorded wrong. I'd like to make a query which subtracts one day from each date IF the days are in a certain date range.
I know I'll have to use DATEADD and UPDATE, but I can't seem to figure it out. Thanks in advance.
This should do it:
UPDATE [SomeTable]
SET [DateColumn] = DATEADD(d, -1, [DateColumn])
WHERE [DateColumn] BETWEEN [Date1] AND [Date2]
Here's the MSDN doc's on the DATEADD function: http://msdn.microsoft.com/en-us/library/ms186819.aspx
When performing updates on data like this, it's always best to run a select statement first with the same criteria to ensure that you're updating the correct records. It also helps reduce the stress level of updating (especially if you're unfamiliar with SQL).
SELECT *, --Depending on what columns you would like to see, the wildcard could be replaced
DATEADD(d, -1, [DateColumn]) AS ProposedDate
FROM [SomeTable]
WHERE [DateColumn] BETWEEN [Date1] AND [Date2]
DECLARE #min_date datetime, #max_date datetime;
UPDATE yourtable
SET date_column = DATEADD(day, -1, date_column)
WHERE id IN (
SELECT id
FROM yourtable
WHERE date_column BETWEEN #min_date AND #max_date
);
You'll have to set appropriate values for #min_date and #max_date.
UPDATE MyTable
SET DateField = DATEADD(Day, -1, DateField)
WHERE Datefield BETWEEN '1/1/2011' AND '2/1/2011'
use UPDATE
set the data = ( do the date math )
WHERE the date is in the range you want
and the key is the row you want.

SQL Stored Procedure to get Date and Time

I wish to create a stored procedure which can retrieve the datetime less than or greater than current sys date.. in my table,startdate and enddate has the value as 'datetime'
How do I get details between startdate and enddate in SQL stored procedure?
thanks in advance
eg:
SELECT *
FROM MyTable
WHERE DATEDIFF ('d',mydatefield ,getdate() ) < 3
gets within 3 days
Considering this table definitionCREATE TABLE [dbo].[Dates](
[StartDate] [datetime] NOT NULL,
[EndDate] [datetime] NOT NULL
)
I assume that if you pass a date you want to know which rows satisfy the condition: startDate < date < EndDate. If this is the case you can use the query: select *
from Dates
where convert(datetime, '20/12/2010', 103) between StartDate and EndDate;
A stored procedure could look like: ALTER PROCEDURE [dbo].[GetDataWithinRange]
#p_Date datetime
AS
BEGIN
SELECT *
from Dates
where #p_Date between StartDate and EndDate;
END
It sounds like you're trying to filter data in a table based on a date range. If this is the case (I'm having some trouble understanding your question), you'd do something like this:
select *
from MyTable m
where m.Date between #DateFrom and #DateTo
Now, I'm assuming your filtering dates are put into the variables #DateFrom and #DateTo.
There are two things:
1> To get todays date we can write
SET #today_date = GETTDDT();
2> To get Current time we can us ethe following query:
SET #today_time = (SELECT
digits(cast(hour(current time) as decimal(2,0)))||
digits(cast(minute(current time) as decimal(2,0)))||
digits(cast(second(current time) as decimal(2,0)))
FROM sysibm/sysdummy1);

How to Determine Values for Missing Months based on Data of Previous Months in T-SQL

I have a set of transactions occurring at specific points in time:
CREATE TABLE Transactions (
TransactionDate Date NOT NULL,
TransactionValue Integer NOT NULL
)
The data might be:
INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('1/1/2009', 1)
INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('3/1/2009', 2)
INSERT INTO Transactions (TransactionDate, TransactionValue)
VALUES ('6/1/2009', 3)
Assuming that the TransactionValue sets some kind of level, I need to know what the level was between the transactions. I need this in the context of a set of T-SQL queries, so it would be best if I could get a result set like this:
Month Value
1/2009 1
2/2009 1
3/2009 2
4/2009 2
5/2009 2
6/2009 3
Note how, for each month, we either get the value specified in the transaction, or we get the most recent non-null value.
My problem is that I have little idea how to do this! I'm only an "intermediate" level SQL Developer, and I don't remember ever seeing anything like this before. Naturally, I could create the data I want in a program, or using cursors, but I'd like to know if there's a better, set-oriented way to do this.
I'm using SQL Server 2008, so if any of the new features will help, I'd like to hear about it.
P.S. If anyone can think of a better way to state this question, or even a better subject line, I'd greatly appreciate it. It took me quite a while to decide that "spread", while lame, was the best I could come up with. "Smear" sounded worse.
I'd start by building a Numbers table holding sequential integers from 1 to a million or so. They come in really handy once you get the hang of it.
For example, here is how to get the 1st of every month in 2008:
select firstOfMonth = dateadd( month, n - 1, '1/1/2008')
from Numbers
where n <= 12;
Now, you can put that together using OUTER APPLY to find the most recent transaction for each date like so:
with Dates as (
select firstOfMonth = dateadd( month, n - 1, '1/1/2008')
from Numbers
where n <= 12
)
select d.firstOfMonth, t.TransactionValue
from Dates d
outer apply (
select top 1 TransactionValue
from Transactions
where TransactionDate <= d.firstOfMonth
order by TransactionDate desc
) t;
This should give you what you're looking for, but you might have to Google around a little to find the best way to create the Numbers table.
here's what i came up with
declare #Transactions table (TransactionDate datetime, TransactionValue int)
declare #MinDate datetime
declare #MaxDate datetime
declare #iDate datetime
declare #Month int
declare #count int
declare #i int
declare #PrevLvl int
insert into #Transactions (TransactionDate, TransactionValue)
select '1/1/09',1
insert into #Transactions (TransactionDate, TransactionValue)
select '3/1/09',2
insert into #Transactions (TransactionDate, TransactionValue)
select '5/1/09',3
select #MinDate = min(TransactionDate) from #Transactions
select #MaxDate = max(TransactionDate) from #Transactions
set #count=datediff(mm,#MinDate,#MaxDate)
set #i=1
set #iDate=#MinDate
while (#i<=#count)
begin
set #iDate=dateadd(mm,1,#iDate)
if (select count(*) from #Transactions where TransactionDate=#iDate) < 1
begin
select #PrevLvl = TransactionValue from #Transactions where TransactionDate=dateadd(mm,-1,#iDate)
insert into #Transactions (TransactionDate, TransactionValue)
select #iDate, #prevLvl
end
set #i=#i+1
end
select *
from #Transactions
order by TransactionDate
To do it in a set-based way, you need sets for all of your data or information. In this case there's the overlooked data of "What months are there?" It's very useful to have a "Calendar" table as well as a "Number" table in databases as utility tables.
Here's a solution using one of these methods. The first bit of code sets up your calendar table. You can fill it using a cursor or manually or whatever and you can limit it to whatever date range is needed for your business (back to 1900-01-01 or just back to 1970-01-01 and as far into the future as you want). You can also add any other columns that are useful for your business.
CREATE TABLE dbo.Calendar
(
date DATETIME NOT NULL,
is_holiday BIT NOT NULL,
CONSTRAINT PK_Calendar PRIMARY KEY CLUSTERED (date)
)
INSERT INTO dbo.Calendar (date, is_holiday) VALUES ('2009-01-01', 1) -- New Year
INSERT INTO dbo.Calendar (date, is_holiday) VALUES ('2009-01-02', 1)
...
Now, using this table your question becomes trivial:
SELECT
CAST(MONTH(date) AS VARCHAR) + '/' + CAST(YEAR(date) AS VARCHAR) AS [Month],
T1.TransactionValue AS [Value]
FROM
dbo.Calendar C
LEFT OUTER JOIN dbo.Transactions T1 ON
T1.TransactionDate <= C.date
LEFT OUTER JOIN dbo.Transactions T2 ON
T2.TransactionDate > T1.TransactionDate AND
T2.TransactionDate <= C.date
WHERE
DAY(C.date) = 1 AND
T2.TransactionDate IS NULL AND
C.date BETWEEN '2009-01-01' AND '2009-12-31' -- You can use whatever range you want
John Gibb posted a fine answer, already accepted, but I wanted to expand on it a bit to:
eliminate the one year limitation,
expose the date range in a more
explicit manner, and
eliminate the need for a separate
numbers table.
This slight variation uses a recursive common table expression to establish the set of Dates representing the first of each month on or after from and to dates defined in DateRange. Note the use of the MAXRECURSION option to prevent a stack overflow (!); adjust as necessary to accommodate the maximum number of months expected. Also, consider adding alternative Dates assembly logic to support weeks, quarters, even day-to-day.
with
DateRange(FromDate, ToDate) as (
select
Cast('11/1/2008' as DateTime),
Cast('2/15/2010' as DateTime)
),
Dates(Date) as (
select
Case Day(FromDate)
When 1 Then FromDate
Else DateAdd(month, 1, DateAdd(month, ((Year(FromDate)-1900)*12)+Month(FromDate)-1, 0))
End
from DateRange
union all
select DateAdd(month, 1, Date)
from Dates
where Date < (select ToDate from DateRange)
)
select
d.Date, t.TransactionValue
from Dates d
outer apply (
select top 1 TransactionValue
from Transactions
where TransactionDate <= d.Date
order by TransactionDate desc
) t
option (maxrecursion 120);
If you do this type of analysis often, you might be interested in this SQL Server function I put together for exactly this purpose:
if exists (select * from dbo.sysobjects where name = 'fn_daterange') drop function fn_daterange;
go
create function fn_daterange
(
#MinDate as datetime,
#MaxDate as datetime,
#intval as datetime
)
returns table
--**************************************************************************
-- Procedure: fn_daterange()
-- Author: Ron Savage
-- Date: 12/16/2008
--
-- Description:
-- This function takes a starting and ending date and an interval, then
-- returns a table of all the dates in that range at the specified interval.
--
-- Change History:
-- Date Init. Description
-- 12/16/2008 RS Created.
-- **************************************************************************
as
return
WITH times (startdate, enddate, intervl) AS
(
SELECT #MinDate as startdate, #MinDate + #intval - .0000001 as enddate, #intval as intervl
UNION ALL
SELECT startdate + intervl as startdate, enddate + intervl as enddate, intervl as intervl
FROM times
WHERE startdate + intervl <= #MaxDate
)
select startdate, enddate from times;
go
it was an answer to this question, which also has some sample output from it.
I don't have access to BOL from my phone so this is a rough guide...
First, you need to generate the missing rows for the months you have no data. You can either use a OUTER join to a fixed table or temp table with the timespan you want or from a programmatically created dataset (stored proc or suchlike)
Second, you should look at the new SQL 2008 'analytic' functions, like MAX(value) OVER ( partition clause ) to get the previous value.
(I KNOW Oracle can do this 'cause I needed it to calculate compounded interest calcs between transaction dates - same problem really)
Hope this points you in the right direction...
(Avoid throwing it into a temp table and cursoring over it. Too crude!!!)
-----Alternative way------
select
d.firstOfMonth,
MONTH(d.firstOfMonth) as Mon,
YEAR(d.firstOfMonth) as Yr,
t.TransactionValue
from (
select
dateadd( month, inMonths - 1, '1/1/2009') as firstOfMonth
from (
values (1), (2), (3), (4), (5), (7), (8), (9), (10), (11), (12)
) Dates(inMonths)
) d
outer apply (
select top 1 TransactionValue
from Transactions
where TransactionDate <= d.firstOfMonth
order by TransactionDate desc
) t