How can I expand a table with a range between two dates as a new column in SQL Server? - sql

I have this table for example:
Start date
End date
value
2022-01-01
2022-01-03
value1
2022-01-02
2022-01-04
value2
The output I want would be this:
Start date
End date
value
Date between
2022-01-01
2022-01-03
value1
2022-01-01
2022-01-01
2022-01-03
value1
2022-01-02
2022-01-01
2022-01-03
value1
2022-01-03
2022-01-02
2022-01-04
value2
2022-01-02
2022-01-02
2022-01-04
value2
2022-01-03
2022-01-02
2022-01-04
value2
2022-01-04
Thank you in advance!

As already suggested, you need a calendar table.
Here is how you can create one
create table calendar (id int identity, cdate date not null)
and fill it one time like this (choose a enddate far enough in the future, and a startdate far enough in the past so you won't have to add rows to this table anymore
;WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT 0 AS I
UNION ALL --startdate enddate
SELECT TOP (DATEDIFF(DAY, '20220101', '20220301'))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3), --up to 1,000 days
Dates AS(
SELECT DATEADD(DAY, T.I, '20220101') AS Date
FROM Tally T)
insert into calendar (cdate)
SELECT D.Date
FROM Dates D
So now you have a table called calendar where you can join on, the query you need is now very simple
select t.startdate,
t.enddate,
t.value,
c.cdate as datebetween
from mytable t
left join calendar c on c.cdate >= t.startdate
and c.cdate <= t.enddate
Click on this DBFiddle to see how it works

Related

snowflake sql: sum for each day between two dates

I hope someone can help. Suppose I have this table
id
actual_date
target_date
qty
1
2022-01-01
2022-01-01
2
2
2022-01-02
2022-01-01
1
3
2022-01-03
2022-01-01
3
4
2022-01-03
2022-01-02
1
5
2022-01-03
2022-01-03
2
what i would like to calculate is the qty that has to be processed on each date.
E.g. on the target date 2022-01-01 the quota qty is 6 (2+1+3).
On the 2.1.2022 i would also have to add the qtys that havent been processed on the day before, which means id 2 because the actual date is 2022-01-02 (so after the target date) and id 3. The quota qty for the 2022-01-02 is then 1+3+1.
And for the 2022-01-03 is 6 = 2+1+3, because id 3 has an actual date on 2022-01-02 (it wasnt processed neither on 01-01 nor on 01-02 and id 4 wasnt processed on 01-02.
Here's what the desired output would look like:
target_date
qty_qouta
2022-01-01
6
2022-01-02
4
2022-01-03
6
Hopefully this gets you started ... recommend testing heaps more edge cases, the business rules don't quite feel right to me -> as you don't seem to show when actual>target. But hope this helps.
WITH CTE AS( SELECT 1 ID, '2022-01-01'::DATE ACTUAL_DATE,'2022-01-01'::DATE TARGET_DATE, 2 QTY
UNION ALL SELECT 2 ID, '2022-01-02'::DATE ACTUAL_DATE,'2022-01-01'::DATE TARGET_DATE, 1 QTY
UNION ALL SELECT 3 ID, '2022-01-03'::DATE ACTUAL_DATE,'2022-01-01'::DATE TARGET_DATE, 3 QTY
UNION ALL SELECT 4 ID, '2022-01-03'::DATE ACTUAL_DATE,'2022-01-02'::DATE TARGET_DATE, 1 QTY
UNION ALL SELECT 5 ID, '2022-01-03'::DATE ACTUAL_DATE,'2022-01-03'::DATE TARGET_DATE, 2 QTY
)
,CTE2 AS(SELECT
ACTUAL_DATE D
, SUM(QTY) ACTUAL_QTY
FROM CTE GROUP BY 1)
,CTE3 AS(SELECT
TARGET_DATE D
, SUM(QTY) TARGET_QTY
FROM CTE GROUP BY 1)
SELECT
D DATE
,ACTUAL_QTY
,TARGET_QTY
,TARGET_QTY-ACTUAL_QTY DELTA
,ZEROIFNULL(LAG(DELTA)OVER(PARTITION BY 1 ORDER BY D))GHOST
,GREATEST(TARGET_QTY,DELTA+GHOST,ACTUAL_QTY)VOLIA
FROM
CTE2 FULL OUTER JOIN CTE3 USING(D);

Generate multiples rows of new column based on one value of another column

I have a table like below:
ID
Date
1
2022-01-01
2
2022-03-21
I want to add a new column based on the date and it should look like this
ID
Date
NewCol
1
2022-01-01
2022-02-01
1
2022-01-01
2022-03-01
1
2022-01-01
2022-04-01
1
2022-01-01
2022-05-01
2
2022-03-21
2022-04-21
2
2022-03-21
2022-05-21
Let's say that there is a #EndDate = 2022-05-31 (that's where it should stop)
I'm having a hard time trying to figure out how to do it in SSMS. Would appreciate any insights! Thanks :)
In the following solutions we leverage string_split with combination with replicate to generate new records.
select ID
,Date
,dateadd(month, row_number() over(partition by ID order by (select null)), Date) as NewCol
from (
select *
from t
outer apply string_split(replicate(',',datediff(month, Date, '2022-05-31')-1),',')
) t
ID
Date
NewCol
1
2022-01-01
2022-02-01
1
2022-01-01
2022-03-01
1
2022-01-01
2022-04-01
1
2022-01-01
2022-05-01
2
2022-03-21
2022-04-21
2
2022-03-21
2022-05-21
Fiddle
For SQL in Azure and SQL Server 2022 we have a cleaner solution based on [ordinal][4].
"The enable_ordinal argument and ordinal output column are currently
supported in Azure SQL Database, Azure SQL Managed Instance, and Azure
Synapse Analytics (serverless SQL pool only). Beginning with SQL
Server 2022 (16.x) Preview, the argument and output column are
available in SQL Server."
select ID
,Date
,dateadd(month, ordinal, Date) as NewCol
from (
select *
from t
outer apply string_split(replicate(',',datediff(month, Date, '2022-05-31')-1),',',1)
) t
with cal (id, dt) as
(
select id, date as dt from t
union all select id, dateadd(month, 1, dt) from cal where month(dt) < month('2022-05-31')
)
select t.id
,t.date
,cal.dt as new_col
from cal join t on t.id = cal.id and t.date != cal.dt
order by id, new_col
id
date
new_col
1
2022-01-01
2022-02-01
1
2022-01-01
2022-03-01
1
2022-01-01
2022-04-01
1
2022-01-01
2022-05-01
2
2022-03-21
2022-04-21
2
2022-03-21
2022-05-21
Fiddle
There are many ways to "explode" a row into a set, the simplest in my opinion is a recursive CTE:
DECLARE #endpoint date = '20220531';
DECLARE #prev date = DATEADD(MONTH, -1, #endpoint);
WITH x AS
(
SELECT ID, date, NewCol = DATEADD(MONTH, 1, date) FROM #d
UNION ALL
SELECT ID, date, DATEADD(MONTH, 1, NewCol) FROM x
WHERE NewCol < #prev
)
SELECT * FROM x
ORDER BY ID, NewCol;
Working example in this fiddle.
Keep in mind that if you could have > 100 months you'll need to add OPTION (MAXRECURSION) (or just consider using a different solution at scale).

How to fill missing values for missing dates with value from date before in sql bigquery? [duplicate]

This question already has an answer here:
Create Balance Sheet with every date is filled in Bigquery
(1 answer)
Closed 8 months ago.
Hi I have a product table with daily price, the catch here is that for the table only updates if there's a price change, and for the dates in between will not be written into the table because the price is the same as the day before.
How do I fill missing values of price with the last entry of date before?
date
id
price
2022-01-01
1
5
2022-01-03
1
6
2022-01-05
1
7
2022-01-01
2
10
2022-01-02
2
11
2022-01-06
2
12
into
date
id
price
2022-01-01
1
5
2022-01-02
1
5
2022-01-03
1
6
2022-01-04
1
6
2022-01-05
1
7
2022-01-01
2
10
2022-01-02
2
11
2022-01-03
2
11
2022-01-04
2
11
2022-01-05
2
11
2022-01-06
2
12
I am currently thinking of creating a table for dates and joining and using lag function. Anyone can help?
select
date,id,
case
when price is null then nullPrice
else price
end as price
from(
select *,
Lag(price, 1) OVER(.
ORDER BY date,id ASC) AS nullPrice
from price_table
join date_table using(date)
)
Consider below:
WITH days_by_id AS (
SELECT id, GENERATE_DATE_ARRAY(MIN(date), MAX(date)) days
FROM sample
GROUP BY id
)
SELECT date, id,
IFNULL(price, LAST_VALUE(price IGNORE NULLS) OVER (PARTITION BY id ORDER BY date)) AS price
FROM days_by_id, UNNEST(days) date LEFT JOIN sample USING (id, date);
output :
You can use generate_date_array function for this
with date_arr
as(
select *
from unnest(generate_date_array('2022-01-01', '2022-05-01')) as dt)
select da.dt, t1.*
from date_arr da
left outer join table1 t1
on da.dt = t1.dt
You can replace hardcoded dates with max and min date from table.

Create a row for each date in a range, and add 1 for each day within a date range for a record in SQL

Suppose I have a date range, #StartDate = 2022-01-01 and #EndDate = 2022-02-01, and this is a reporting period.
In addition, I also have customer records, where each customer has a LIVE Date and a ServiceEndDate (or ServiceEndDate = NULL as they are an ongoing customer)
Some customers may have their Live Date and Service end date range extend outside of the reporting period range. I would only want to report for days that they were a customer in the period.
Name
LiveDate
ServiceEndDate
Tom
2021-10-11
2022-01-13
Mark
2022-11-13
2022-02-15
Andy
2022-01-02
2022-02-10
Rob
2022-01-09
2022-01-14
I would like to create a table where column A is the Date (iterating between every date in the reporting period) and column B is a sum of the number of customers that were a customer on that date.
Something like this
Date
NumberOfCustomers
2022-01-01
2
2022-01-02
3
2022-01-03
3
2022-01-04
3
2022-01-05
3
2022-01-06
3
2022-01-07
3
2022-01-08
3
2022-01-09
4
2022-01-10
4
2022-01-11
4
2022-01-12
4
2022-01-13
4
2022-01-14
3
2022-01-15
3
And so on until the end the #EndDate
Any help would be much appreciated, thanks.
You can join your table to a calendar table containing all the dates you need:
with calendar as
(select cast('2022-01-01' as datetime) as d
union all select dateadd(day, 1, d)
from calendar
where d < '2022-02-01')
select d as "Date", count(*) as NumberOfCustomers
from calendar inner join table_name
on d between LiveDate and coalesce(ServiceEndDate, '9999-12-31')
group by d;
Fiddle
I would personally suggest using a Tally, rather than an rCTE, as a Tally is significantly more performant.
SELECT *
INTO dbo.YourTable
FROM (VALUES('Tom ',CONVERT(date,'2021-10-11 '),CONVERT(date,'2022-01-13')),
('Mark',CONVERT(date,' 2022-11-13'),CONVERT(date,' 2022-02-15')),
('Andy',CONVERT(date,' 2022-01-02'),CONVERT(date,' 2022-02-10')),
('Rob ',CONVERT(date,'2022-01-09 '),CONVERT(date,'2022-01-14')))V(Name,LiveDate,ServiceEndDate);
GO
SELECT *
FROM dbo.YourTable;
GO
DECLARE #StartDate date = '20220101',
#EndDate date = '20220201';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT 0 AS I
UNION ALL
SELECT TOP (DATEDIFF(DAY, #StartDate, #EndDate))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2, N N3), --up to 1,000 days
Dates AS(
SELECT DATEADD(DAY, T.I, #StartDate) AS Date
FROM Tally T)
SELECT D.Date,
COUNT(YT.[Name]) AS NumberOfCustomers
FROM Dates D
LEFT JOIN dbo.YourTable YT ON D.[Date] >= YT.LiveDate
AND (D.[Date] <= YT.ServiceEndDate
OR YT.ServiceEndDate IS NULL)
GROUP BY D.[Date]
ORDER BY D.[Date];
GO
DROP TABLE dbo.YourTable;
Note that then results don't reflect your expected results, I suspect your expected results are wrong. For example you have 2 people live on 2022-01-01, however, there is only 1 person who is live on that date: Tom.
This solution will also never have Mark as "live" (the rCTE method in the other answer won't either) as their end date is before their Live date. If someone can have their service end before it started, I would suggest you have a data quality issue, and you should be adding a CHECK CONSTRAINT to the table to ensure that value of ServiceEndDate is >= LiveDate.

Finding all dates after a date for a variable number of days

I have a list of dates in a table. For this examples the 1st day of each month. Let's call it table timeperiod with column endTime
endTime
1-1-2019
2-1-2019
3-1-2019
4-1-2019
I want to find all dates x number of days after each date in a list. Lets say x = 4. Then the list should be:
1-1-2019
1-2-2019
1-3-2019
1-4-2019
2-1-2019
2-2-2019
2-3-2019
2-4-2019
3-1-2019
3-2-2019
3-3-2019
3-4-2019
4-1-2019
4-2-2019
4-3-2019
4-4-2019
I have found solutions to find all dates between dates but I keep getting "Subquery returned more than 1 value" error when I try to use it with a list of dates.
Here is an example of something I tried but doesn't work
declare #days DECIMAL = 4
declare #StartDate date = (select convert(varchar, DATEADD(Day, +0, endTime),101) from timeperiod
declare #EndDate date = (select convert(varchar, DATEADD(Day, +#days, endTime),101) from timeperiod;
;WITH cte AS (
SELECT #StartDate AS myDate
UNION ALL
SELECT DATEADD(day,1,myDate) as myDate
FROM cte
WHERE DATEADD(day,1,myDate) <= #EndDate
)
SELECT myDate
FROM cte
OPTION (MAXRECURSION 0)
Here is a row generator that generates 5 rows, 0 to 4:
WITH rg AS (
SELECT 0 AS rn
UNION ALL
SELECT rg.rn + 1
FROM rg
WHERE rn < 4
)
Here we join it with your existing table that has firsts of the month and use DATEADD to add rn numbers of days (between 0 and 4) to the endPeriod. CROSS JOINing it caused the rows in timePeriod to repeat 5 times each:
SELECT
DATEADD(DAY, rg.rn, timePeriod.endTime) as fakeEndTime
FROM
rg CROSS JOIN timePeriod
I wasn't really clear when you say "days X days after the date, say x = 4" - to me if there is a day that is 1-Jan-2000, then the date 4 days after this is 5-Jan-2000
If you only want the 1,2,3 and 4 of Jan make the row generator < 3 instead of < 4
Already +1'd on Caius Jard's recursive cte.
Here is yet another option using an ad-hoc tally table in concert with a CROSS JOIN
Example
Declare #YourTable Table ([endTime] date)
Insert Into #YourTable Values
('1-1-2019')
,('2-1-2019')
,('3-1-2019')
,('4-1-2019')
Select NewDate = dateadd(DAY,N-1,EndTime)
From #YourTable A
Cross Join (
Select Top (4) N=row_number() over (order by (select null))
From master..spt_values N1
) B
Returns
NewDate
2019-01-01
2019-01-02
2019-01-03
2019-01-04
2019-02-01
2019-02-02
2019-02-03
2019-02-04
2019-03-01
2019-03-02
2019-03-03
2019-03-04
2019-04-01
2019-04-02
2019-04-03
2019-04-04