Calculating the number of scheduled dates within a date range from a Weekly Schedule - sql

I have a Schedule table with a Worker ID columns Monday to Sunday. If the contains value greater than 0, it means the Worker is scheduled to work on that day.
How can I efficiently calculate how many days a worker needs to work between a date range? Currently I'm building a date table and cross joining it with the Schedule table and then generating a 1 or 0 for each date. Then I sum up the values to get the total.
http://sqlfiddle.com/#!3/a3292a/7
This appears to work, but it's relatively slow because of the cross-join. Is there a better/faster way of doing it?

You could try UNPIVOT on the WorkSchedule and join to your DateValue query on DateName.. Then you just SUM the values
;WITH
L0 AS (SELECT 1 AS c UNION ALL SELECT 1),
L1 AS (SELECT 1 AS c FROM L0 A CROSS JOIN L0 B),
L2 AS (SELECT 1 AS c FROM L1 A CROSS JOIN L1 B),
L3 AS (SELECT 1 AS c FROM L2 A CROSS JOIN L2 B),
L4 AS (SELECT 1 AS c FROM L3 A CROSS JOIN L3 B),
Nums AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS i FROM L4),
Schedule AS (
SELECT *
FROM [WorkerSchedule]
UNPIVOT (
Working
FOR [WorkDay] IN ([Sunday], [Monday], [Tuesday], [Wednesday], [Thursday], [Friday], [Saturday])
) u
)
SELECT s.WorkerId, SUM(s.Working) ScheduledDays
FROM Schedule s
INNER JOIN (SELECT DATENAME(weekday, DATEADD(DAY, i - 1, #StartDate)) AS DateValue
FROM Nums
WHERE i <= 1 + DATEDIFF(DAY, #StartDate, #EndDate)) d ON s.[WorkDay] = d.DateValue
WHERE s.Working = 1
GROUP BY s.WorkerId

Related

SQL: Capturing first row in WHILE loop

I'm thinking my issue is based on how I've written the loop, but how it is written is how I've come to understand loops. So I'm really wondering if there is a way to consolidate this? As it sits, I have 2 sections: the first which captures the very first row, and it exists solely because my second section will capture the 2nd row up through the max row (as determined by #I).
Is there a way to get the first row inside the loop?
DECLARE #COUNTER TINYINT = 15
DECLARE #EndDate DATE= CAST(CONCAT(YEAR(GETDATE()), '-', MONTH(GETDATE()), '-01') AS DATE)
DECLARE #StartDate DATE= DATEADD(MONTH, -12, #EndDate)
--THIS SECTION EXISTS SO I CAN CAPTURE THE FIRST ROW
SELECT count = COUNT(DISTINCT value)
FROM [TABLE]
WHERE DATE >= #StartDate
AND DATE < #EndDate
--THE LOOP BELOW CAPTURES THE SECOND ROW THROUGH THE REST OF THE ROWS BASED ON THE UPPER THRESHOLD OF #I
DECLARE #I TINYINT
SET #I = 0
WHILE #I <= #COUNTER
BEGIN
SET #I = #I + 1
SELECT count = COUNT(DISTINCT value)
FROM [TABLE]
WHERE DATE >= DATEADD(MONTH, -(#I), #StartDate)
AND DATE < DATEADD(MONTH, -(#I), #EndDate)
END
EDIT 1:
Given the interest in the approach here, I thought I'd try to explain why I went with a loop as opposed to a set based query.
So here is my actual query:
DECLARE #EndDate DATE= CAST(CONCAT(YEAR(GETDATE()), '-', MONTH(GETDATE()), '-01') AS DATE)
DECLARE #StartDate DATE= DATEADD(MONTH, -12, #EndDate)
SELECT count = COUNT(DISTINCT o.ClinicLocationId)
FROM [order].package p WITH(NOLOCK)
INNER JOIN [order].[order] o WITH(NOLOCK) ON o.packageid = p.packageid
INNER JOIN Profile.ClinicLocationInfo cli WITH(NOLOCK) ON cli.LocationId = o.ClinicLocationId
AND cli.FacilityType IN('CLINIC', 'HOSPITAL')
WHERE CAST(p.ShipDTM AS DATE) >= #StartDate
AND CAST(p.ShipDTM AS DATE) < #EndDate
AND p.isshipped = 1
AND o.IsShipped = 1
AND ISNULL(o.iscanceled, 0) = 0
This gives me a count of 1670, which I know is correct because I have an older report with which to compare output. So when I add a date column to the SELECT statement, which is then also added to the GROUP BY, I get a list of numbers. You would think that by simply tallying the count column within those date ranges, you'd get the same value. But that is not what happens here. For just the first row, where I'd expect a tally of 1670, I'm actually getting 3956. I believe this is because of how Active is being calculated.
Active is determined by a rolling 12 month range. So for example, as of 7/1/2022 (with a starting date of 7/1/2021), there are 1670 locations. If I wanted to look to see how many Active locations there were as of 6/1/2022, I'd have to subtract a month from my #Start and #End to attain that rolling 12 month block. This is why I went with a loop, it seemed much easier to get my results this way. I just verified it takes 7 seconds to run for a 15-month span.
So given this further explanation, I'm curious if there would be a set-based solution for this? I did try the answer provided by Joel, but it did not produce correct numbers (understandable as he did not have more information that's now provided).
One option is moving the SET #I = #I + 1 line to after the rest of the loop body (and then also run for one iteration longer). In this way, the first adjustment for the dates is still 0. But don't do this.
I'm thinking my issue is based on how I've written the loop
It's not in how you've written the loop, but that a loop was written at all. Nearly every case where you want to use a loop in SQL there is a set-based alternative that is vastly more efficient... usually multiple orders of magnitude. This is no exception. Six-ten seconds is an eternity for a process like this; no reason it shouldn't finish almost instantly.
The code for that will look something like this:
WITH
-- generate numbers
L0 AS(SELECT 1 AS c UNION ALL SELECT 1), -- 2^1
L1 AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B), -- 2^2
L2 AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B), -- 2^4
L3 AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B), -- 2^8
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))-1 AS n FROM L3),
-- project numbers as start and end dates
Dates As (SELECT TOP 17
DATEADD(month, DATEDIFF(month, 0, current_timestamp) -n-12, 0) as StartDate,
DATEADD(month, DATEDIFF(month, 0, current_timestamp) -n, 0) as EndDate
FROM Nums ORDER BY n)
SELECT d.StartDate, COUNT(DISTINCT value) as [count]
FROM [TABLE] t
-- use the dates to filter the table
INNER JOIN Dates d on t.[Date] > = d.StartDate and t.[Date] < d.EndDate
GROUP BY d.StartDate
Or I can show this as actually runnable code:
WITH
L0 AS(SELECT 1 AS c UNION ALL SELECT 1),
L1 AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),
L2 AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),
L3 AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))-1 AS n FROM L3),
Dates As (SELECT top 17
DATEADD(month, DATEDIFF(month, 0, current_timestamp) -n-12, 0) as StartDate,
DATEADD(month, DATEDIFF(month, 0, current_timestamp) -n, 0) as EndDate
FROM Nums ORDER BY n)
SELECT StartDate, EndDate
FROM Dates d
We can see this gives 17 results with same start and end values as a modified version of the original code in the question.
Update:
Now that we have the full original code, I can adapt my answer to use it:
WITH
L0 AS(SELECT 1 AS c UNION ALL SELECT 1), -- 2^1
L1 AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B), -- 2^2
L2 AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B), -- 2^4
L3 AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B), -- 2^8
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))-1 AS n FROM L3),
Dates As (SELECT TOP 17
DATEADD(month, DATEDIFF(month, 0, current_timestamp) -n-12, 0) as StartDate,
DATEADD(month, DATEDIFF(month, 0, current_timestamp) -n, 0) as EndDate
FROM Nums ORDER BY n
)
SELECT d.StartDate, COUNT(DISTINCT o.ClinicLocationId) As [count]
FROM [order].package p
INNER JOIN [order].[order] o ON o.packageid = p.packageid
INNER JOIN Profile.ClinicLocationInfo cli ON cli.LocationId = o.ClinicLocationId
AND cli.FacilityType IN ('CLINIC', 'HOSPITAL')
-- PLEASE tell me ShipDTM is a datetime value and not a varchar
INNER JOIN Dates d ON d.StartDate <= p.ShipDTM and p.ShipDTM < d.EndDate
WHERE p.IsShipped = 1 and o.IsShipped = 1 o.IsCanceled IS NULL
GROUP BY d.StartDate
Alternatively, if this still somehow gives you the wrong results (I think the GROUP BY will have fixed it), you can use an APPLY instead, like so (the JOIN/GROUP BY should still be faster):
WITH
L0 AS(SELECT 1 AS c UNION ALL SELECT 1), -- 2^1
L1 AS(SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B), -- 2^2
L2 AS(SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B), -- 2^4
L3 AS(SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B), -- 2^8
Nums AS(SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))-1 AS n FROM L3),
Dates As (SELECT TOP 17
DATEADD(month, DATEDIFF(month, 0, current_timestamp) -n-12, 0) as StartDate,
DATEADD(month, DATEDIFF(month, 0, current_timestamp) -n, 0) as EndDate
FROM Nums ORDER BY n
)
SELECT d.StartDate, counts.[count]
FROM Dates d
CROSS APPLY (
SELECT count = COUNT(DISTINCT o.ClinicLocationId)
FROM [order].package p
INNER JOIN [order].[order] o ON o.packageid = p.packageid
INNER JOIN Profile.ClinicLocationInfo cli ON cli.LocationId = o.ClinicLocationId
AND cli.FacilityType IN('CLINIC', 'HOSPITAL')
WHERE p.ShipDTM >= d.StartDate
AND p.ShipDTM < d.EndDate
AND p.isshipped = 1
AND o.IsShipped = 1
AND IsCanceled IS NOT NULL
) counts
One final note here, regarding the ShipDTM column. I know you may not have any control over this, but the CAST() around that column makes it look like it's a varchar or similar. If it is, you should see if you can fix it, and I say "fix" because the schema really is considered broken.
As it is, you're likely converting every row in the table to a Date value — even rows you don't need. Thanks to internationalization issues, these conversion are not the simple or fast process you might expect; in fact converting between either date or numeric and string values is always something to avoid as much as possible. They also invalidate any index you might have on the column. Even worse, you are repeating these conversions for each iteration! No wonder the query runs for multiple seconds!
Almost much as the loop, these conversions are likely the source of the slowness. The good news is the the JOIN + GROUP BY version of my solution should at least get you back to only needing to convert these values once. Fixing the column (because again: it is broken) will get yet another speed boost. I do understand this is likely to be either above your pay grade or a vendor system you can't change, but you should at least bring up the issue with someone who can influence this: either an architect/senior dev or the vendor directly.

Display All Quarters based on Year In SQL Server

I Need to display all the Quarters from date field.
SELECT '2021' AS YOE,DATEPART(Quarter,'2021')AS [Quarter],450 AS Qty
Actual Result:
YOE
Quarter
Qty
2021
1
450
Expected Result:
YOE
Quarter
Qty
2021
1
450
2021
2
0
2021
3
0
2021
4
0
Here is an answer that will get you results for any given year.
What you need to do is start with a calendar table as the driving row, then LEFT JOIN your table to it.
We will use Itzik Ben-Gan's tally table for this purpose. We pass through the starting year as #startingYear:
;WITH
L0 AS ( SELECT 1 AS c
FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),
(1),(1),(1),(1),(1),(1),(1),(1)) AS D(c) ),
L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
FROM L1 ),
Years AS ( SELECT DATEADD(year, rownum - 1, #startingYear) AS Year FROM Nums)
SELECT
y.Year AS YOE,
q.Quarter,
SUM(t.Value) AS Qty
FROM Years AS y
CROSS JOIN (VALUES (1), (2), (3), (4) ) AS q(Quarter)
LEFT JOIN (Table AS t
-- any other inner or left joins between Table and ON
)
ON DATEPART(Quarter,t.Date) = q.Quarter AND
t.Date >= DATETIMEFROMPARTS(#startingYear, 1, 1);
I would suggest values():
SELECT '2021' AS YOE, vv.*
FROM (VALUES (1, 350), (2, 0), (3, 0), (4, 0)) v([Quarter], Qty)
Considering it seems that this isn't against a table, but you are "creating" the data, use a VALUES table construct:
SELECT YOE,
[Quarter],
Qty
FROM (VALUES(2021,1,450), --Numerical values don't go in quotes.
(2021,2,0), --Strings and Numerical values are very different,
(2021,3,0), --don't confuse the 2.
(2021,4,0))V(YOE,[Quarter],Qty)
ORDER BY YOE,
[Quarter];

Showing list of all 24 hours in sql server if there is no data also

I have a query where I need to show 24 hour calls for each day.
But I am getting the hours which I have calls only.
My requirement is I need to get all the hours split and 0 if there are no calls.
Please suggest
Below is my code.
select #TrendStartDate
,isd.Name
,isd.Call_ID
,isd.callType
,DATEPART(HOUR,isd.ArrivalTime)
from [PHONE_CALLS] ISD WITH (NOLOCK)
WHERE CallType = 'Incoming'
and Name not in ('DefaultQueue')
and CAST(ArrivalTime as DATe) between #TrendStartDate and #TrendEndDate
The basic idea is that you use a table containing numbers from 0 to 23, and left join that to your data table:
WITH CTE AS
(
SELECT TOP 24 ROW_NUMBER() OVER(ORDER BY ##SPID) - 1 As TheHour
FROM sys.objects
)
SELECT #TrendStartDate
,isd.Name
,isd.Call_ID
,isd.callType
,TheHour
FROM CTE
LEFT JOIN [PHONE_CALLS] ISD WITH (NOLOCK)
ON DATEPART(HOUR,isd.ArrivalTime) = TheHour
AND CallType = 'Incoming'
AND Name NOT IN ('DefaultQueue')
AND CAST(ArrivalTime as DATe) BETWEEN #TrendStartDate AND #TrendEndDate
If you have a tally table, you should use that. If not, the cte will provide you with numbers from 0 to 23.
If you have a numbers table you can use a query like the following:
SELECT d.Date,
h.Hour,
Calls = COUNT(pc.Call_ID)
FROM ( SELECT [Hour] = Number
FROM dbo.Numbers
WHERE Number >= 0
AND Number < 24
) AS h
CROSS JOIN
( SELECT Date = DATEADD(DAY, Number, #TrendStartDate)
FROM dbo.Numbers
WHERE Number <= DATEDIFF(DAY, #TrendStartDate, #TrendEndDate)
) AS d
LEFT JOIN [PHONE_CALLS] AS pc
ON pc.CallType = 'Incoming'
AND pc.Name NOT IN ('DefaultQueue')
AND CAST(pc.ArrivalTime AS DATE) = d.Date
AND DATEPART(HOUR, pc.ArrivalTime) = h.Hour
GROUP BY d.Date, h.Hour
ORDER BY d.Date, h.Hour;
The key is to get all the hours you need:
SELECT [Hour] = Number
FROM dbo.Numbers
WHERE Number >= 0
AND Number < 24
And all the days that you need in your range:
SELECT Date = DATEADD(DAY, Number, #TrendStartDate)
FROM dbo.Numbers
WHERE Number < DATEDIFF(DAY, #TrendStartDate, #TrendEndDate)
Then cross join the two, so that you are guaranteed to have all 24 hours for each day you want. Finally, you can left join to your call table to get the count of calls.
Example on DB<>Fiddle
You can use SQL SERVER recursivity with CTE to generate the hours between 0 and 23 and then a left outer join with the call table
You also use any other Method mentioned in this link to generate numbers from 0 to 23
Link to SQLFiddle
set dateformat ymd
declare #calls as table(date date,hour int,calls int)
insert into #calls values('2020-01-02',0,66),('2020-01-02',1,888),
('2020-01-02',2,5),('2020-01-02',3,8),
('2020-01-02',4,9),('2020-01-02',5,55),('2020-01-02',6,44),('2020-01-02',7,87),('2020-01-02',8,90),
('2020-01-02',9,34),('2020-01-02',10,22),('2020-01-02',11,65),('2020-01-02',12,54),('2020-01-02',13,78),
('2020-01-02',23,99);
with cte as (select 0 n,date from #calls union all select 1+n,date from cte where 1+n <24)
select distinct(cte.date),cte.n [Hour],isnull(ca.calls,0) calls from cte left outer join #calls ca on cte.n=ca.hour and cte.date=ca.date

Oracle left join behaving like inner join [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed 4 years ago.
Improve this question
I have Two tables:
Dates:
|ReportHeader|
--------------
| 2015-04|
| 2015-05|
| 2015-06|
Data:
|ReportHeader|Customer|Sales|
-----------------------------
| 2015-04| Plop| 684|
| 2015-06| Plop| 486|
I have my query:
select
Dates.ReportHeader,
Data.Customer,
Data.Sales
from Dates
left outer join data on Dates.ReportHeader = Data.ReportHeader
What I would expect back is:
|ReportHeader|Customer|Sales|
-----------------------------
| 2015-04| Plop| 684|
| 2015-05| null| null|
| 2015-06| Plop| 486|
The results that I am getting are:
|ReportHeader|Customer|Sales|
-----------------------------
| 2015-04| Plop| 684|
| 2015-06| Plop| 486|
Is there any reason to be found why this left join would behave like an inner join?
Or do I just not understand how a left join is supposed to work.
The Oracle version i am using is:
Oracle Database 12c Standard Edition Release 12.1.0.2.0 - 64bit Production
Thanks in advance for any help extended
edit:
It seems i have over simplified the problem so i better put the whole query here:
WITH
L0 AS (SELECT 1 C from dual UNION ALL SELECT 1 O from dual),
L1 AS (SELECT 1 C FROM L0 A CROSS JOIN L0 B),
L2 AS (SELECT 1 C FROM L1 A CROSS JOIN L1 B),
L3 AS (SELECT 1 C FROM L2 A CROSS JOIN L2 B),
L4 AS (SELECT 1 C FROM L3 A CROSS JOIN L3 B),
Nums AS (SELECT 0 N FROM dual union SELECT ROW_NUMBER() OVER ( ORDER BY (SELECT NULL from dual) ) N FROM L4),
Dates as (
select distinct
to_char(to_date(last_day(current_timestamp + (-1 * N))) , 'yyyy-MM') as ReportHeader,
to_date(add_months(last_day(current_timestamp + (-1 * N)),-1)+1) as FirstDayOfMonth,
to_date(current_timestamp + (-1 * N)) as ReportDate,
to_date(last_day(current_timestamp + (-1 * N))) as LastDayOfMonth
from Nums
where N <= 3 * 365
), Invoices AS (
select
TP.ID,
TP.Name,
h.FORMATTEDINVOICENUMBER,
l.SCHEDULEID,
to_date(h.INVOICEDUEDATE) as date_due,
to_date(L.EFFECTIVEDATEFULLYPAID) as date_payed
from ODSTHIRDPARTY TP
LEFT JOIN ODSINVOICELINE L on L.INVCUSID = TP.ID
LEFT JOIN ODSINVOICEHEADER H on h.ID = l.INVOICEHEADERID
where TP.NAME not in ('<None>','<Unknown>')
), data as (
select distinct
Invoices.Name as Customer,
Dates.ReportHeader,
count(distinct Invoices.SCHEDULEID) over (partition by Dates.ReportHeader, Invoices.Name) as NumContracts,
floor(avg(
case
when Invoices.date_due > Dates.LastDayOfMonth THEN 0
when coalesce(Invoices.date_payed, current_date) < Dates.FirstDayOfMonth THEN 0
when Invoices.date_payed < Invoices.date_due then Invoices.date_payed - Invoices.date_due
else to_date(least(coalesce(Invoices.date_payed, current_date), Dates.LastDayOfMonth )) - to_date(Invoices.date_due)
end
) over (partition by Dates.ReportHeader, Invoices.Name)) past_due,
Dates.FirstDayOfMonth as ReportStartDate,
Dates.LastDayOfMonth as ReportEndDate,
coalesce(Invoices.date_payed, current_date) as calc_date
from Dates
left join Invoices on Dates.ReportDate between Invoices.date_due and coalesce(Invoices.date_payed, current_date)
where coalesce(Invoices.date_payed, current_date) >= Invoices.date_due
)
select distinct
Dates.ReportHeader,
data.Customer,
data.NumContracts,
data.past_due
from Dates
left join data on data.ReportHeader = Dates.ReportHeader
order by data.Customer,dates.ReportHeader
The troublesome part is the final query. Select from dates left join on data
For those interested, as #APC was saying in the comments. Create a testable use case. Doing that triggered me to rewrite the complete query that does work now. My initial premise was wrong and i took an initial approach that was wrong.
Revised query below:
WITH
L0 AS (SELECT 1 C from dual UNION ALL SELECT 1 O from dual),
L1 AS (SELECT 1 C FROM L0 A CROSS JOIN L0 B),
L2 AS (SELECT 1 C FROM L1 A CROSS JOIN L1 B),
L3 AS (SELECT 1 C FROM L2 A CROSS JOIN L2 B),
L4 AS (SELECT 1 C FROM L3 A CROSS JOIN L3 B),
Nums AS (SELECT 0 N FROM dual union SELECT ROW_NUMBER() OVER ( ORDER BY (SELECT NULL from dual) ) N FROM L4),
Dates as (
select distinct
to_char(to_date(last_day(current_timestamp + (-1 * N))), 'yyyy-MM') as ReportHeader,
to_date(add_months(last_day(current_timestamp + (-1 * N)), -1) + 1) as FirstDayOfMonth,
to_date(current_timestamp + (-1 * N)) as ReportDate,
to_date(last_day(current_timestamp + (-1 * N))) as LastDayOfMonth
from Nums
where N <= 2 * 365
), ThirdParties as (
select distinct
TP.ID,
TP.Name,
Dates.ReportHeader,
Dates.FirstDayOfMonth,
Dates.ReportDate,
Dates.LastDayOfMonth
from ODSTHIRDPARTY TP
cross join Dates
where TP.NAME not in ('<None>','<Unknown>')
), Invoices AS (
select distinct
TP.ID,
TP.Name,
TP.ReportHeader,
TP.FirstDayOfMonth,
OH.FORMATTEDINVOICENUMBER,
TP.LastDayOfMonth,
il.SCHEDULEID,
to_date(il.invoiceReferenceDate) as date_due,
to_date(iL.EFFECTIVEDATEFULLYPAID) as date_payed,
case
when il.id is null then 0
else
case
when to_date(il.invoiceReferenceDate) <= to_date(coalesce(iL.EFFECTIVEDATEFULLYPAID,least(current_date,tp.LastDayOfMonth))) then
to_date(coalesce(iL.EFFECTIVEDATEFULLYPAID,least(current_date,tp.LastDayOfMonth))) - to_date(il.invoiceReferenceDate)
else 0
end
end as daysLate
from ThirdParties TP
LEFT JOIN ODSINVOICELINE IL on IL.INVCUSID = TP.ID and TP.ReportDate
between il.invoiceReferenceDate and coalesce(il.EFFECTIVEDATEFULLYPAID, current_date)
left join ODSINVOICEHEADER OH on IL.INVOICEHEADERID = OH.ID
), data as (
select distinct
Invoices.ReportHeader,
Invoices.Name as Customer,
count(distinct Invoices.SCHEDULEID) over (partition by Invoices.ReportHeader, Invoices.Name) as NumContracts,
floor(avg(
case
when Invoices.date_due > Invoices.LastDayOfMonth THEN 0
when coalesce(Invoices.date_payed, current_date) < Invoices.FirstDayOfMonth THEN 0
when Invoices.date_payed < Invoices.date_due then Invoices.date_payed - Invoices.date_due
else to_date(least(coalesce(Invoices.date_payed, current_date), Invoices.LastDayOfMonth )) - to_date(Invoices.date_due)
end
) over (partition by Invoices.ReportHeader, Invoices.Name)) past_due,
Invoices.FirstDayOfMonth as ReportStartDate,
Invoices.LastDayOfMonth as ReportEndDate,
coalesce(Invoices.date_payed, current_date) as calc_date
from Invoices
-- and coalesce(Invoices.date_payed, current_date) >= Invoices.date_due
order by Invoices.Name, Invoices.reportheader
), upvt as (
select distinct
row_number()
over (
partition by customer
order by ReportHeader ) as ColNum,
data.ReportHeader ColName,
data.Customer,
data.NumContracts,
data.past_due
from data
), pvt as (
select * from (
select Customer, ColName, ColNum, NumContracts, past_due from upvt
) pivot (
MAX(past_due) as DueDays, MAX(NumContracts) as Contracts, Max(ColName) as ColName
for ColNum in ( '1', '2', '3', '4', '5', '6', '7', '8', '9','10','11','12','13','14','15',
'16','17','18','19','20','21','22','23','24')) pvt
)
select
row_number() over (order by Customer) as No,
pvt.*
from pvt;
use below query
select
Dates.ReportHeader,
Data.Customer,
Data.Sales
from
Dates, Data
where
Dates.ReportHeader = Data.ReportHeader(+)

SQL - Select values from a table based on dates using incrementing dates

I have a SQL table of dates (MM/DD format), targets, and levels, as such:
Date Target Level
10/2 1000 1
10/4 2000 1
10/7 2000 2
I want to use those dates as tiers, or checkpoints, for when to use the respective targets and levels. So, anything on or after those dates (until the next date) would use that target/level. Anything before the first date just uses the values from the first date.
I want to select a range of dates (a 5 week range of dates, with the start date and end date of the range being determined by the current day: 3 weeks back from today, to 2 weeks forward from today) and fill in the targets and levels accordingly, as such:
Date Target Level
10/1 1000 1
10/2 1000 1
10/3 1000 1
10/4 2000 1
10/5 2000 1
10/6 2000 1
10/7 2000 2
10/8 2000 2
...
11/5 2000 2
How do I go about:
Selecting the range of dates (as efficiently as possible)
Filling in the range of dates with the respective target/level from the appropriate date in my table?
Thank you.
You can do this using outer apply. The following creates a list of dates using a recursive CTE:
with d as (
select cast(getdate() as date) as dte
union all
select dateadd(day, -1, dte)
from d
where dte >= getdate() - 30
select d.dte, t.target, t.level
from d outer apply
(select top 1 t.*
from t
where d.dte >= t.dte
order by t.dte desc
);
you can use a CTE to generate your 'missing' dates, then use a CROSS APPLY to obtain the target and level that was last active (by querying the TOP 1 DESC where the date is on or before current date) - finally I introduced 'maximum date' as a variable
DECLARE #MAXD as DATETIME = '20161105';
WITH DATS AS (SELECT MIN([Date]) D FROM dbo.YourTab
UNION ALL
SELECT dateadd(day,1,D) FROM DATS WHERE D < #MAXD)
select DATS.D, CA.Target, CA.Level from DATS
CROSS APPLY
(SELECT TOP 1 Y.Target, Y.Level FROM YourTab Y
WHERE
Y.[Date] <= DATS.D
ORDER BY Y.Date DESC) CA
option (maxrecursion 0);
I made a bit of a change with dates to go back 3 and forward two weeks - also I switched to outer apply to handle no data in force
DECLARE #MIND as DATETIME = dateadd(week,-3,cast(getdate() as date));
DECLARE #MAXD as DATETIME = dateadd(week, 5,#MIND);
WITH DATS AS (SELECT #MIND D
UNION ALL
SELECT dateadd(day,1,D) FROM DATS WHERE D < #MAXD)
select DATS.D, CA.Target, CA.Level from DATS
OUTER APPLY
(SELECT TOP 1 Y.Target, Y.Level FROM YourTab Y WHERE Y.[Date] <= DATS.D ORDER BY Y.Date DESC) CA
ORDER BY DATS.D
option (maxrecursion 0);
Final change - if there is no earlier value for the date - take first future row
DECLARE #MIND as DATETIME = dateadd(week,-3,cast(getdate() as date));
DECLARE #MAXD as DATETIME = dateadd(week, 5,#MIND);
WITH DATS AS (SELECT #MIND D
UNION ALL
SELECT dateadd(day,1,D) FROM DATS WHERE D < #MAXD)
select DATS.D, COALESCE(CA.Target, MQ.Target) Target , COALESCE(CA.Level, MQ.Level) Level from DATS
OUTER APPLY
(SELECT TOP 1 Y.Target, Y.Level FROM YourTab Y WHERE Y.[Date] <= DATS.D ORDER BY Y.Date DESC) CA
OUTER APPLY
(
SELECT TOP 1 M.Target, M.Level FROM YourTab M ORDER BY M.[Date] ASC
) MQ
ORDER BY DATS.D
option (maxrecursion 0);
I don't know why you store dates as MM/DD but you need some conversion into right datatype. This could do a trick:
;WITH YourTable AS (
SELECT *
FROM (VALUES
('10/2', 1000, 1),
('10/4', 2000, 1),
('10/7', 2000, 2)
) as t([Date], [Target], [Level])
), dates_cte AS ( --this CTE is generating dates you need
SELECT DATEADD(week,-3,GETDATE()) as d --3 weeks back
UNION ALL
SELECT dateadd(day,1,d)
FROM dates_cte
WHERE d < DATEADD(week,2,GETDATE()) --2 weeks forward
)
SELECT REPLACE(CONVERT(nvarchar(5),d,101),'/0','/') as [Date],
COALESCE(t.[Target],t1.[Target]) [Target],
COALESCE(t.[Level],t1.[Level]) [Level]
FROM dates_cte dc
OUTER APPLY ( --Here we got PREVIOUS values
SELECT TOP 1 *
FROM YourTable
WHERE CONVERT(datetime,REPLACE([Date],'/','/0')+'/2016',101) <= dc.d
ORDER BY CONVERT(datetime,REPLACE([Date],'/','/0')+'/2016',101) DESC
) t
OUTER APPLY ( --Here we got NEXT values and use them if there is no PREV
SELECT TOP 1 *
FROM YourTable
WHERE CONVERT(datetime,REPLACE([Date],'/','/0')+'/2016',101) >= dc.d
ORDER BY CONVERT(datetime,REPLACE([Date],'/','/0')+'/2016',101) ASC
) t1
Output:
Date Target Level
10/5 2000 1
10/6 2000 1
10/7 2000 2
10/8 2000 2
10/9 2000 2
10/10 2000 2
10/11 2000 2
10/12 2000 2
...
11/9 2000 2
EDIT
With Categories:
;WITH YourTable AS (
SELECT *
FROM (VALUES
('10/2', 1000, 1, 'A'),
('10/4', 3000, 1, 'B'),
('10/7', 2000, 2, 'A')
) as t([Date], [Target], [Level], [Category])
), dates_cte AS (
SELECT DATEADD(week,-3,GETDATE()) as d
UNION ALL
SELECT dateadd(day,1,d)
FROM dates_cte
WHERE d < DATEADD(week,2,GETDATE())
)
SELECT REPLACE(CONVERT(nvarchar(5),d,101),'/0','/') as [Date],
COALESCE(t.[Target],t1.[Target]) [Target],
COALESCE(t.[Level],t1.[Level]) [Level],
c.Category
FROM dates_cte dc
CROSS JOIN (
SELECT DISTINCT Category
FROM YourTable
) c
OUTER APPLY (
SELECT TOP 1 *
FROM YourTable
WHERE CONVERT(datetime,REPLACE([Date],'/','/0')+'/2016',101) <= dc.d
AND c.Category = Category
ORDER BY CONVERT(datetime,REPLACE([Date],'/','/0')+'/2016',101) DESC
) t
OUTER APPLY (
SELECT TOP 1 *
FROM YourTable
WHERE CONVERT(datetime,REPLACE([Date],'/','/0')+'/2016',101) >= dc.d
AND c.Category = Category
ORDER BY CONVERT(datetime,REPLACE([Date],'/','/0')+'/2016',101) ASC
) t1
ORDER BY c.Category, d
Not sure if I'm over simplifying this, but:
select min(X.Date) Date_Range_Start, max(X.date) Date_Range_End
, V.<value_date>
, isnull(X.Target, 'Out of range') Target
, isnull(X.Level, 'Out of range') Level
from X --replace this with your table
left join <value_table> V --table with dates to be assessed
on V.<Date> between X.Date_Range_Start and X.Date_Range_End
group by Target, Level, V.<value_date>