Reputation: 2201
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.
Upvotes: 2
Views: 1233
Reputation: 69809
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
Upvotes: 1
Reputation: 239814
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.
Upvotes: 2
Reputation: 66737
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.
Upvotes: 0