SqlServer - How to display data of each month including months with no data - sql

Can you please help me in Sql server, I have table from where I'am getting date wise wise data.
Table structure .
Date Amount
-----------------
2019-05-04 16128.00
2019-05-06 527008.00
2019-05-07 407608.00
2019-05-10 407608.00
Above query I want to fill the missing date , My expectation as shown below
Date Amount
-----------------
2019-05-04 16128.00
2019-05-05 00
2019-05-06 527008.00
2019-05-07 407608.00
2019-05-08 0
2019-05-09 0
2019-05-10 407608.00
Thanks in Advance

You can use calndar help table otherwise assign start date and from date like this DECLARE #fromdate DATE = '20190504', #todate DATE = '20190510' this from date and to date you can change.
CREATE TABLE #amounttable
(
Dt DATE,
Amount BIGINT
);
INSERT into #amounttable(Dt, Amount) VALUES('2019-05-04',16128);
INSERT into #amounttable(Dt, Amount) VALUES('2019-05-06',527008);
INSERT into #amounttable(Dt, Amount) VALUES('2019-05-07',407608);
INSERT into #amounttable(Dt, Amount) VALUES('2019-05-10',407608);
DECLARE #fromdate DATE = '20190504', #todate DATE = '20190510';
SELECT c.d as Date, Amount = COALESCE(s.Amount,0)
FROM
(
SELECT TOP (DATEDIFF(DAY, #fromdate, #todate)+1)
DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY number)-1, #fromdate)
FROM [master].dbo.spt_values
WHERE [type] = N'P' ORDER BY number
) AS c(d)
LEFT OUTER JOIN #amounttable AS s
ON c.d = s.Dt
WHERE c.d >= #fromdate
AND c.d < DATEADD(DAY, 1, #todate);
for more reference check the answer here from stack exchange

One pretty simple method is to use a recursive CTE to generate the dates and then a left join to bring in the days:
with cte as (
select min(t.dte) as dte, max(t.dte) as maxdate
from t
union all
select dateadd(day, 1, dte), maxdate
from cte
where dte < maxdate
)
select cte.dte, coalesce(t.amount, 0) as amount
from cte left join
t
on cte.dte = t.dte;
Here is a db<>fiddle.
Note that the default depth for recursion is 100, so for longer periods you should add OPTION (MAXRECURSION 0).

Related

How to insert values based on another column value

Below is a subset of my table (for the first id)
date
id
value
01/01/2022
1
5
08/01/2022
1
2
For each id, the dates are not consecutive (e.g., for id 1, the min date is 01/01/2022 and the max date is 08/01/2022)--there are 7 days in between both dates. I want to insert rows to make the dates for each id consecutive and contiguous - the value for the value field/column to be filled with 0s so that the updated table looks like:
date
id
value
01/01/2022
1
5
02/01/2022
1
0
03/01/2022
1
0
04/01/2022
1
0
05/01/2022
1
0
06/01/2022
1
0
07/01/2022
1
0
08/01/2022
1
2
Any SQL code on how to implement this would be highly appreciated. I have a calendar table but am unsure how to join it with the above table so that I fill in missing dates dynamically for each id with 0s.
My calendar table looks like:
date
01/01/2022
02/01/2022
03/01/2022
04/01/2022
Considering you state you have a calendar table, it seems what you need to do with JOIN to it with the MIN and MAX dates from your other table, and the LEFT JOIN back to your table:
WITH MinMax AS(
SELECT ID,
MIN(date) AS MinDate,
MAX(date) AS MaxDate
FROM dbo.YourTable
GROUP BY ID),
Dates AS(
SELECT MM.ID,
C.CalendarDate AS [Date]
FROM MinMax MM
JOIN dbo.CalendarTable C ON MM.MinDate <= C.CalendarDate
AND MM.MaxDate >= C.CalendarDate)
SELECT D.ID,
D.[Date],
ISNULL(YT.[Value],0) AS [Value]
FROM Dates D
LEFT JOIN dbo.YourTable YT ON D.ID = YT.ID
AND D.[Date] = YT.[Date];
SET DATEFORMAT DMY
-- CREATE A TABLE WITH OUR INPUT DATA
DROP TABLE IF EXISTS #TheData
GO
CREATE TABLE #TheData
(TheDate DATE, id INT, TheValue INT)
INSERT INTO #TheData
(TheDate,id,Thevalue)
VALUES
('01/01/2022',1,5),
('08/01/2022',1,2),
('17/01/2022',2,7),
('25/01/2022',2,7),
('15/02/2022',2,7)
-- CREATE A CALENDAR CTE
DECLARE #StartDate date = '20210101';
DECLARE #CutoffDate date = DATEADD(DAY, -1, DATEADD(YEAR, 2, #StartDate));
;WITH DateSeq(TheDate) AS
(
SELECT #StartDate
UNION ALL
SELECT DATEADD(dd,1,TheDate) FROM DateSeq
WHERE TheDate < #CutoffDate
)
-- CROSS JOIN OUR CALENDAR CTE TO OUR SOURCE DATA. DERIVED TABLE TO GET FIRST AND LAST OF EACH RANGE TO USE FOR JOIN
SELECT
ds.*
,SourceDataRangesByID.ID
,ISNULL(td.TheValue,0) AS TheValue
FROM
DateSeq ds
CROSS JOIN
(
SELECT
d.ID
,MIN(d.TheDate) AS MinDatePerID
,MAX(d.TheDate) AS MaxDatePerID
FROM #TheData d
GROUP BY d.ID
) SourceDataRangesByID
LEFT JOIN #TheData td ON td.id = SourceDataRangesByID.ID AND td.TheDate = ds.TheDate
WHERE ds.TheDate >= SourceDataRangesByID.MinDatePerID
AND ds.TheDate <= SourceDataRangesByID.MaxDatePerID
OPTION (MAXRECURSION 0);
try the generate_series to create a date table then right join with it and coalesce for the non null value
SELECT generate_series('2016-01-01', -- series start date
'2018-06-30', -- series end date
'1 day'::interval)::date AS day) AS daily_series
from mytable
See Generate_Series for TSQL
https://dba.stackexchange.com/questions/255165/does-ms-sql-server-have-generate-series-function
(Sql server 2022)
https://learn.microsoft.com/en-us/sql/t-sql/functions/generate-series-transact-sql?view=sql-server-ver16

Join Generated Date Sequence

Currently I'm trying to join a date table to a ledger table so I can fill the gaps of the ledger table whenever there are no transactions in certain instances (e.g. there are transactions on March 1st and in March 3rd, but no transaction in March 2nd. And by joining both tables March 2nd would appear in the ledger table but with 0 for the variable we're analyzing.)
The challenge is that I can't create a Date object/table/dimension because I don't have permissions to create tables in the database. Therefore I've been generating a date sequence with this code:
DECLARE #startDate date = CAST('2016-01-01' AS date),
#endDate date = CAST(GETDATE() AS date);
SELECT DATEADD(day, number - 1, #startDate) AS [Date]
FROM (
SELECT ROW_NUMBER() OVER (
ORDER BY n.object_id
)
FROM sys.all_objects n
) S(number)
WHERE number <= DATEDIFF(day, #startDate, #endDate) + 1;
So, is there the possibility to join both tables into the same statement? Let's say the ledger table looks like this:
SELECT
date,cost
FROM ledger
I'd assume it can be done by using a subquery but I don't know how.
Thank you.
There is a very good article by Aaron Bertrand showing several methods for generating a sequence of numbers (or dates) in SQL Server: Generate a set or sequence without loops – part 1.
Try them out and see for yourself which is faster or more convenient to you. (spoiler - Recursive CTE is rather slow)
Once you've picked your preferred method you can wrap it in a CTE (common-table expression).
Here I'll use your method from the question
WITH
CTE_Dates
AS
(
SELECT
DATEADD(day, number - 1, #startDate) AS dt
FROM (
SELECT ROW_NUMBER() OVER (
ORDER BY n.object_id
)
FROM sys.all_objects n
) S(number)
WHERE number <= DATEDIFF(day, #startDate, #endDate) + 1
)
SELECT
...
FROM
CTE_Dates
LEFT JOIN Ledger ON Ledger.dt = CTE_Dates.dt
;
You can use your generated date sequence as a CTE and LEFT JOIN that to your ledger table. For example:
DECLARE #startDate date = CAST('2020-02-01' AS date);
DECLARE #endDate date = CAST(GETDATE() AS date);
WITH dates AS (
SELECT DATEADD(day, number - 1, #startDate) AS [Date]
FROM (
SELECT ROW_NUMBER() OVER (
ORDER BY n.object_id
)
FROM sys.all_objects n
) S(number)
WHERE number <= DATEDIFF(day, #startDate, #endDate) + 1
)
SELECT dates.Date, COALESCE(ledger.cost, 0)
FROM dates
LEFT JOIN (VALUES ('2020-02-02', 14), ('2020-02-05', 10)) AS ledger([Date], [cost]) ON dates.Date = ledger.Date
Output:
Date cost
2020-02-01 0
2020-02-02 14
2020-02-03 0
2020-02-04 0
2020-02-05 10
2020-02-06 0
Demo on dbfiddle

Loop Through Recordset and count if Within Date Range

Please note - this is my first post, so I apologize for anything I have missed.
I have a large event table that has a record for each time someone moves within a facility. What I would like to do, is say if that person was in the facility over the previous 365 days, count them (for each day). Essentially, I need the Average Daily Population (for everyone in the facility over the previous 365 days).
thank you.
Example Data:
PersonID ArriveDt LeaveDt Location
1111 1/1/2019 1/3/2019 ABC
1122 1/1/2019 1/5/2019 ABC
1123 1/2/2019 1/6/2019 ABC
Date Count
1/1/2019 2
1/2/2019 3
1/3/2019 3
1/4/2019 2
1/5/2019 2
1/6/2019 1
If you don't care about the intermediate values then really you just need to compute the total man-days and divide.
select sum(datediff(day, adjusted_start, adjusted_end) + 1) / 365.0
from T
cross apply (
select
dateadd(day, -365, cast(getdate() as date)),
dateadd(day, -1, cast(getdate() as date))
) d(range_start, range_end)
cross apply (
select
case when ArriveDt < range_start then range_start else ArriveDt end,
case when LeaveDt > range_end then range_end else LeaveDt end
) a(adjusted_start, adjusted_end);
If not, then come up with a table of dates (from any of numerous sources around the internet) and join from that. An outer join allows for dates with zero census.
with dates as (
select dt from calendar -- exercise for the reader
where
dt >= dateadd(day, -365, cast(getdate() as date)),
and dt <= dateadd(day, -1, cast(getdate() as date))
)
select d.dt, count(*)
from dates d left outer join T t
on d.dt between t.ArriveDt and t.LeaveDt
group by d.dt;
Create basic calendar table for the last X days:
IF OBJECT_ID('tempdb..#Calendar') IS NOT NULL
DROP TABLE #Calendar;
GO
DECLARE #StartDate DATE = DATEADD(d, -365, GETDATE())
DECLARE #EndDate DATE = GETDATE()
CREATE TABLE #Calendar
(
[CalendarDate] DATE
)
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO #Calendar
(
CalendarDate
)
SELECT
#StartDate
SET #StartDate = DATEADD(dd, 1, #StartDate)
END
GO
Now CROSS JOIN with Calendar Table to get rows per Person per day they were at a Location and the GROUP BY
SELECT
c.CalendarDate
,Location
,COUNT(PersonID) AS PersonCount
FROM (
SELECT PersonID ,ArriveDt ,LeaveDt ,Location FROM dbo.Table
) t
CROSS JOIN Calendar c
WHERE c.CalendarDate BETWEEN t.ArriveDt AND t.LeaveDt
GROUP BY
c.CalendarDate
,Location
Edit (2019-10-08):
If you're unable to create tables, you can use temp tables instead and do a DROP IF EXISTS. You can run both of these parts in one go on the fly to generate your final report.

Generate data between two range date by some values

I have 2 dates, StartDate and EndDate:
Declare #StartDate date='2018/01/01', #Enddate date ='2018/12/31'
Then there is some data with a date and value in a mytable table:
----------------------------
ID date value
----------------------------
1 2018/02/14 4
2 2018/09/26 7
3 2017/09/20 2
data maybe start before 2018 and if it exist before #startdate get before values
else get 0
I'm looking to get a result that looks like this:
-----------------------------------
fromdate todate value
-----------------------------------
2018/01/01 2018/02/13 2
2018/02/14 2018/09/25 4
2018/09/26 2018/12/31 7
The first fromdate comes from #StartDate and the last todate is from #Enddate, and the other data should be generated.
I'm hoping to get this in an SQL query. I use sql-server 2016
You could use a CTE to create your full range of dates, and then LEAD to create the ToDate column:
DECLARE #FromDate date = '20180101',
#ToDate date = '20181231';
WITH VTE AS(
SELECT ID,
CONVERT(date,[date]) [date], --This is why using keywords for column names is a bad idea
[value]
FROM (VALUES(1,'20180214',4),
(2,'20180926',7),
(3,'20170314',4))V(ID,[date],[value])),
Dates AS(
SELECT [date]
FROM VTE V
WHERE V.[date] BETWEEN #FromDate and #ToDate
UNION ALL
SELECT [date]
FROM (VALUES(#FromDate))V([date]))
SELECT D.[date] AS FromDate,
LEAD(DATEADD(DAY, -1,D.[date]),1,#ToDate) OVER (ORDER BY D.[date]) AS ToDate,
ISNULL(V.[value],0) AS [value]
FROM Dates D
LEFT JOIN VTE V ON D.[date] = V.[date];
db<>fiddle
with cte as
(
select 0 as row_num, #StartDate as start_date, 0 as val
UNION
select ROW_NUMBER() OVER(ORDER BY start_date) as row_num, * from input
)
select curr.start_date
, DATEADD(day,-1,ISNULL(nex.start_date,DATEADD(day,1,#Enddate))) as end_date
, curr.val
from cte curr
left join cte nex on curr.row_num = nex.row_num - 1;
You can find the simulation here: https://rextester.com/EIAXW23839

SELECT DateTime not in SQL

I have the following table:
oDateTime pvalue
2017-06-01 00:00:00 70
2017-06-01 01:00:00 65
2017-06-01 02:00:00 90
ff.
2017-08-01 08:00:00 98
The oDateTime field is an hourly data which is impossible to have a duplicate value.
My question is, how can I know if the oDateTime data is correct? I meant, I need to make sure the data is not jump? It should be always 'hourly' base.
Am I missing the date? Am I missing the time?
Please advice. Thank you.
Based on this answer, you can get the missing times form your table MyLogTable it like this:
DECLARE #StartDate DATETIME = '20170601', #EndDate DATETIME = '20170801'
SELECT DATEADD(hour, nbr - 1, #StartDate)
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY c.object_id ) AS Nbr
FROM sys.columns c
) nbrs
WHERE nbr - 1 <= DATEDIFF(hour, #StartDate, #EndDate) AND
NOT EXISTS (SELECT 1 FROM MyLogTable WHERE DATEADD(hour, nbr - 1, #StartDate)= oDateTime )
If you need to check longer period, you can just add CROSS JOIN like this
FROM sys.columns c
CROSS JOIN sys.columns c1
It enables you to check much more than cca thousand records (rowcount of sys.columns table) in one query.
Since your table is not having any unique id number, use a row_number() to get the row number in the cte , then perform an self inner join with the row id and next id ,take the difference of oDateTime accordingly, this will show exactly which row do not have time difference of one hour
;with cte(oDateTime,pValue,Rid)
As
(
select *,row_number() over(order by oDateTime) from [YourTableName] t1
)
select *,datediff(HH,c1.oDateTime,c2.oDateTime) as HourDiff from cte c1
inner join cte c2
on c1.Rid=c2.Rid-1 where datediff(HH,c1.oDateTime,c2.oDateTime) >1
You could use DENSE_RANK() for numbering the hours in a day from 1 to 24. Then all you have to do is to check whether the max rank is 24 or not for a day. if there is at least one entry for each hour, then dense ranking will have max value of 24.
Use the following query to find the date when you have a oDateTime missing.
SELECT [date]
FROM
(
SELECT *
, CAST(oDateTime AS DATE) AS [date]
, DENSE_RANK() OVER(PARTITION BY CAST(oDateTime AS DATE) ORDER BY DATEPART(HOUR, oDateTime)) AS rank_num
FROM Test
) AS t
GROUP BY [date]
HAVING(MAX(rank_num) != 24);
If you need validation for each row of oDateTime, you could do self join based on rank and get the missing hour for each oDateTime.
Perhaps you are looking for this? This will return dates having count < 24 - which indicates a "jump"
;WITH datecount
AS ( SELECT CAST(oDateTime AS DATE) AS [date] ,
COUNT(CAST(oDateTime AS DATE)) AS [count]
FROM #temp
GROUP BY ( CAST(oDateTime AS DATE) )
)
SELECT *
FROM datecount
WHERE [count] < 24;
EDIT: Since you changed the requirement from "How to know if there is missing" to "What is the missing", here's an updated query.
DECLARE #calendar AS TABLE ( oDateTime DATETIME )
DECLARE #min DATETIME = (SELECT MIN([oDateTime]) FROM #yourTable)
DECLARE #max DATETIME = (SELECT MAX([oDateTime]) FROM #yourTable)
WHILE ( #min <= #max )
BEGIN
INSERT INTO #calendar
VALUES ( #min );
SET #min = DATEADD(hh, 1, #min);
END;
SELECT t1.[oDateTime]
FROM #calendar t1
LEFT JOIN #yourTable t2 ON t1.[oDateTime] = t2.[oDateTime]
GROUP BY t1.[oDateTime]
HAVING COUNT(t2.[oDateTime]) = 0;
I first created a hourly calendar based on your MAX and MIN Datetime, then compared your actual table to the calendar to find out if there is a "jump".