Display All Quarters based on Year In SQL Server - sql

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];

Related

In the same column, compare each value with previous multiple values with condition

I'm working on a table looks like this. The actual dataset contains thousand of Guest_ID, and I'm just showing few sample lines here.
Guest_ID
Visit_ID
Collection_time
Value
6
a178
2007-11-09 11:28:00
2.6
6
a188
2007-11-10 20:28:00
6.6
12
a278
2008-11-11 10:28:00
2.7
12
a278
2008-11-11 11:38:00
3.2
12
a278
2008-11-12 11:48:00
6.8
12
c348
2009-10-12 11:38:00
3.8
15
e179
2013-01-15 09:25:00
1.8
15
e179
2013-01-15 10:26:00
1.6
15
e179
2013-01-15 12:15:00
3.8
15
e179
2013-01-17 09:25:00
3.6
What I'm trying to do here is to find out the values that had increased by at least 3 within the past 48hr, and these values need to be under the same visit_id. In this case, result should only return
Guest_ID
Visit_ID
Collection_time
Value
12
a278
2008-11-12 11:48:00
6.8
I have some vague ideas of creating islands and gaps in SQL Server, but not sure how to approach it. Conceptually, for each value X, I need to extract all the previous value meets the conditions (within past 48hr AND under the same Visit_ID), then check if X - min(previous value) >= 3. And if yes, keep or label X as 1, and repeat the procedure.
I read a lot of posts like using lag() or row_number() over (partition by ... order by ...), but still unsure about what to do. Any help is appreciated!
This would have been a good spot to use window functions with a date range specification. Alas, SQL Server does not support that (yet?).
The simplest approach might be exists and a correlated subquery:
select t.*
from mytable t
where exists (
select 1
from mytable t1
where
t1.visit_id = t.visit_id
and t1.collection_time >= dateadd(day, -2.collection_time)
and t1.collection_time < t.collection_time
and t1.value < t.value - 3
)
Or you can use cross apply:
select t.*
from mytable t
cross apply (
select min(t1.value) as min_value
from mytable t1
where
t1.visit_id = t.visit_id
and t1.collection_time >= dateadd(day, -2.collection_time)
and t1.collection_time < t.collection_time
) t1
where t1.min_value < t.value - 3
I used a CTE to filter out the qualifying rows first and then just join it back up to the original table to grab those rows:
CREATE TABLE #tmp(Guest_ID int, Visit_ID varchar(10), Collection_time datetime, Value decimal(10,1))
INSERT INTO #tmp VALUES
(6, 'a178', '2007-11-09 11:28:00', 2.6),
(6, 'a188', '2007-11-10 20:28:00', 6.6),
(12, 'a278', '2008-11-11 10:28:00', 2.7),
(12, 'a278', '2008-11-11 11:38:00', 3.2),
(12, 'a278', '2008-11-12 11:48:00', 6.8),
(12, 'c348', '2009-10-12 11:38:00', 3.8),
(15, 'e179', '2013-01-15 09:25:00', 1.8),
(15, 'e179', '2013-01-15 10:26:00', 1.6),
(15, 'e179', '2013-01-15 12:15:00', 3.8),
(15, 'e179', '2013-01-17 09:25:00', 3.6)
;WITH CTE AS(
SELECT MAX(Collection_time) MaxCollection_Time, Max(Value) - Min(Value) DiffInValue ,Visit_ID
FROM #tmp
GROUP BY Visit_ID
HAVING Max(Value) - Min(Value) >= 3
)
SELECT t1.*
FROM #tmp t1
INNER JOIN CTE t2 on t1.Visit_ID = t2.Visit_ID and T1.Collection_time = t2.MaxCollection_Time

Calculating missing months

I use the following query in ordre to filling missing months
Declare #Sample Table(year int, month int,product as nvarchar(50), qty_ytd int);
Insert #Sample(year, month, qty_ytd) Values
(2017, 01,'book', 20),
(2017, 02, 'pc', 30),
(2018, 01, 'book', 50);
;With Months As
(Select 1 As month
Union All
Select month + 1 From Months Where month < 12)
, YearsAndMonths As
(Select distinct year,m.month from #Sample cross join Months m)
select ym.*, coalesce(s.qty_ytd, s2.qty_ytd) qty_ytd, coalesce(s.qty_ytd, 0) QTY from YearsAndMonths ym
left join #sample s on ym.year = s.year and ym.month = s.month
left join (select qty_ytd, year,
row_number() over (partition by year order by month desc) rn
from #Sample) s2 on ym.year = s2.year and rn = 1
How could I add 'product ' as well ?
Firstly, I would recommend creating a calendar table since this pops up as a use case every once in a while. A quick example can be found here
Now, once you have the calendar table (let's call it static.calendar) ready, the code is fairly simple as follows:
with Products
as
(
SELECT distinct product
FROM #Sample
),
TimeRange
as
(
SELECT DISTINCT year,
month
FROM static.calendar
)
ProductTimeRange
as
(
SELECT p.products,
tr.year,
tr.month
FROM Products as p
CROSS JOIN TimeRange as tr
)
SELECT ptr.products,
ptr.year,
ptr.month,
s.qty_ytd
FROM ProductTimeRange as ptr
LEFT JOIN #sample as s
ON ptr.products = s.products
AND ptr.year = s.year
AND ptr.month = s.month
ORDER BY ptr.products,
ptr.year,
ptr.month
Use a cross join to generate the rows that you want -- all the years, months, and products.
Then use left join to bring in the data you want:
With Months As (
Select 1 As month
Union All
Select month + 1
From Months
Where month < 12
)
select y.year, m.month, s.product, coalesce(qty_ytd, 0) as qty_ytd
from (select distinct year from #sample) y cross join
months m cross join
(select distinct product from #sample) p left join
#sample s
on s.year = y.year and s.month = m.month and s.product = p.product;
Here is a db<>fiddle.

SQL Addition Formula

Noob alert...
I have an example table as followed.
I am trying to create a column in SQL that shows the what percentage each customer had of size S per year.
So output should be something like:
(Correction: the customer C for 2019 Percentage should be 1)
Window functions will get you there.
DECLARE #TestData TABLE
(
[Customer] NVARCHAR(2)
, [CustomerYear] INT
, [CustomerCount] INT
, [CustomerSize] NVARCHAR(2)
);
INSERT INTO #TestData (
[Customer]
, [CustomerYear]
, [CustomerCount]
, [CustomerSize]
)
VALUES ( 'A', 2017, 1, 'S' )
, ( 'A', 2017, 1, 'S' )
, ( 'B', 2017, 1, 'S' )
, ( 'B', 2017, 1, 'S' )
, ( 'B', 2018, 1, 'S' )
, ( 'A', 2018, 1, 'S' )
, ( 'C', 2017, 1, 'S' )
, ( 'C', 2019, 1, 'S' );
SELECT DISTINCT [Customer]
, [CustomerYear]
, SUM([CustomerCount]) OVER ( PARTITION BY [Customer]
, [CustomerYear]
) AS [CustomerCount]
, SUM([CustomerCount]) OVER ( PARTITION BY [CustomerYear] ) AS [TotalCount]
, SUM([CustomerCount]) OVER ( PARTITION BY [Customer]
, [CustomerYear]
) * 1.0 / SUM([CustomerCount]) OVER ( PARTITION BY [CustomerYear] ) AS [CustomerPercentage]
FROM #TestData
ORDER BY [CustomerYear]
, [Customer];
Will give you
Customer CustomerYear CustomerCount TotalCount CustomerPercentage
-------- ------------ ------------- ----------- ---------------------------------------
A 2017 2 5 0.400000000000
B 2017 2 5 0.400000000000
C 2017 1 5 0.200000000000
A 2018 1 2 0.500000000000
B 2018 1 2 0.500000000000
C 2019 1 1 1.000000000000
Assuming there are no duplicate rows for a customer in a year, you can use window functions:
select t.*,
sum(count) over (partition by year) as year_cnt,
count * 1.0 / sum(count) over (partition by year) as ratio
from t;
Break it apart into tasks - that's probably the best rule to follow when it comes to SQL. So, I created a variable table #tmp which I populated with your sample data, and started out with this query:
select
customer,
year
from #tmp
where size = 'S'
group by customer, year
... this gets a row for each customer/year combo for 'S' entries.
Next, I want the total count for that customer/year combo:
select
customer,
year,
SUM(itemCount) as customerItemCount
from #tmp
where size = 'S'
group by customer, year
... now, how do we get the count for all customers for a specific year? We need a subquery - and we need that subquery to reference the year from the main query.
select
customer,
year,
SUM(itemCount) as customerItemCount,
(select SUM(itemCount) from #tmp t2 where year=t.year) as FullTotalForYear
from #tmp t
where size = 'S'
GROUP BY customer, year
... that make sense? That new line in the ()'s is a subquery - and it's hitting the table again - but this time, its just getting a SUM() over the particular year that matches the main table.
Finally, we just need to divide one of those columns by the other to get the actual percent (making sure not to make it int/int - which will always be an int), and we'll have our final answer:
select
customer,
year,
cast(SUM(itemCount) as float) /
(select SUM(itemCount) from #tmp t2 where year=t.year)
as PercentageOfYear
from #tmp t
where size = 'S'
GROUP BY customer, year
Make sense?
With a join of 2 groupings:
the 1st by size, year, customer and
the 2nd by size, year.
select
t.customer, t.year, t.count, t.size,
ty.total_count, 1.0 * t.count / ty.total_count percentage
from (
select t.customer, t.year, sum(t.count) count, t.size
from tablename t
group by t.size, t.year, t.customer
) t inner join (
select t.year, sum(t.count) total_count, t.size
from tablename t
group by t.size, t.year
) ty
on ty.size = t.size and ty.year = t.year
order by t.size, t.year, t.customer;
See the demo

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>

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

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