Best way to pairing & finding anomalies in SQL data - sql

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

Related

SQL Server Find the Date Range based on Start Date and Frequence

I'm trying to create a function where I can give it any date and it can return a billing date range for a specific asset.
E.g.
Below I've got an example for AssetBilling table which tells me how frequently an asset should be billed based on the start date.
If I give the function a date: 25/02/2021 for the asset Red Car it should tell me that the billing period it falls under is: FROM 01/02/2021 TO 01/03/2021
I have tried the follow CTE but I'm not entirely sure how to use it as I've not done it before:
DECLARE #InvDate AS DATE = '25/02/2021',
#ID AS INT = 1 -- Red Car example
;WITH CTE AS (
SELECT StartDate, DATEADD(MM, Frequency, StartDate) EndDate
FROM AssetBilling WHERE ID = #ID
UNION ALL
SELECT DATEADD(MM, 1, StartDate), DATEADD(MM, 1, EndDate)
FROM CTE
WHERE
StartDate >= #InvDate AND EndDate <= #InvDate
)
SELECT *
FROM
CTE
This is the result that I get:
CREATE TABLE [dbo].[AssetBilling](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Asset] [varchar] (150) NULL,
[StartDate] [date] NULL,
[Frequency] [int] NULL,
[BillType][varchar](50)NULL
);
INSERT INTO AssetBilling
(Asset, StartDate, Frequency, BillType)
VALUES
('Red Car', '01/01/2021', 1, 'Monthly'),
('Blue Car', '25/05/2021', 3, 'Quarterly')
I don't believe you need a recursive CTE here but to Larnu's point please show what you want to happen when the frequency is not monthly (to modify this query to support that I'd need to understand exactly what you want when it's quarterly for example, and the InvDate provided is in the 2nd or 3rd month of the period).
DECLARE #InvDate date = '20210225', -- avoid regional, ambiguous formats
#ID int = 1;
;WITH src(StartDate, Frequency) AS
(
SELECT DATEADD(MONTH,
DATEDIFF(MONTH, StartDate, #InvDate), StartDate), Frequency
FROM dbo.AssetBilling -- always use schema prefix
WHERE ID = #ID
)
SELECT StartDate, EndDate = DATEADD(MONTH,Frequency,StartDate)
FROM src;
This is very much a guess, with one vague example and no further example or explanation were given on request, but maybe this:
DECLARE #InvDate date = '20220225',
#ID int = 1;
SELECT StartDate,
CASE WHEN #InvDate > EndDate THEN EndDate --I assume you have an end date
WHEN DATEDIFF(MONTH,StartDate,#InvDate) < Frequency THEN DATEADD(MONTH, Frequency, StartDate)
WHEN DATEDIFF(MONTH,StartDate,#InvDate) % Frequency != 0 THEN DATEADD(MONTH, ((DATEDIFF(MONTH,StartDate,#InvDate) / Frequency) * Frequency) + Frequency, StartDate)
ELSE #InvDate
END
FROM dbo.AssetBilling
WHERE ID = #ID;
db<>fiddle
There may be scenarios I have no covered in the WHEN, but this is hard to know when, again, we have 1 example. Maybe this gives you a big enough clue.

Selecting count of consecutives dates before and after a specified date based on start/end

I'm trying to determine the number of records with consecutive dates (previous record ends on the same date as the start date of the next record) before and after a specified date, and ignore any consecutive records as soon as there is a break in the chain.
If I have the following data:
-- declare vars
DECLARE #dateToCheck date = '2020-09-20'
DECLARE #numRecsBefore int = 0
DECLARE #numRecsAfter int = 0
DECLARE #tempID int
-- temp table
CREATE TABLE #dates
(
[idx] INT IDENTITY(1,1),
[startDate] DATETIME ,
[endDate] DATETIME,
[prevEndDate] DATETIME
)
-- insert temp table
INSERT INTO #dates
( [startDate], [endDate] )
VALUES ( '2020-09-01', '2020-09-04' ),
( '2020-09-04', '2020-09-10' ),
( '2020-09-10', '2020-09-16' ),
( '2020-09-17', '2020-09-19' ),
( '2020-09-19', '2020-09-20' ),
--
( '2020-09-20', '2020-09-23' ),
( '2020-09-25', '2020-09-26' ),
( '2020-09-27', '2020-09-28' ),
( '2020-09-28', '2020-09-30' ),
( '2020-10-01', '2020-09-05' )
-- update with previous records endDate
DECLARE #maxRows int = (SELECT MAX(idx) FROM #dates)
DECLARE #intCount int = 0
WHILE #intCount <= #maxRows
BEGIN
UPDATE #dates SET prevEndDate = (SELECT endDate FROM #dates WHERE idx = (#intCount - 1) ) WHERE idx=#intCount
SET #intCount = #intCount + 1
END
-- clear any breaks in the chain?
-- number of consecutive records before this date
SET #numRecsBefore = (SELECT COUNT(idx) FROM #dates WHERE startDate = prevEndDate AND endDate <= #dateToCheck)
-- number of consecutive records after this date
SET #numRecsAfter = (SELECT COUNT(idx) FROM #dates WHERE startDate = prevEndDate AND endDate >= #dateToCheck)
-- return & clean up
SELECT * FROM #dates
SELECT #numRecsBefore AS numBefore, #numRecsAfter AS numAfter
DROP TABLE #dates
With the specified date being '2020-09-20, I would expect #numRecsBefore = 2 and #numRecsAfter = 1. That is not what I am getting, as its counting all the consecutive records.
There has to be a better way to do this. I know the loop isn't optimal, but I couldn't get LAG() or LEAD() to work. I've spend all morning trying different methods and searching, but everything I find doesn't deal with two dates, or breaks in the chain.
This reads like a gaps-and-island problem. Islands represents rows whose date ranges are adjacent, and you want to count how many records preceed of follow a current date in the same island.
You could do:
select
max(case when #dateToCheck > startdate and #dateToCheck <= enddate then numRecsBefore end) as numRecsBefore,
max(case when #dateToCheck >= startdate and #dateToCheck < enddate then numRecsAfter end) as numRecsAfter
from (
select d.*,
count(*) over(partition by grp order by startdate) as numRecsBefore,
count(*) over(partition by grp order by startdate desc) as numRecsAfter
from (
select d.*,
sum(case when startdate = lag_enddate then 0 else 1 end) over(order by startdate) as grp
from (
select d.*,
lag(enddate) over(order by startdate) as lag_enddate
from #dates d
) d
) d
) d
This uses lag() and a cumulative sum() to define the islands. The a window count gives the number and preceding and following records on the same island. The final step is conditional aggrgation; extra care needs to be taken on the inequalities to take in account various possibilites (typically, the date you search for might not always match a range bound).
Demo on DB Fiddle
I think this is what you are after, however, this does not give the results in your query; I suspect that is because they aren't the expected results? One of the conditional aggregated may also want to be a >= or <=, but I don't know which:
WITH CTE AS(
SELECT startDate,
endDate,
CASE startDate WHEN LAG(endDate) OVER (ORDER BY startDate ASC) THEN 1 END AS IsSame
FROM #dates d)
SELECT COUNT(CASE WHEN startDate < #dateToCheck THEN IsSame END) AS numBefore,
COUNT(CASE WHEN startDate > #dateToCheck THEN IsSame END) AS numAfter
FROM CTE;

Get total working hours from SQL table

I have an attendance SQL table that stores the start and end day's punch of employee. Each punch (punch in and punch out) is in a separate record.
I want to calculate the total working hour of each employee for a requested month.
I tried to make a scalar function that takes two dates and employee ID and return the calculation of the above task, but it calculate only the difference of one date between all dates.
The data is like this:
000781 2015-08-14 08:37:00 AM EMPIN 539309898
000781 2015-08-14 08:09:48 PM EMPOUT 539309886
My code is:
#FromDate NVARCHAR(10)
,#ToDate NVARCHAR(10)
,#EmpID NVARCHAR(6)
CONVERT(NVARCHAR,DATEDIFF(HOUR
,(SELECT Time from PERS_Attendance att where attt.date between convert(date,#fromDate) AND CONVERT(Date,#toDate)
AND (EmpID= #EmpID OR ISNULL(#EmpID, '') = '') AND Funckey = 'EMPIN')
,(SELECT Time from PERS_Attendance att where attt.date between convert(date,#fromDate) AND CONVERT(Date,#toDate)
AND (EmpID= #EmpID OR ISNULL(#EmpID, '') = '') AND Funckey = 'EMPOUT') ))
FROM PERS_Attendance attt
One more approach that I think is simple and efficient.
It doesn't require modern functions like LEAD
it works correctly if the same person goes in and out several times during the same day
it works correctly if the person stays in over the midnight or even for several days in a row
it works correctly if the period when person is "in" overlaps the start OR end date-time.
it does assume that data is correct, i.e. each "in" is matched by "out", except possibly the last one.
Here is an illustration of a time-line. Note that start time happens when a person was "in" and end time also happens when a person was still "in":
All we need to do it calculate a plain sum of time differences between each event (both in and out) and start time, then do the same for end time. If event is in, the added duration should have a positive sign, if event is out, the added duration should have a negative sign. The final result is a difference between sum for end time and sum for start time.
summing for start:
|---| +
|----------| -
|-----------------| +
|--------------------------| -
|-------------------------------| +
--|====|--------|======|------|===|=====|---|==|---|===|====|----|=====|--- time
in out in out in start out in out in end out in out
summing for end:
|---| +
|-------| -
|----------| +
|--------------| -
|------------------------| +
|-------------------------------| -
|--------------------------------------| +
|-----------------------------------------------| -
|----------------------------------------------------| +
I would recommend to calculate durations in minutes and then divide result by 60 to get hours, but it really depends on your requirements. By the way, it is a bad idea to store dates as NVARCHAR.
DECLARE #StartDate datetime = '2015-08-01 00:00:00';
DECLARE #EndDate datetime = '2015-09-01 00:00:00';
DECLARE #EmpID nvarchar(6) = NULL;
WITH
CTE_Start
AS
(
SELECT
EmpID
,SUM(DATEDIFF(minute, (CAST(att.[date] AS datetime) + att.[Time]), #StartDate)
* CASE WHEN Funckey = 'EMPIN' THEN +1 ELSE -1 END) AS SumStart
FROM
PERS_Attendance AS att
WHERE
(EmpID = #EmpID OR #EmpID IS NULL)
AND att.[date] < #StartDate
GROUP BY EmpID
)
,CTE_End
AS
(
SELECT
EmpID
,SUM(DATEDIFF(minute, (CAST(att.[date] AS datetime) + att.[Time]), #StartDate)
* CASE WHEN Funckey = 'EMPIN' THEN +1 ELSE -1 END) AS SumEnd
FROM
PERS_Attendance AS att
WHERE
(EmpID = #EmpID OR #EmpID IS NULL)
AND att.[date] < #EndDate
GROUP BY EmpID
)
SELECT
CTE_End.EmpID
,(SumEnd - ISNULL(SumStart, 0)) / 60.0 AS SumHours
FROM
CTE_End
LEFT JOIN CTE_Start ON CTE_Start.EmpID = CTE_End.EmpID
OPTION(RECOMPILE);
There is LEFT JOIN between sums for end and start times, because there can be EmpID that has no records before the start time.
OPTION(RECOMPILE) is useful when you use Dynamic Search Conditions in T‑SQL. If #EmpID is NULL, you'll get results for all people, if it is not NULL, you'll get result just for one person.
If you need just one number (a grand total) for all people, then wrap the calculation in the last SELECT into SUM(). If you always want a grand total for all people, then remove #EmpID parameter altogether.
It would be a good idea to have an index on (EmpID,date).
My approach would be as follows:
CREATE FUNCTION [dbo].[MonthlyHoursByEmpID]
(
#StartDate Date,
#EndDate Date,
#Employee NVARCHAR(6)
)
RETURNS FLOAT
AS
BEGIN
DECLARE #TotalHours FLOAT
DECLARE #In TABLE ([Date] Date, [Time] Time)
DECLARE #Out TABLE ([Date] Date, [Time] Time)
INSERT INTO #In([Date], [Time])
SELECT [Date], [Time]
FROM PERS_Attendance
WHERE [EmpID] = #Employee AND [Funckey] = 'EMPIN' AND ([Date] > #StartDate AND [Date] < #EndDate)
INSERT INTO #Out([Date], [Time])
SELECT [Date], [Time]
FROM PERS_Attendance
WHERE [EmpID] = #Employee AND [Funckey] = 'EMPOUT' AND ([Date] > #StartDate AND [Date] < #EndDate)
SET #TotalHours = (SELECT SUM(CONVERT([float],datediff(minute,I.[Time], O.[Time]))/(60))
FROM #in I
INNER JOIN #Out O
ON I.[Date] = O.[Date])
RETURN #TotalHours
END
Assuming the entries are properly paired (in -> out -> in -> out -> in etc).
SQL Server 2012 and later:
DECLARE #Year int = 2015
DECLARE #Month int = 8
;WITH
cte AS (
SELECT EmpID,
InDate = LAG([Date], 1) OVER (PARTITION BY EmpID ORDER BY [Date]),
OutDate = [Date],
HoursWorked = DATEDIFF(hour, LAG([Date], 1) OVER (PARTITION BY EmpID ORDER BY [Date]), [Date]),
Funckey
FROM PERS_Attendance
)
SELECT EmpID,
TotalHours = SUM(HoursWorked)
FROM cte
WHERE Funckey = 'EMPOUT'
AND YEAR(InDate) = #Year
AND MONTH(InDate) = #Month
GROUP BY EmpID
SQL Server 2005 and later:
;WITH
cte1 AS (
SELECT *,
rn = ROW_NUMBER() OVER (PARTITION BY EmpID ORDER BY [Date])
FROM PERS_Attendance
),
cte2 AS (
SELECT a.EmpID, b.[Date] As InDate, a.[Date] AS OutDate,
HoursWorked = DATEDIFF(hour, b.[Date], a.[Date])
FROM cte1 a
LEFT JOIN cte1 b ON a.EmpID = b.EmpID and a.rn = b.rn + 1
WHERE a.Funckey = 'EMPOUT'
)
SELECT EmpID,
TotalHours = SUM(HoursWorked)
FROM cte2
WHERE YEAR(InDate) = #Year
AND MONTH(InDate) = #Month
GROUP BY EmpID

finding local maximums and local minimums in SQL

In order to find the max draw down of a stock price versus time graph, you first have to find all local maximums (peaks) and local minimums (valleys) for a given set of prices and days. How would you do this in SQL Server 2005?
edit:
There is a brute force way of doing this with cursors:
compare the high of the first day to the high of the next day.
if the high of the first day is higher than the high of the next day, the high of the first day is a local Max.
effectively, I need to find every point at which the trend of the price graph changes direction.
edit2: I should note that the database table to work from has the following columns:
stockid int
day date
hi int --this is in pennies
low int --also in pennies
so for a given date range, you'll see the same stockid every day for that date range.
OK, step by step here is what I am thinking:
1 - Find all your "peaks" which are max values with LOWER max values the next day:
DECLARE #HiTable (hi int, day date)
INSERT INTO #HiTable
SELECT hi, day
FROM table t1
WHERE EXISTS (
SELECT t2.hi
FROM Table t2
WHERE t1.hi > t2.hi AND t1.day < t2.day and StockID = X)
2 - Find all your "valleys" which are the min values with HIGHER min values the next day:
DECLARE #LowTable (low int, day date)
INSERT INTO #LowTable
SELECT low, day
FROM table t1
WHERE EXISTS (
SELECT t2.low
FROM Table t2
WHERE t1.low < t2.low AND t1.day < t2.day and StockID = X)
3 - Combine these into a table ordered by date with a identity value to keep us in order
DECLARE #TableVar (low int, hi int, day date, autoid int IDENTITY)
INSERT INTO #TableVar
(SELECT low, hi, day
FROM (
SELECT Low, NULL as 'hi', date FROM #LowTable
UNION ALL
SELECT NULL as 'Low', hi, date FROM #HiTable
)
ORDER BY DATE)
4 - Delete outliers
DELETE FROM #TableVar WHERE AutoID > (SELECT MAX(AutoID) FROM #Table WHERE low IS NULL)
DELETE FROM #TableVar WHERE AutoID < (SELECT MIN(AutoID) FROM #Table WHERE hi IS NULL)
Admitedly not thoroughly tested - but how about using a CTE, and ROWNUMBER() to do this in two steps
1) Identify all the nextsubseqent hi's for each row
2) any row that immediate next row has a subsequent high less than the current row - then current row must be a local max.
or something like that:
begin
DECLARE #highTable as table (high bigint, day date)
declare #securityid int,
#start datetime,
#end datetime
set #start = '1-1-2010'
set #end = '2-1-2010'
select #securityid = id from security where riccode = 'MSFT.OQ' ;
with highsandlows_cte as (
SELECT
ROW_NUMBER() over (order by day) i
, high
, day
, (select top 1 day from quotes nextHi where nextHi.high > today.high and nextHi.day >= today.day and nextHi.securityId = today.securityId order by day asc) nextHighestDay
FROM
quotes today
WHERE
today.securityid = #securityid )
select
*
, (Coalesce((select 1 from highsandlows_cte t2 where t1.i + 1 = t2.i and t1.nextHighestDay > t2.nextHighestDay),0)) as isHigh
from
highsandlows_cte t1
order by
day
end
ok the above is wrong - this appears to be more on track:
begin
DECLARE #highTable as table (high bigint, day date)
declare #securityid int,
#start datetime,
#end datetime
set #start = '1-1-2010'
set #end = '2-1-2010'
select #securityid = id from security where riccode = 'MSFT.OQ' ;
with highsandlows_cte as (
SELECT
ROW_NUMBER() over (order by day) i
, high
, day
, low
FROM
quote today
WHERE
today.securityid = #securityid and today.day > convert(varchar(10), #start, 111) and convert(varchar(10), #end, 111) >today.day)
select
cur.day
, cur.high
, cur.low
, case when ((cur.high > prv.high or prv.high IS null)and(cur.high > nxt.high or nxt.high is null)) then 1 else 0 end as isLocalMax
, case when ((cur.low < prv.low or prv.low IS null)and(cur.low < nxt.low or nxt.low is null)) then 1 else 0 end as isLocalMin
from
highsandlows_cte cur left outer join highsandlows_cte nxt
on cur.i + 1 = nxt.i
left outer join highsandlows_cte prv
on cur.i - 1 = prv.i
order by
cur.day
end
Get issues with duplicates (highs / lows) though...

Improve SQL query: Cumulative amounts over time

Suppose I have a SQL table of Awards, with fields for Date and Amount. I need to generate a table with a sequence of consecutive dates, the amount awarded in each day, and the running (cumulative) total.
Date Amount_Total Amount_RunningTotal
---------- ------------ -------------------
1/1/2010 100 100
1/2/2010 300 400
1/3/2010 0 400
1/4/2010 0 400
1/5/2010 400 800
1/6/2010 100 900
1/7/2010 500 1400
1/8/2010 300 1700
This SQL works, but isn't as quick as I'd like:
Declare #StartDate datetime, #EndDate datetime
Select #StartDate=Min(Date), #EndDate=Max(Date) from Awards
; With
/* Returns consecutive from numbers 1 through the
number of days for which we have data */
Nbrs(n) as (
Select 1 Union All
Select 1+n
From Nbrs
Where n<=DateDiff(d,#StartDate,#EndDate)),
/* Returns all dates #StartDate to #EndDate */
AllDays as (
Select Date=DateAdd(d, n, #StartDate)
From Nbrs )
/* Returns totals for each day */
Select
d.Date,
Amount_Total = (
Select Sum(a.Amount)
From Awards a
Where a.Date=d.Date),
Amount_RunningTotal = (
Select Sum(a.Amount)
From Awards a
Where a.Date<=d.Date)
From AllDays d
Order by d.Date
Option(MAXRECURSION 1000)
I tried adding an index to Awards.Date, but it made a very minimal difference.
Before I resort to other strategies like caching, is there a more efficient way to code the running total calculation?
I generally use a temporary table for this:
DECLARE #Temp TABLE
(
[Date] date PRIMARY KEY,
Amount int NOT NULL,
RunningTotal int NULL
)
INSERT #Temp ([Date], Amount)
SELECT [Date], Amount
FROM ...
DECLARE #RunningTotal int
UPDATE #Temp
SET #RunningTotal = RunningTotal = #RunningTotal + Amount
SELECT * FROM #Temp
If you can't make the date column a primary key then you need to include an ORDER BY [Date] in the INSERT statement.
Also, this question's been asked a few times before. See here or search for "sql running total". The solution I posted is, as far as I know, still the one with the best performance, and also easy to write.
I don't have a database setup in front of me so I hope the below works first shot. A pattern like this should result in a much speedier query...you're just joining twice, similar amount of aggregation:
Declare #StartDate datetime, #EndDate datetime
Select #StartDate=Min(Date), #EndDate=Max(Date) from Awards
;
WITH AllDays(Date) AS (SELECT #StartDate UNION ALL SELECT DATEADD(d, 1, Date)
FROM AllDays
WHERE Date < #EndDate)
SELECT d.Date, sum(day.Amount) Amount_Total, sum(running.Amount) Amount_RunningTotal
FROM AllDays d
LEFT JOIN (SELECT date, SUM(Amount) As Amount
FROM Awards
GROUP BY Date) day
ON d.Date = day.Date
LEFT JOIN (SELECT date, SUM(Amount) As Amount
FROM Awards
GROUP BY Date) running
ON (d.Date >= running.Date)
Group by d.Date
Order by d.Date
Note: I changed your table expression up top, it was leaving out the first day before...if this is intentional just slap a where clause on this to exclude it. Let me know in the comments if this doesn't work or doesn't fit and I'll make whatever adjustments.
Here's a working solution based on #Aaronaught's answer. The only gotcha I had to overcome in T-SQL was that #RunningTotal etc. can't be null (need to be converted to zero).
Declare #StartDate datetime, #EndDate datetime
Select #StartDate=Min(StartDate),#EndDate=Max(StartDate) from Awards
/* #AllDays: Contains one row per date from #StartDate to #EndDate */
Declare #AllDays Table (
Date datetime Primary Key)
; With
Nbrs(n) as (
Select 0 Union All
Select 1+n from Nbrs
Where n<=DateDiff(d,#StartDate,#EndDate)
)
Insert into #AllDays
Select Date=DateAdd(d, n, #StartDate)
From Nbrs
Option(MAXRECURSION 10000) /* Will explode if working with more than 10000 days (~27 years) */
/* #AmountsByDate: Contains one row per date for which we have an Award, along with the totals for that date */
Declare #AmountsByDate Table (
Date datetime Primary Key,
Amount money)
Insert into #AmountsByDate
Select
StartDate,
Amount=Sum(Amount)
from Awards a
Group by StartDate
/* #Result: Joins #AllDays and #AmountsByDate etc. to provide totals and running totals for every day of the award */
Declare #Result Table (
Date datetime Primary Key,
Amount money,
RunningTotal money)
Insert into #Result
Select
d.Date,
IsNull(bt.Amount,0),
RunningTotal=0
from #AllDays d
Left Join #AmountsByDate bt on d.Date=bt.Date
Order by d.Date
Declare #RunningTotal money Set #RunningTotal=0
Update #Result Set #RunningTotal = RunningTotal = #RunningTotal + Amount
Select * from #Result