I have a MSSQL database table Events. I am worried that performance could be improved.
EventId
LocationId
Start
End
Quantity
Price
Currency
1
4
2022-08-31 22:00:00.0000000 +02:00
2022-08-31 23:00:00.0000000 +02:00
7.50000
2.0
EUR
2
2
2022-04-04 19:00:00.0000000 +01:00
2022-04-04 20:00:00.0000000 +01:00
1.50000
7.5
EUR
3
2
2022-04-04 19:00:00.0000000 +01:00
2022-04-04 20:00:00.0000000 +01:00
4.00000
8.2
EUR
I already have the following index:
CREATE NONCLUSTERED INDEX [IDX__Events__Location_Start_End] on [Events]
(
[LocationId] asc,
[Start] asc,
[End] asc
)
But Azure suggests that I create this index (medium impact):
CREATE NONCLUSTERED INDEX [IDX__Events__Location_End] ON [dbo].[Events] ([LocationId], [End]) INCLUDE ([Currency], [Price], [Quantity], [Start]) WITH (ONLINE = ON)
Hint: I do a lot of queries where I select Events greater than a start time and less than an end time.
Why is this extra index useful? Should I change my first index instead?
EDIT:
I run this code (EF Core) very often:
var relevantEvents = await _events.Where($#"
[{nameof(Events.LocationId)}] = #locationId
and [{nameof(Events.End)}] > #start
and [{nameof(Events.Start)}] < #end
", args);
Besides that, I upsert to the table often as well.
If you always do queries of the form shown at the end of your question with an equality predicate on LocationId and an inequality predicate on both End and Start then both LocationId, End and LocationId, Start would be viable indexing choices.
Note there is no benefit of adding the third column in as a key column because it will only be able to do a range seek for one or the other of them but the other one should be added as an included column.
My suspicion is that for typical scenarios LocationId, Start will generally involve reading more rows in the range seek than LocationId, End would (as the table accumulates years worth of data Start < #end will still need to read all the old rows from years ago).
The reason your existing index might not be being used and it feels moved to suggest an additional one is due to the INCLUDE ([Currency], [Price], [Quantity]) in the suggested one. If you add these included columns to your existing one you may well see the recommendation go away but you should consider which of LocationId, End and LocationId, Start will typically be able to narrow down the rows better (see "Numbers of rows read" in the execution plan).
CREATE TABLE dbo.Events
(
EventId INT IDENTITY PRIMARY KEY,
LocationId INT,
Start DATETIME2,
[End] DATETIME2,
Quantity DECIMAL(10,6),
Price DECIMAL(10,2),
Currency CHAR(3),
INDEX IDX__Events__Location_Start_End(LocationId, Start, [End]),
INDEX IDX__Events__Location_End(LocationId, [End]) INCLUDE (Start)
)
INSERT INTO dbo.Events
(LocationId, Start,[End], Quantity, Price,Currency)
SELECT LocationId = 1,
Start = DATEADD(SECOND, -Num, GETDATE()),
[End] = DATEADD(SECOND, 60-Num, GETDATE()),
Qty = 7.5,
Price = 2,
Currency = 'EUR'
FROM
(
SELECT TOP 1000000 Num = ROW_NUMBER() OVER (ORDER BY ##SPID)
FROM sys.all_columns c1, sys.all_columns c2
) Nums
DECLARE #Start DATE = GETDATE(), #End DATE = DATEADD(DAY, 1, GETDATE())
SELECT COUNT(*)
FROM dbo.Events WITH (INDEX = IDX__Events__Location_Start_End)
WHERE LocationId = 1 AND [End] > #Start AND Start < #End
OPTION (RECOMPILE)
SELECT COUNT(*)
FROM dbo.Events WITH (INDEX = IDX__Events__Location_End)
WHERE LocationId = 1 AND [End] > #Start AND Start < #End
OPTION (RECOMPILE)
Related
I don't know how to calculate the average age of a column of type date in SQL Server.
You can use datediff() and aggregation. Assuming that your date column is called dt in table mytable, and that you want the average age in years over the whole table, then you would do:
select avg(datediff(year, dt, getdate())) avg_age
from mytable
You can change the first argument to datediff() (which is called the date part), to any other supported value depending on what you actually mean by age; for example datediff(day, dt, getdate()) gives you the difference in days.
First, lets calculate the age in years correctly. See the comments in the code with the understanding that DATEDIFF does NOT calculate age. It only calculates the number of temporal boundaries that it crosses.
--===== Local obviously named variables defined and assigned
DECLARE #StartDT DATETIME = '2019-12-31 23:59:59.997'
,#EndDT DATETIME = '2020-01-01 00:00:00.000'
;
--===== Show the difference in milliseconds between the two date/times
-- Because of the rounding that DATETIME does on 3.3ms resolution, this will return 4ms,
-- which certainly does NOT depict an age of 1 year.
SELECT DATEDIFF(ms,#StartDT,#EndDT)
;
--===== This solution will mistakenly return an age of 1 year for the dates given,
-- which are only about 4ms apart according the SELECT above.
SELECT IncorrectAgeInYears = DATEDIFF(YEAR, #StartDT, #EndDT)
;
--===== This calulates the age in years correctly in T-SQL.
-- If the anniversary data has not yet occurred, 1 year is substracted.
SELECT CorrectAgeInYears = DATEDIFF(yy, #StartDT, #EndDT)
- IIF(DATEADD(yy, DATEDIFF(yy, #StartDT, #EndDT), #StartDT) > #EndDT, 1, 0)
;
Now, lets turn that correct calculation into a Table Valued Function that returns a single scalar value producing a really high speed "Inline Scalar Function".
CREATE FUNCTION [dbo].[AgeInYears]
(
#StartDT DATETIME, --Date of birth or date of manufacture or start date.
#EndDT DATETIME --Usually, GETDATE() or CURRENT_TIMESTAMP but
--can be any date source like a column that has an end date.
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
SELECT AgeInYears = DATEDIFF(yy, #StartDT, #EndDT)
- IIF(DATEADD(yy, DATEDIFF(yy, #StartDT, #EndDT), #StartDT) > #EndDT, 1, 0)
;
Then, to Dale's point, let's create a test table and populate it. This one is a little overkill for this problem but it's also useful for a lot of different examples. Don't let the million rows scare you... this runs in just over 2 seconds on my laptop including the Clustered Index creation.
--===== Create and populate a large test table on-the-fly.
-- "SomeInt" has a range of 1 to 50,000 numbers
-- "SomeLetters2" has a range of "AA" to "ZZ"
-- "SomeDecimal has a range of 10.00 to 100.00 numbers
-- "SomeDate" has a range of >=01/01/2000 & <01/01/2020 whole dates
-- "SomeDateTime" has a range of >=01/01/2000 & <01/01/2020 Date/Times
-- "SomeRand" contains the value of RAND just to show it can be done without a loop.
-- "SomeHex9" contains 9 hex digits from NEWID()
-- "SomeFluff" is a fixed width CHAR column just to give the table a little bulk.
SELECT TOP 1000000
SomeInt = ABS(CHECKSUM(NEWID())%50000) + 1
,SomeLetters2 = CHAR(ABS(CHECKSUM(NEWID())%26) + 65)
+ CHAR(ABS(CHECKSUM(NEWID())%26) + 65)
,SomeDecimal = CAST(RAND(CHECKSUM(NEWID())) * 90 + 10 AS DECIMAL(9,2))
,SomeDate = DATEADD(dd, ABS(CHECKSUM(NEWID())%DATEDIFF(dd,'2000','2020')), '2000')
,SomeDateTime = DATEADD(dd, DATEDIFF(dd,0,'2000'), RAND(CHECKSUM(NEWID())) * DATEDIFF(dd,'2000','2020'))
,SomeRand = RAND(CHECKSUM(NEWID())) --CHECKSUM produces an INT and is MUCH faster than conversion to VARBINARY.
,SomeHex9 = RIGHT(NEWID(),9)
,SomeFluff = CONVERT(CHAR(170),'170 CHARACTERS RESERVED') --Just to add a little bulk to the table.
INTO dbo.JBMTest
FROM sys.all_columns ac1 --Cross Join forms up to a 16 million rows
CROSS JOIN sys.all_columns ac2 --Pseudo Cursor
;
GO
--===== Add a non-unique Clustered Index to SomeDateTime for this demo.
CREATE CLUSTERED INDEX IXC_Test ON dbo.JBMTest (SomeDateTime ASC)
;
Now, lets find the average age of those million represented by the SomeDateTime column.
SELECT AvgAgeInYears = AVG(age.AgeInYears )
,RowsCounted = COUNT(*)
FROM dbo.JBMTest tst
CROSS APPLY dbo.AgeInYears(SomeDateTime,GETDATE()) age
;
Results:
We have 3 nested selects that are each creating temporary table. The outer two go very fast. But the inner one (below takes about 1/4 second sometimes to execute. It's creating a table with 7 rows, each holding a date:
declare #StartDate datetime
declare #EndDate datetime
select #StartDate = cast(#Weeks_Loop_TheDate as date), #EndDate = cast((#Weeks_Loop_TheDate + 6) as date)
declare #temp3 table
(
TheDate datetime
)
while (#StartDate<=#EndDate)
begin
insert into #temp3
values (#StartDate )
select #StartDate=DATEADD(dd,1,#StartDate)
end
select * from #temp3
The params are set with a DateTime variable so the cast shouldn't be significant. And the populating should be trivial and fast. So any ideas why it's slow?
And is there a better way to do this? We need to get back a result set that is 7 dates in this range.
thanks - dave
Wouldn't this work? Loops/cursors are slow in SQL Server compared to set operations.
DECLARE #StartDate DATE = '2017-05-03';
SELECT DATEADD(DAY, RowNr, #StartDate)
FROM (SELECT ROW_NUMBER () OVER (ORDER BY object_id) - 1 AS RowNr FROM sys.objects) AS T
WHERE T.RowNr < 7;
Subquery will generate a sequence of numbers from 0 to n (amount of objects you have in database, it's always going to be more than 7, and if not, you can just CROSS JOIN inside).
Then just use DATEADD to add these generated numbers.
And finally limit amount of days you want to add in your WHERE clause.
And if you're going to use this quite often, you can wrap it up in a Inline Table-Valued Function.
CREATE FUNCTION dbo.DateTable (
#p1 DATE,
#p2 INT)
RETURNS TABLE
AS RETURN
SELECT DATEADD(DAY, RowNr, #p1) AS TheDate
FROM (SELECT ROW_NUMBER () OVER (ORDER BY object_id) - 1 AS RowNr FROM sys.objects) AS T
WHERE T.RowNr < #p2;
GO
And then query it like that:
SELECT *
FROM dbo.DateTable ('2017-05-03', 7);
Result in both cases:
+------------+
| TheDate |
+------------+
| 2017-05-03 |
| 2017-05-04 |
| 2017-05-05 |
| 2017-05-06 |
| 2017-05-07 |
| 2017-05-08 |
| 2017-05-09 |
+------------+
Yet another useful tool is a Numbers table. It can be created just like that (source: http://dataeducation.com/you-require-a-numbers-table/) :
CREATE TABLE Numbers
(
Number INT NOT NULL,
CONSTRAINT PK_Numbers
PRIMARY KEY CLUSTERED (Number)
WITH FILLFACTOR = 100
)
INSERT INTO Numbers
SELECT
(a.Number * 256) + b.Number AS Number
FROM
(
SELECT number
FROM master..spt_values
WHERE
type = 'P'
AND number <= 255
) a (Number),
(
SELECT number
FROM master..spt_values
WHERE
type = 'P'
AND number <= 255
) b (Number)
GO
Then you would not have to use ROW_NUMBER() and your function could be as follows:
ALTER FUNCTION dbo.DateTable (
#p1 DATE,
#p2 INT)
RETURNS TABLE
AS RETURN
SELECT DATEADD(DAY, Number, #p1) AS TheDate
FROM Numbers
WHERE Number < #p2;
GO
This is going to work like a charm and Numbers table could be reused in many other scenarios where you need a sequence of numbers to do some sort of calculations.
It shouldn't take more than a millisecond to run your script. There must be a server issue that requires investigation.
That said, this operation can be done as a more efficient set-based operation instead of looping. The example below uses a CTE to generate the number sequence. A utility numbers table facilitates set-based processing like this so I suggest you create a permanent table with a sequence of numbers (with number as the primary key) to improve performance further.
DECLARE #StartDate date = #Weeks_Loop_TheDate;
WITH numbers(n) AS (
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT 0)) - 1 FROM (VALUES(0),(0),(0),(0),(0),(0),(0)) AS a(b)
)
SELECT DATEADD(day, n, #StartDate)
FROM numbers
ORDER BY n;
Rather than using a variable such as #temp use a temp table (#) instead. The query analyser doesn't do optimizing well when using #temp.
The problem is that it takes way to long in SQL and there must be a better way. I’ve picked out the slow part for the scenario bellow.
Scenario:
Two (temp) tables with event times for start and end for vehicles that have to be paired up to figure idle durations. The issue is that some of the event data is missing. I figured out a rudimentary way of going through and determining when the last end time is after the next start time and removing the invalid start. Again not elegant + very slow.
Tables :
create table #start(VehicleIp int null, CurrentDate datetime null,
EventId int null,
StartId int null)
create table #end(VehicleIp int null,
CurrentDate datetime null,
EventId int null,
EndId int null)
--//Note: StartId and EndId are both pre-filled with something like:
ROW_NUMBER() Over(Partition by VehicleIp order by VehicleIp, CurrentDate)
--//Slow SQL
while exists(
select top 1 tOn.EventId
from #start as tOn
left JOIN #end tOff
on tOn.VehicleIp = tOff.VehicleIp and
tOn.StartID = tOff.EndID +1
)
begin
declare #badEntry int
select top 1 #badEntry = tOn.EventId
from #s as tOn
left JOIN #se tOff
on tOn.VehicleIp = tOff.VehicleIp and
tOn.StartID = tOff.EndID +1
order by tOn.CurrentDate
delete from #s where EventId = #badEntry
;with _s as ( select VehicleIp, CurrentDate, EventId,
ROW_NUMBER() Over(Partition by VehicleIp
order by VehicleIp, CurrentDate) StartID
from #start)
update #start
set StartId = _s.StartId
from #s join _s on #s.EventId = _s.EventId
end
Assuming you start with a table containing Vehicle and interval in which it was used, this query will identify gaps.
select b.VehicleID, b.IdleStart, b.IdleEnd
from
(
select VehicleID,
-- If EndDate is not inclusive, remove +1
EndDate + 1 IdleStart,
-- First date after current for this vehicle
-- If you don't want to show unused vehicles to current date remove isnull part
isnull((select top 1 StartDate
from TableA a
where a.VehicleID = b.VehicleID
and a.StartDate > b.StartDate
order by StartDate
), getdate()) IdleEnd
from TableA b
) b
where b.IdleStart < b.IdleEnd
If dates have time portion they should be truncated to required precision, here is for day:
dateadd(dd, datediff(dd,0, getDate()), 0)
Replace dd with hh, mm or whatever precision is needed.
And here is Sql Fiddle with test
I have a table of PricePlan that looks like
Table PricePlan
ID MerchantID NAME VALIDFROM VALIDUPTO
1. M1 Plan A 1-sep-09 30-sep-09
2. M1 Plan B 7-sep-09 21-sep-09
3. M2 Plan Da 1-sep-09 30-Sep-09
Given a #FromDate and #ToDate I need to find the matching id and Unique MerchantID. Example
#FromDate = '7-sep-09'
#Todate = '9-sep-09'
The return result should be ID 2- M1-Plan B , ID 3-M2-Plan Da
Can anyone help me on the SQL query
thanks in advance - Thomas
SELECT
*
FROM
PricePlan
WHERE
ValidFrom <= #ToDate
AND
ValidTo >= #FromDate
EDIT: This will find you all ranges that come under your given range. If you wish to prioritise one over the other in instances where two plans cover your specified dates then you will need to come up with that rule.
If I understand correctly, for each MerchantID you are looking for the plan that includes the requested dates, but is the one most recently in effect. To accomplish that, try the following query:
select ID, MerchantID, NAME
from PricePlan pp
inner join (
select MerchantID, max(VALIDFROM) as VALIDFROM
from PricePlan
where VALIDFROM <= '7-sep-09'
and VALIDUPTO >= '9-sep-09'
group by MerchantID
) pp2 on pp.MerchantID = pp2.MerchantID
and pp.VALIDFROM = pp2.VALIDFROM
It seems like you want to select the plan whose boundary dates are most limiting - in other words, where the difference between #toDate and ValidFrom is minimized, and #FromDate and ValidTo are minimized as well.
If that is a correct restatement of your goal you could add to Robin Day's answer the code to order your result set by the difference (although, to be honest, I'm not sure if SQL supports arithmetic on dates) and select the first result using limit 1.
try this:
DECLARE #YourTable table (RowID int, Merchant char(2), NameOf varchar(7), ValidFrom datetime, ValidTo datetime)
INSERT INTO #YourTable VALUES (1,'M1','Plan A' ,'1-sep-09','30-sep-09')
INSERT INTO #YourTable VALUES (2,'M1','Plan B' ,'7-sep-09','21-sep-09')
INSERT INTO #YourTable VALUES (3,'M2','Plan Da','1-sep-09','30-Sep-09')
--SELECT * FROM #YourTable
DECLARE #FromD datetime
DECLARE #ToD datetime
SET #FromD ='7-sep-09'
SET #ToD ='9-sep-09'
SELECT TOP 1
*
FROM #YourTable
WHERE ValidFrom <= #FromD AND ValidTo >= #ToD
ORDER BY datediff(day,ValidFrom,#FromD)+datediff(day,#ToD,ValidTo) ASC, RowID
OUTPUT:
RowID Merchant NameOf ValidFrom ValidTo
----------- -------- ------- ----------------------- -----------------------
2 M1 Plan B 2009-09-07 00:00:00.000 2009-09-21 00:00:00.000
(1 row(s) affected)
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