Check Missing Time Interval In SQL in Minutes - sql

I have a SQL statement as below and I wish to check the data in time interval in minutes where the D.DatalogValue didn't have any value and it won't show as Null or zero value either. The sample as below output result will be show 2016-06-01 00:32:29 as missing createdDate.
SELECT
A.DefID, A.ObjID,
C.ObjName, C.Dev_ID,
A.Pro_ID, A.ArrayIndex,
A.DefType, A.TObjID, A.DimeId, A.DefId,
D.DatalogValue, D.PanelDt, D.CreatedDate
FROM
Table A, Table C, Table D
WHERE
A.ObjID = C.ObjID
AND C.ObjID = '2627'
AND A.DefID = D.DefID
AND D.CreatedDate BETWEEN '2016-06-01' AND '2016-06-02'
ORDER BY
C.ObID,C.ObjName;
Sample data:
Create Date DatalogValue
-------------------------------------
2016-06-01 00:29:29 0.01
2016-06-01 00:30:29 0.02
2016-06-01 00:31:29 0.03
2016-06-01 00:33:29 0.04
By using the solution provided i have come out a SQL statement but it still no able to show the result i want. I not sure which part i doing wrong my code as below:
DECLARE #StartDate DATETIME = '2016-07-01';
DECLARE #EndDate DATETIME = '2016-07-31';
WITH Check_Dates AS (
SELECT #StartDate [Date]
UNION ALL
SELECT DATEADD(MINUTE, 1, [Date]) FROM Check_Dates
WHERE [Date] < DATEADD(DAY, 1, #EndDate)
)
SELECT
FORMAT(d.Date, 'yyyy-MM-dd HH:mm') [Created Date]
FROM Check_Dates d
WHERE
NOT EXISTS(
SELECT
Format(D.CreatedDate, 'yyyy-MM-dd HH:mm')as created_dt
FROM TABLE A
,TABLE C
,TABLE D
WHERE A.ObjID=C.ObjID
AND C.ObjID IN('3915')
AND A.DefID=D.DefID
AND D.CreatedDate BETWEEN '2016-07-01' AND '2016-08-01'
)
OPTION (MAXRECURSION 0);

A solution is to use a CTE to create a list of DATETIMEs then LEFT JOIN these onto your original query. You can also create a pair of tables instead (as mentioned in the comments) - google DimDate and/or DimTime.
Something like (untested):
DECLARE #StartDate DATETIME = '2016-06-01';
DECLARE #EndDate DATETIME = '2016-06-02';
WITH Dates AS (
SELECT #StartDate [Date]
UNION ALL
SELECT DATEADD(SECOND, 1, [Date]) FROM Dates
WHERE [Date] < DATEADD(DAY, 1, #EndDate)
)
SELECT
d.Date [Created Date]
,COALESCE(Qry.DatalogValue, 0) DatalogValue
FROM Dates d
LEFT JOIN (
Your query goes here
) Qry
ON d.Date = Qry.CreatedDate
OPTION (MAXRECURSION 0)

Your solution seems very risky to me. Are you sure seconds should be compared? I would truncate to minutes. I suggest more robust solution:
WITH Dates AS
( --Your dates and values
SELECT * FROM (VALUES
('2016-06-01 00:29:29', 0.01),
('2016-06-01 00:30:29', 0.02),
('2016-06-01 00:31:29', 0.03),
('2016-06-01 00:33:29', 0.04)--,('2016-06-01 01:00:28', 0.05)
) T(CreateDate, CatalogValues)
), Minute10 AS --Generate numbers from 0-999999
(
SELECT * FROM (VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(0)) T1(Value)
), Minute1000 AS
(
SELECT M1.Value FROM Minute10 M1 CROSS JOIN Minute10 M2 CROSS JOIN Minute10
), Minute1000000 AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1))-1 Value
FROM Minute1000
CROSS JOIN Minute1000 M2
), RangeValues AS --for simplicity, min and max values from dates
(
SELECT DATEADD(MINUTE, DATEDIFF(MINUTE, 0, MIN(CreateDate)), 0) MinDate,
DATEADD(MINUTE, DATEDIFF(MINUTE, 0, MAX(CreateDate)), 0) MaxDate
FROM Dates
)
SELECT TOP(1+DATEDIFF(MINUTE, (SELECT MinDate FROM RangeValues), (SELECT MaxDate FROM RangeValues)))
DATEADD(MINUTE,Value,MinDate) ExpectedDate, CreateDate, CatalogValues
FROM Minute1000000
CROSS APPLY (SELECT MinDate FROM RangeValues) T
LEFT JOIN Dates ON DATEADD(MINUTE,Value,MinDate)=DATEADD(MINUTE, DATEDIFF(MINUTE, 0, CreateDate), 0)
Note that all dates are truncated to minutes. You can simplyfy query by removing number generation part (numbers can be placed in utility table, 1440 values if that's all you need). Min and Max can be precalculated.
This results in following output (can handle ranges minDate+999999 minutes, can be simply extended):
ExprectedDate CreateDate CatalogValues
2016-06-01 00:29:00.000 2016-06-01 00:29:29 0.01
2016-06-01 00:30:00.000 2016-06-01 00:30:29 0.02
2016-06-01 00:31:00.000 2016-06-01 00:31:29 0.03
2016-06-01 00:32:00.000 NULL NULL
2016-06-01 00:33:00.000 2016-06-01 00:33:29 0.04
Explanation:
Dates is just source table. Tables Minute10..Minute1000000 are to generate numbers from 0 to 999999 (10 cross joined 10 = 100, 100 cross joined x3 = 100^3 = 1000000. Records from last table are numbered to get sequential values. Don't worry, TOP prevents from evaluating all 1000000 values. RangeValues contains MAX and MIN dates, for simplicity.
Algorithm:
Since you need records from MIN date to MAX date every minute, you evaluate TOP DATETIFF(MINUTE,MIN,MAX)+1 records (+1 to avoid Fencepost error). All required tables are joined (CROSS APLLY adds MIN column to every record), Expected date is calculated as MIN date + sequential value in minutes. Last join, LEFT one, matches date generated for every minute with source table. If there is match, record is appended (joined). If there is no match, NULL is appended. Note `DATEADD(MINUTE, DATEDIFF(MINUTE, 0, #someDate), 0)' truncates seconds from date.

Related

Finding Active Clients By Date

I'm having trouble writing a recursive function that would count the number of active clients on any given day.
Say I have a table like this:
Client
Start Date
End Date
1
1-Jan-22
2
1-Jan-22
3-Jan-22
3
3-Jan-22
4
4-Jan-22
5-Jan-22
5
4-Jan-22
6-Jan-22
6
7-Jan-22
9-Jan-22
I want to return a table that would look like this:
Date
NumActive
1-Jan-22
2
2-Jan-22
2
3-Jan-22
3
4-Jan-22
4
5-Jan-22
4
6-Jan-22
3
7-Jan-22
3
8-Jan-22
3
9-Jan-22
4
Is there a way to do this? Ideally, I'd have a fixed start date and go to today's date.
Some pieces I have tried:
Creating a recursive date table
Truncated to Feb 1, 2022 for simplicity:
WITH DateDiffs AS (
SELECT DATEDIFF(DAY, '2022-02-02', GETDATE()) AS NumDays
)
, Numbers(Numbers) AS (
SELECT MAX(NumDays) FROM DateDiffs
UNION ALL
SELECT Numbers-1 FROM Numbers WHERE Numbers > 0
)
, Dates AS (
SELECT
Numbers
, DATEADD(DAY, -Numbers, CAST(GETDATE() -1 AS DATE)) AS [Date]
FROM Numbers
)
I would like to be able to loop over the dates in that table, such as by modifying the query below for each date, such as by #loopdate. Then UNION ALL it to a larger final query.
I'm now stuck as to how I can run the query to count the number of active users:
SELECT
COUNT(Client)
FROM clients
WHERE [Start Date] >= #loopdate AND ([End Date] <= #loopdate OR [End Date] IS NULL)
Thank you!
You don't need anything recursive in this particular case, you need as a minimum a list of dates in the range you want to report on, ideally a permanent calendar table.
for purposes of demonstration you can create something on the fly, and use it like so, with the list of dates something you outer join to:
with dates as (
select top(9)
Convert(date,DateAdd(day, -1 + Row_Number() over(order by (select null)), '20220101')) dt
from master.dbo.spt_values
)
select d.dt [Date], c.NumActive
from dates d
outer apply (
select Count(*) NumActive
from t
where d.dt >= t.StartDate and (d.dt <= t.EndDate or t.EndDate is null)
)c
See this Demo Fiddle

T- SQL Split time in half hour intervals

I have a table calls that shows every call for every employee and looks like this:
date
employee
call_pick_up_time
2021-10-08
12345
2021-10-08 08:13:26
2021-10-08
123456
2021-10-08 08:16:42
Now I want to show the call count for each employee for every 30 minutes interval:
interval
employee
call_count
08:00
12345
4
08:00
123456
7
08:30
12345
5
08:30
123456
3
The considered period is 08:00 - 08:30 / 08:30 -09:00 and so on.
Is there an easy way to get the desired result?
Thanks in advance.
The way I like to round datetime values to the nearest n-minute interval is to take advantage of SQL Server's integer math behavior. If you take the difference in minutes between midnight and the time in question, then divide by n and then multiply by n, it gets rid of any remainder. So to round right now down to the previous 30-minute interval:
DECLARE #now datetime = GETDATE();
DECLARE #today datetime = CONVERT(date, #now);
SELECT DATEADD
(
MINUTE,
DATEDIFF(MINUTE, #today, #now)/30*30,
#today
);
We can apply this to your query by taking your source table and using CROSS APPLY as Charlie suggested and apply that same calculation to your source values (you have to do a little more conversion inline because you don't have nice, static variables to use):
DECLARE #WindowSizeInMinutes smallint = 30;
SELECT x.interval, c.employee, call_count = COUNT(*)
FROM dbo.Calls AS c
CROSS APPLY
(
VALUES
(
DATEADD
(
MINUTE,
DATEDIFF
(
MINUTE,
CONVERT(datetime, CONVERT(date, call_pick_up_time)),
call_pick_up_time
) / #WindowSizeInMinutes * #WindowSizeInMinutes,
CONVERT(datetime, CONVERT(date, call_pick_up_time))
)
)
) AS x(interval)
-- WHERE c.something something
GROUP BY c.employee, x.interval;
If there is an index on call_pick_up_time you were hoping to use, that's out the window.
Another approach that could make use of an index is to pre-determine all the possible 30-minute windows in the range you're after, and then inner join to those:
DECLARE #WindowSizeInMinutes smallint = 30,
#min_date datetime = '20211001',
#max_date datetime = '20211014';
;WITH n(n) AS
(
SELECT 0 UNION ALL
SELECT n + 1
FROM n WHERE n <= 24*60/#WindowSizeInMinutes
),
days(d) AS
(
SELECT #min_date UNION ALL
SELECT DATEADD(DAY, 1, d)
FROM days WHERE d < #max_date
),
intervals AS
(
SELECT interval_start = DATEADD(MINUTE, n*#WindowSizeInMinutes, d),
interval_end = DATEADD(MINUTE, (n+1)*#WindowSizeInMinutes, d)
FROM n CROSS JOIN days
)
SELECT interval = i.interval_start,
c.employee,
call_count = COUNT(c.employee)
FROM intervals AS i
INNER JOIN dbo.Calls AS c
ON c.call_pick_up_time >= i.interval_start
AND c.call_pick_up_time < i.interval_end
GROUP BY c.employee, i.interval_start;
While more complicated, one nice thing about this approach is if you want to show slots for windows where no employees had calls, you could just change the join to an inner join, and if you wanted a slot for each employee, you could just add a CTE with the list of employees and cross join to that.
Both examples on this db<>fiddle

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

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".

Query to check number of records created in a month.

My table creates a new record with timestamp daily when an integration is successful. I am trying to create a query that would check (preferably automated) the number of days in a month vs number of records in the table within a time frame.
For example, January has 31 days, so i would like to know how many days in january my process was not successful. If the number of records is less than 31, than i know the job failed 31 - x times.
I tried the following but was not getting very far:
SELECT COUNT (DISTINCT CompleteDate)
FROM table
WHERE CompleteDate BETWEEN '01/01/2015' AND '01/31/2015'
Every 7 days the system executes the job twice, so i get two records on the same day, but i am trying to determine the number of days that nothing happened (failures), so i assume some truncation of the date field is needed?!
One way to do this is to use a calendar/date table as the main source of dates in the range and left join with that and count the number of null values.
In absence of a proper date table you can generate a range of dates using a number sequence like the one found in the master..spt_values table:
select count(*) failed
from (
select dateadd(day, number, '2015-01-01') date
from master..spt_values where type='P' and number < 365
) a
left join your_table b on a.date = b.CompleteDate
where b.CompleteDate is null
and a.date BETWEEN '01/01/2015' AND '01/31/2015'
Sample SQL Fiddle (with count grouped by month)
Assuming you have an Integers table*. This query will pull all dates where no record is found in the target table:
declare #StartDate datetime = '01/01/2013',
#EndDate datetime = '12/31/2013'
;with d as (
select *, date = dateadd(d, i - 1 , #StartDate)
from dbo.Integers
where i <= datediff(d, #StartDate, #EndDate) + 1
)
select d.date
from d
where not exists (
select 1 from <target> t
where DATEADD(dd, DATEDIFF(dd, 0, t.<timestamp>), 0) = DATEADD(dd, DATEDIFF(dd, 0, d.date), 0)
)
Between is not safe here
SELECT 31 - count(distinct(convert(date, CompleteDate)))
FROM table
WHERE CompleteDate >= '01/01/2015' AND CompleteDate < '02/01/2015'
You can use the following query:
SELECT DATEDIFF(day, t.d, dateadd(month, 1, t.d)) - COUNT(DISTINCT CompleteDate)
FROM mytable
CROSS APPLY (SELECT CAST(YEAR(CompleteDate) AS VARCHAR(4)) +
RIGHT('0' + CAST(MONTH(CompleteDate) AS VARCHAR(2)), 2) +
'01') t(d)
GROUP BY t.d
SQL Fiddle Demo
Explanation:
The value CROSS APPLY-ied, i.e. t.d, is the ANSI string of the first day of the month of CompleteDate, e.g. '20150101' for 12/01/2015, or 18/01/2015.
DATEDIFF uses the above mentioned value, i.e. t.d, in order to calculate the number of days of the month that CompleteDate belongs to.
GROUP BY essentially groups by (Year, Month), hence COUNT(DISTINCT CompleteDate) returns the number of distinct records per month.
The values returned by the query are the differences of [2] - 1, i.e. the number of failures per month, for each (Year, Month) of your initial data.
If you want to query a specific Year, Month then just simply add a WHERE clause to the above:
WHERE YEAR(CompleteDate) = 2015 AND MONTH(CompleteDate) = 1