I have a table stored on a SQL Server 2008, that associate a value to a date range.
DateFrom DateTo Value
2012-01-01 2012-02-01 10
2012-02-02 2012-02-15 15
The application that deal with this table, can insert a new range between the existings.
For example, If i insert
DateFrom DateTo Value
2012-02-07 2012-02-10 12
The result must be
DateFrom DateTo Value
2012-01-01 2012-02-01 10
2012-02-02 2012-02-06 15
2012-02-07 2012-02-10 12
2012-02-11 2012-02-15 15
I can do that programmatically from the application, but I wonder if there is some fast SQL statement that make me able to set the data values by referencing other row's field and performing data operation on it.
A MUST requirement is that the date range must represent a time sequence, two range cannot span each other.
I've written an example based on the example I gave you in a comment, it may do what you want. Since, in general terms, there might be multiple rows to insert/delete, it's best to define them all separately, then use a MERGE to perform the overall change.
I've also assumed that it's okay to delete/insert to achieve the splitting - you can't update and produce 2 rows from 1, so you'd always have to do an insert, and the symmetry is cleaner if I do both:
declare #T table (DateFrom datetime2, DateTo datetime2,Value int)
insert into #T(DateFrom , DateTo , Value) VALUES
('20120101', '20120201', 10),
('20120202', '20120206', 15),
('20120207', '20120210', 12),
('20120211', '20120215', 15)
select * from #t order by DateFrom
declare #NewFrom datetime2 = '20120205'
declare #NewTo datetime2 = '20120208'
declare #NewValue int = 8
--We need to identify a) rows to delete, b) new sliced rows to create, and c) the new row itself
;With AlteredRows as (
select #NewFrom as DateFrom,#NewTo as DateTo,#NewValue as Value,1 as toInsert
union all
select DateFrom,DATEADD(day,-1,#NewFrom),Value,1 from #t where #NewFrom between DATEADD(day,1,DateFrom) and DateTo
union all
select DATEADD(day,1,#NewTo),DateTo,Value,1 from #t where #NewTo between DateFrom and DATEADD(day,-1,DateTo)
union all
select DateFrom,DateTo,0,0 from #t where DateTo > #NewFrom and DateFrom < #NewTo
)
merge into #t t using AlteredRows ar on t.DateFrom = ar.DateFrom and t.DateTo = ar.DateTo
when matched and toInsert=0 then delete
when not matched then insert (DateFrom,DateTo,Value) values (ar.DateFrom,ar.DateTo,ar.Value);
select * from #t order by DateFrom
It may be possible to re-write the CTE so that it's a single scan of #t - but I only think it's worth doing that if performance is critical.
I've had similar problems in the past, and found that if the range needs to be continuous the best approach is to do away with the End Date of the range, and calculate this as the Next start date. Then if needs be create a view as follows:
SELECT FromDate,
( SELECT DATEADD(DAY, -1, MIN(DateFrom))
FROM YourTable b
WHERE b.FromDate > a.FromDate
) [ToDate],
Value
FROM YourTable a
This ensures that 2 ranges can never cross, however does not necessarily ensure no work is required upon insert to get the desired result, but it should be more maintainable and have less scope for error than storing both the start and end date.
ADDENDUM
Once I had written out all of the below I realised it does not improve maintainability that much to do away with the DateTo Field, it still requires a fair amount of code for the validation, but here's how I would do it anyway.
DECLARE #T table (DateFrom DATE, Value INT)
INSERT INTO #T VALUES ('20120101', 10), ('20120202', 15), ('20120207', 12), ('20120211', 15)
DECLARE #NewFrom DATE = '20120209',
#NewTo DATE = '20120210',
#NewValue INT = 8
-- SHOW INITIAL VALUES FOR DEMONSTATIVE PURPOSES --
SELECT DateFrom,
ISNULL(( SELECT DATEADD(DAY, -1, MIN(DateFrom))
FROM #t b
WHERE b.DateFrom > a.DateFrom
), CAST(GETDATE() AS DATE)) [DateTo],
Value
FROM #t a
ORDER BY DateFrom
;WITH CTE AS
( SELECT DateFrom,
( SELECT DATEADD(DAY, -1, MIN(DateFrom))
FROM #t b
WHERE b.DateFrom > a.DateFrom
) [DateTo],
Value
FROM #t a
),
MergeCTE AS
( SELECT #NewFrom [DateFrom], #NewValue [Value], 'INSERT' [RowAction]
WHERE #NewFrom < #NewTo -- ENSURE A VALID RANGE IS ENTERED
UNION ALL
-- INSERT A ROW WHERE THE NEW DATE TO SLICES AN EXISTING PERIOD
SELECT DATEADD(DAY, 1, #NewTo), Value, 'INSERT'
FROM CTE
WHERE #NewTo BETWEEN DateFrom AND DateTo
UNION ALL
-- DELETE ALL ENTRIES STARTING WITHIN THE DEFINED PERIOD
SELECT DateFrom, Value, 'DELETE'
FROM CTE
WHERE DateFrom BETWEEN #NewFrom AND #NewTo
)
MERGE INTO #t t USING MergeCTE c ON t.DateFrom = c.DateFrom AND t.Value = c.Value
WHEN MATCHED AND RowAction = 'DELETE' THEN DELETE
WHEN NOT MATCHED THEN INSERT VALUES (c.DateFrom, c.Value);
SELECT DateFrom,
ISNULL(( SELECT DATEADD(DAY, -1, MIN(DateFrom))
FROM #t b
WHERE b.DateFrom > a.DateFrom
), CAST(GETDATE() AS DATE)) [DateTo],
Value
FROM #t a
ORDER BY DateFrom
You can use a cursor to get each row from the table at a time and aftwerwards do the necessary calculations.
If NewDateFrom >= RowDateFrom and NewDateFrom <= RowDateTo ...
Check this article to see how to make a cursor.
Related
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]);
I have a SQL Server table:
CREATE TABLE tblExample
(
ID int,
Name nvarchar(256),
Date datetime,
IsAnual bit
)
This is a simplified example.
Now I scan the next 30 days from GETDATE(). If there is result, I insert information into another table:
WHERE DATEDIFF(dd, GETDATE(), Date) <= 30
Up to now there is no problem. But
WHERE IsAnual = 1
I must take into account their continuations. How can I do this?
Suppose that GETDATE() is 2013-10-22 and the column contains 2013-10-30, there is not problem. What if GETDATE() is 2014-10-28 and column contains 2013-10-30 AND IsAnual = 1?
Updated:
I found solution. I used recursive query.
CREATE TABLE tblExample
(
ID int IDENTITY(1,1) PRIMARY KEY NOT NULL,
Name nvarchar(256),
Date datetime,
IsAnual bit
)
And inserted some rows:
INSERT INTO tblExample
(Name, Date, IsAnual)
VALUES
('A', '2012-11-01', 1),
('B', '2013-11-01', 0),
('C', '2013-01-01', 1)
And final section is properly working query:
WITH TempTable AS
(
SELECT
e.ID,
e.Name,
e.Date,
e.IsAnual
FROM tblExample AS e
UNION ALL
SELECT
e.ID,
e.Name,
DATEADD(yy, 1, t.Date),
e.IsAnual
FROM tblExample AS e
INNER JOIN TempTable AS t
ON e.ID = t.ID
WHERE e.IsAnual = 1
AND DATEDIFF(yy, t.Date, DATEADD(yy, 1, GETDATE())) > 0
)
SELECT
*
FROM TempTable
WHERE DATEDIFF(dd, GETDATE(), Date) BETWEEN 0 AND 30
Results here:
14 B 01.11.2013 False
13 A 01.11.2013 True
WHERE DATEDIFF(dd, GETDATE(),
CASE
WHEN IsAnnual = 0 THEN Date
WHEN IsAnnual = 1 THEN DATEADD(year,DATEDIFF(year,Date,GETDATE()),Date)
END
) <= 30
The expression DATEADD(year,DATEDIFF(year,Date,GETDATE()),Date) will give you the date provided in the Date column but with its year set to the current year.
I think that's what you were asking for.
It should be noted, however, that the above will not be able to leverage any indexes on Date, so may not provide the absolute best performance on a large table.
(My initial attempt had the CASE expression incorrect, but it's hopefully correct now)
WHERE DATEDIFF( DAY ,DATEADD(YEAR,(1753 -DATEPART(YEAR ,GETDATE())) *IsAnual ,GETDATE()) ,DATEADD(YEAR ,(1753 -DATEPART(YEAR ,Date)) *IsAnual ,Date)) BETWEEN 0 AND 30
This question already has an answer here:
CTE looping query
(1 answer)
Closed 8 years ago.
situation: I got 2 tables. One where there are 2 fields StartDate & EndDate. And one table where there is only one field date. So when you got 3 days between Start- and Enddate. He must insert 3 rows in the new table.
I have next code and it insert perfect my line in the table Availability.
with View_Solidnet_Training as
(
select cast('2013-04-09' as datetime) DateValue
union all
select DateValue + 1
from View_Solidnet_Training
where DateValue + 1 <= cast('2013-04-11' as datetime)
)
insert into OBJ_Availability
select 34, DateValue, 'AM', 2, 'Test' from View_Solidnet_Training;
But now, after he inserts the lines in the new table, he stops. But after the loop, he must change the the Start and EndDate again in the new values of the next row in the view: View_Solidnet_Training.
So is there a possible solution, or should I make a new loop where I check if ID of the view is not zero?
As i understand the your question i think you should start from getting the dates into list and then do insert.
Sample:
create table Dates
(
startdate datetime,
endDate datetime
)
insert dates
SELECT '2013-04-06','2013-04-08'
SELECT * from Dates
Declare #date int
Declare #tbl table
(
date_ datetime
)
SELECT #date = datediff(day,startDate-1,EndDate) from Dates
SELECT #date
while(#date != 0 )
Begin
insert into #tbl
SELECT dateadd(day,#date,StartDate-1) from dates
set #date = #date -1
END
/*
--TO-DO
--Update StartDate and EndDate values in table Dates
-- insert YourTable
-- select date_ from #tbl
*/
SELECT * from #tbl
order by date_
The situation:
The user creates a case record that includes a date field (DateOpened), and wants to send the client a follow up every 30 days until the case is closed.
The user will run the query periodically (probably weekly) and provide a 'From' and 'To' date range to specify the period in which a record may fall within the mutliple of 30 days.
The request:
I need a method to identify records where the user specified date range includes those records which are a multiple of 30 days since the DateOpened date.
UPDATE
This is what came to me all of a sudden while watching a third rate TV show last night!!!
SELECT
....
FROM
....
WHERE
(CAST((DATEDIFF(dd, Invoice.DateOpened #EndDate)/30) AS INT) - CAST((DATEDIFF(dd, Invoice.DateOpened, #StartDate)/30) AS INT)) >=1
OR DATEDIFF(dd, Invoice.DateOpened, #StartDate) % 30 = 0 --this line to capture valid records but where From and To dates are the same
Is this Microsoft SQL? Is this Express edition? As long as it's not Express, you may want to look into using the SQL Agent service, which lets you schedule tasks that can run against the database. What do you want it to do with the record once it hits 30 days?
You can use the DATEDIFF function to calculate the difference between dates in days. You can use the modulus (%) operator to get the "remainder" of a division operation. Combining the two gives you:
SELECT
....
FROM
....
WHERE
--In MS T-SQL, BETWEEN is inclusive.
DateOpened BETWEEN #UserSuppliedFromDate AND #UserSuppliedToDate
AND DATEDIFF(dd, DateOpened, getdate()) % 30 = 0
which should give you the desired result.
Edit (Give this example a try in MSSQL):
DECLARE #Table TABLE
(
ID integer,
DateOpened datetime
)
DECLARE #FromDate as datetime = '1/1/2012'
DECLARE #ToDate as datetime = '12/31/2012'
INSERT INTO #Table VALUES (0, '1/1/1982')
INSERT INTO #Table values (1, '1/1/2012')
INSERT INTO #Table VALUES (2, '2/17/2012')
INSERT INTO #Table VALUES (3, '3/16/2012')
INSERT INTO #Table VALUES (4, '4/16/2012')
INSERT INTO #Table VALUES (5, '5/28/2012')
INSERT INTO #Table VALUES (6, '1/31/2012')
INSERT INTO #Table VALUES (7, '12/12/2013')
DECLARE #DateLoop as datetime
DECLARE #ResultIDs as table ( ID integer, DateLoopAtTheTime datetime, DaysDifference integer )
--Initialize to lowest possible value
SELECT #DateLoop = #FromDate
--Loop until we hit the maximum date to check
WHILE #DateLoop <= #ToDate
BEGIN
INSERT INTO #ResultIDs (ID,DateLoopAtTheTime, DaysDifference)
SELECT ID, #DateLoop, DATEDIFF(dd,#DateLoop, DateOpened)
FROM #Table
WHERE
DATEDIFF(dd,#DateLoop, DateOpened) % 30 = 0
AND DATEDIFF(dd,#DateLoop,DateOpened) > 0 -- Avoids false positives when #DateLoop and DateOpened are the same
AND DateOpened <= #ToDate
SELECT #DateLoop = DATEADD(dd, 1, #DateLoop) -- Increment the iterator
END
SELECT distinct * From #ResultIDs
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