How to subtract an offset to dates generated through a recursive CTE? - sql

The query below uses a recursive CTE to generate days if there is no data for that specific day. I want to group the daily downtime total starting at 7:15 the previous day until 7:15 the next day and do it over a month. This query works fine but I need to subtract DATEADD(minute, -(7 * 60 + 15) from each day.
WITH dates as (
SELECT CONVERT(date, 'Anydate') as dte
UNION ALL
SELECT DATEADD(day, 1, dte)
FROM dates
WHERE dte < 'Anydate + 1 month later'
)
SELECT CONVERT(datetime,d.dte), ISNULL(SUM(long_stop_minutes), 0) AS downtime
FROM dates d LEFT JOIN
long_stops_table b
ON CAST(t_stamp as DATE) = d.dte AND Type = 'downtime'
GROUP BY CONVERT(datetime, d.dte)
ORDER BY CONVERT(datetime, d.dte) ASC;

Just subtract the appropriate time units. Here is one way:
SELECT d.dte,
COALESCE(SUM(lst.long_stop_minutes), 0) AS downtime
FROM dates d LEFT JOIN
long_stops_table lst
ON CONVERT(date, DATEADD(minute, -(7 * 60 + 15), lst.t_stamp) = d.dte AND
lst.Type = 'downtime'
GROUP BY d.dte
ORDER BY d.dte ASC;
I see no reason to convert dates.dte to a datetime, so I just removed the conversion.

Related

Merging two SELECT queries with same date fields

I have a table of Tasks where I have records for a particular date. I want to have all dates in one month displayed with numbers of tasks per date. If on some date there were no record of a task it should be written 0.
I have results with duplicating records from the same date when there were tasks on a given day.
Table:
Date Tasks
2021-08-01 0
2021-08-02 0
2021-08-03 0
2021-08-03 25
2021-08-04 0
2021-08-04 18
2021-08-05 0
2021-08-05 31
2021-08-06 0
SQL code I am using:
Declare #year int = 2021, #month int = 8;
WITH numbers
as
(
Select 1 as value
UNion ALL
Select value +1 from numbers
where value + 1 <= Day(EOMONTH(datefromparts(#year, #month, 1)))
)
SELECT datefromparts(#year, #month, numbers.value) AS 'Datum', 0 AS 'Tasks' FROM numbers
UNION
SELECT CONVERT(date, added_d) AS 'Datum', COUNT(*) AS 'Tasks' FROM Crm.Task
WHERE YEAR(added_d) = '2021' AND MONTH(added_d) = '8' GROUP BY CONVERT(date, added_d)
How can I remove duplicates that I will have only one date record 21-08-03 with 25 tasks?
Thank you for your help
You requires OUTER JOIN :
WITH numbers as (
Select datefromparts(#year, #month, 1) as value
UNION ALL
Select DATEADD(DAY, 1, value) as value
from numbers
where value < EOMONTH(value)
)
select num.value, COUNT(tsk.added_d) AS Tasks
from numbers num left join
Crm.Task tsk
on CONVERT(date, tsk.added_d) = num.value
GROUP BY num.value;
If you want all dates for one month, you can do:
with dates as (
select datefromparts(#year, #month, 1) as dte
union all
select dateadd(day, 1, dte)
from dates
where dte < eomonth(dte)
)
You can then incorporate this into the logic using an outer join or subquery:
with dates as (
select datefromparts(#year, #month, 1) as dte
union all
select dateadd(day, 1, dte)
from dates
where dte < eomonth(dte)
)
select d.dte, count(t.added_d)
from dates d left join
Crm.Task t
on convert(date, t.added_d) = d.dte
group by d.dte
order by d.dte;
You can easily extend the logic for the CTE for more than one month, by adjusting the where clause in the second clause.

MSSQL query range for a day over a month

I am trying to retrieve the range for the value field over each 24hr period over a predefined time period. The issue is that the time period is between 7am and 7am the next day. (not a daily figure)
For example, I would like the range for day 1, then range for day 2, etc. I've tried using the below query but the production field keeps coming back with the same data, could anyone please shed some light on how I can make this work?
Thank you very much.
select tagname, convert(date,datetime),
(
select (max(Value)-min(Value)) as Range
from Runtime.dbo.AnalogHistory
where (TagName = 'LS_CV004_WX1_PROD_DATA.Actual_Input')
and DateTime BETWEEN dateadd(hh,7,convert(datetime,convert(date,datetime))) AND dateadd(hh,31,convert(datetime,convert(date,datetime)))
) as Production
from runtime.dbo.analoghistory
where (TagName = 'LS_CV004_WX1_PROD_DATA.Actual_Input')
and datetime between '20151101' and '20151201'
group by tagname, convert(date,DateTime)
I would like to result to be as per below
tagname | date | production
Below is one method that uses CTEs to generate a row for each date in the specified range, and then joins to the table on the date plus a 7 hour offset. Consider creating a utility calendar table for this type of task.
DECLARE
#StartDate date = '20151101'
, #EndDate date = '20151201';
WITH
t8 AS (SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0)) t(n))
, t512 AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) - 1 AS num FROM t8 AS a CROSS JOIN t8 AS b CROSS JOIN t8 AS c)
, dates AS (SELECT DATEADD(day, num, #StartDate) AS Date FROM t512 WHERE num <= DATEDIFF(day, #StartDate, #EndDate))
SELECT TagName, Date, MAX(Value)-MIN(Value) as Production
FROM dates
JOIN dbo.AnalogHistory ON
AnalogHistory.DateTime >= DATEADD(hour, 7, Date)
AND AnalogHistory.DateTime < DATEADD(hour, 31, Date)
GROUP BY TagName, Date
ORDER BY TagName, Date;
Your subquery is not correlated to the outer query, so it is no surprise that it returns the same value. I think you want something like this:
select tagname, convert(date, datetime),
(select (max(ah2.Value) - min(ah2.Value)) as Range
from Runtime.dbo.AnalogHistory ah2
where ah2.TagName = 'LS_CV004_WX1_PROD_DATA.Actual_Input' and
ah2.DateTime BETWEEN dateadd(hour, 7, convert(datetime, convert(date, ah.datetime))) AND
dateadd(hour, 31, convert(datetime, convert(date, ah.datetime)))
) as Production
from runtime.dbo.analoghistory ah
where TagName = 'LS_CV004_WX1_PROD_DATA.Actual_Input' and
datetime between '20151101' and '20151201'
group by tagname, convert(date, DateTime);
Note the use of ah2 and ah in the subquery.

How to reduce the query execution time

SELECT dt AS Date
,monthname
,dayname
,(
SELECT COUNT(1)
FROM Calendar
WHERE DATEPART(MM, dt) = DATEPART(MM, c.dt)
AND DATEPART(YEAR, dt) = DATEPART(YEAR, c.dt)
) AS daysInMonth
FROM Calendar AS c
WHERE dt BETWEEN '2000-01-01 00:00:00'
AND '2020-02-01 00:00:00'
the above query is for getting number of days of particular month for a particular date. here iam giving date range and for all the dates between the range iam just showing the days of that month.
The image shows the results for the query and its taking 25secs for ~7500 rows. can someone help me to reduce the time.
Try this one. Here you calculate the total only once instead of 7500 times.
Also create the index for dt field
with monthCount as (
SELECT DATEPART(YEAR, dt) as m_year,
DATEPART(MM, dt) as m_month
COUNT(1) as total
FROM Calendar
GROUP BY
DATEPART(YEAR, dt),
DATEPART(MM, dt)
)
SELECT dt AS Date
,monthname
,dayname
,total
FROM Calendar C
JOIN monthCount M
on DATEPART(YEAR, C.dt) = M.m_year
and DATEPART(MM, C.dt) = M.m_month
WHERE C.dt BETWEEN '2000-01-01 00:00:00'
AND '2020-02-01 00:00:00'

i want to return only single record with sum of hours

The below query is working perfect but it return two rows of hours which I don't want
SELECT
USERINFO.name, USERINFO.BADGENUMBER,
departments.deptname, APPROVEDHRS.hours,
sum(workingdays) as workingdays,TotalWorkingDays
FROM
(SELECT DISTINCT
(DATEDIFF(DAY, '2014-06-01', '2014-06-30') + 1) -
DATEDIFF(WEEK, '2014-06-01', '2014-06-30') * 2 -
(CASE WHEN DATEPART(WEEKDAY, '2014-06-01') = 5 THEN 1 ELSE 0 END) -
(CASE WHEN DATEPART(WEEKDAY, '2014-06-30') = 6 THEN 1 ELSE 0 END) AS TotalWorkingDays,
COUNT(DISTINCT DATEADD(d, 0,DATEDIFF(d, 0, CHECKINOUT.CHECKTIME))) AS workingdays,
USERINFO.BADGENUMBER, USERINFO.NAME, hours
FROM
USERINFO
LEFT JOIN
CHECKINOUT ON USERINFO.USERID = CHECKINOUT.USERID
LEFT JOIN
departments ON departments.deptid = userinfo.DEFAULTDEPTID
left join APPROVEDHRS on APPROVEDHRS.userid = userinfo.userid AND
(APPROVEDHRS.DATE >='2014-06-01') AND (APPROVEDHRS.DATE <='2014-06-30')
WHERE
(DEPARTMENTS.DEPTNAME = 'xyz')
AND (CHECKINOUT.CHECKTIME >= '2014-06-01')
AND (CHECKINOUT.CHECKTIME <= '2014-06-30')
GROUP BY
hours, USERINFO.BADGENUMBER, deptname, USERINFO.NAME,
CONVERT(VARCHAR(10), CHECKINOUT.CHECKTIME, 103)) blue
GROUP BY
name, BADGENUMBER, workingdays, TotalWorkingDays, deptname, hours
The output of above query :
name BADGENUMBER deptname hours
---------------------------------------------------
abc 1111 xyz 00:07:59
abc 1111 xyz 00:08:00
pqr 2222 qwe NULL
Now the total hours (APPROVEDHRS table) in table is :
BADGENUMBER NAME DATE HOURS
-------------------------------------------------
1111 xyz 2014-06-15 00:07:59
1111 xyz 2014-06-14 00:08:00
1111 xyz 2014-07-14 00:10:00
I am fetching record from 2014-06-01 to 2014-06-30
So I want the below output:
name BADGENUMBER deptname hours
--------------------------------------------------------
abc 1111 xyz 00:15:59
pqr 2222 qwe NULL
Help me to get this desired output.
Thank you
Clearly, if you want to add your durations together, you should be storing them as something you can add together. Generally, this takes the form of a numeric type representing the smallest granularity you're interested in (apparently minutes, in this case). You can wrap it as an actual defined type, that standard operators work on (I'm sure somebody's defined an INTERVAL type for some version of SQL Server), but essentially it's simply backed by an INTEGER or something.
If you can't change the actual type in the db, then you need to convert it for this statement (and back for the display). That's perhaps easiest by declaring a pair of functions based on this:
CREATE FUNCTION dbo.Minutes_From_Duration_String (#Duration AS CHAR(8))
RETURNS INTEGER
BEGIN
RETURN (CAST(SUBSTRING(#Duration, 1, 2) AS INTEGER) * 24 * 60) +
(CAST(SUBSTRING(#Duration, 4, 2) AS INTEGER) * 60) +
(CAST(SUBSTRING(#Duration, 7, 2) AS INTEGER))
END;
CREATE FUNCTION dbo.Duration_String_From_Minutes (#Minutes AS INTEGER)
RETURNS CHAR(10)
BEGIN
RETURN RIGHT('00' + (#Minutes / 60 / 24), 2) + ':' +
RIGHT('00' + ((#Minutes / 60) % 24), 2) + ':' +
RIGHT('00' + (#Minutes % 60), 2)
END;
SQL Fiddle example
(note that these are extremely basic and will blow up at the slightest provocation. The rest is left as an exercise to the reader).
That taken care of, they can be used in your query, as usual.
Note that I think your query should be modified a bit. It's a bit difficult to tell without starting data, but I believe it can run faster, and be clearer.
First, always query positive contiguous-range types (like dates/times/timestamps) as lower-bound inclusive (>=), upper-bound exclusive (<), especially for the listed types on SQL Server. This means you never have to worry about dealing with fractions of things.
Next, if you don't have one already, you really want a Calendar Table. It is, in my opinion, the most useful Dimension table to have. You can put essentially as many indices as you want on it, which means you can use them (and range queries) to actually get index-based aggregates that you couldn't before (ie, by week, etc). It also makes getting non-working days (holidays) much easier, and is critical for one other thing here: the results of DATEPART(WEEKDAY, ....) are dependent on the culture/locale of the current session. That's probably not what you want.
If you can't create one now, you can generate a simple one easily with the use of a recursive CTE:
WITH Calendar_Range AS (SELECT CAST('20140601' AS DATE) AS Calendar_Date,
dbo.ISO_Day_Of_Week(CAST('20140601' AS DATE)) AS Day_Of_Week
UNION ALL
SELECT DATEADD(day, 1, Calendar_Date),
dbo.ISO_Day_Of_Week(DATEADD(day, 1, Calendar_Date))
FROM Calendar_Range
WHERE Calendar_Date < CAST('20140701' AS DATE))
SELECT Calendar_Date, Day_Of_Week
FROM Calendar_Range
SQL Fiddle demo
(This assumes you have some way to get the ISO Day-of-week - where Monday is 1. The demo includes a sample function that does this.)
We actually have three different aggregates, so we need to get them all separately:
First, the total hours approved:
SELECT userid, SUM(dbo.Minutes_From_Duration_String(hours)) AS totalHours
FROM ApprovedHrs
WHERE date >= CAST('20140601' AS DATE)
AND date < CAST('20140701' AS DATE)
GROUP BY userid
Then, total number of days worked.
SELECT CheckInOut.userid, COUNT(DISTINCT Calendar_Range.calendar_date) AS daysWorked
FROM Calendar_Range
JOIN CheckInOut
ON CheckInOut.checkTime >= Calendar_Range.calendar_date
AND CheckInOut.checkTime < DATEADD(day, 1, Calendar_Range.calendar_date)
WHERE Calendar_Range.calendar_date >= CAST('20140601' AS DATE)
AND Calendar_Range.calendar_date < CAST('20140701' AS DATE)
GROUP BY CheckInOut.userid
(I'm assuming Calendar_Range is a full-on Calendar table here, with all possible dates)
Lastly, number of days "available" to be worked:
SELECT COUNT(*) AS totalWorkingDays
FROM Calendar_Range
WHERE Day_Of_Week NOT IN (6, 7)
AND calendar_date >= CAST('20140601' AS DATE)
AND calendar_date < CAST('20140701' AS DATE)
(I'm assuming that there are other non-working days that shouldn't be counted here, like Christmas, but I didn't include a condition for it. Otherwise, you can do the calculation similar to what you did before, just be careful of day-of-week issues. The query I'm using here assumes ISO day-of-week values)
We now have all the pieces we need, so we can assemble the final query:
SELECT UserInfo.name, UserInfo.badgeNumber,
Departments.deptName,
dbo.Duration_String_From_Minutes(COALESCE(SummedHours.totalHours, 0)) AS totalHours,
COALESCE(DaysWorked.daysWorked, 0) AS daysWorked,
WorkingDays.totalWorkingDays
FROM UserInfo
JOIN Departments
ON Departments.deptId = UserInfo.defaultDeptId
AND Departments.deptName = 'xyz'
CROSS JOIN (SELECT COUNT(*) AS totalWorkingDays
FROM Calendar_Range
WHERE Day_Of_Week NOT IN (6, 7)
AND calendar_date >= CAST('20140601' AS DATE)
AND calendar_date < CAST('20140701' AS DATE)) WorkingDays
LEFT JOIN (SELECT userid, SUM(dbo.Minutes_From_Duration_String(hours)) AS totalHours
FROM ApprovedHrs
WHERE date >= CAST('20140601' AS DATE)
AND date < CAST('20140701' AS DATE)
GROUP BY userid) SummedHours
ON SummedHours.userId = UserInfo.userId
LEFT JOIN (SELECT CheckInOut.userid, COUNT(DISTINCT Calendar_Range.calendar_date) AS daysWorked
FROM Calendar_Range
JOIN CheckInOut
ON CheckInOut.checkTime >= Calendar_Range.calendar_date
AND CheckInOut.checkTime < DATEADD(day, 1, Calendar_Range.calendar_date)
WHERE Calendar_Range.calendar_date >= CAST('20140601' AS DATE)
AND Calendar_Range.calendar_date < CAST('20140701' AS DATE)
GROUP BY CheckInOut.userid) DaysWorked
ON DaysWorked.userId = UserInfo.userId
Try this.
WITH CTE (name,BADGENUMBER,deptname,hours)
AS
(
YOUR FULL QUERY
)
SELECT name,BADGENUMBER,deptname,SUM(hours)
FROM CTE
GROUP BY name,BADGENUMBER,deptname
you can sum hours by converting the varchar to time, then to seconds, sum them, back to varchar, back to time :) this should do the sum and the required conversions:
CONVERT(VARCHAR, dateadd(s,SUM(DATEDIFF(SECOND, 0, CAST(hours AS TIME))),0),114)

How to get the date for two saturdays ago

I have the following query which displays a table with date:
SELECT *
FROM [Db].[dbo].[btotals]
ORDER BY [Date] DESC
Which displays:
Date
06/07/2014
05/31/2014
05/24/2014
05/17/2014
05/10/2014
05/03/2014
If I pick SELECT TOP 1 will give me the first row. How can I modify my query so I get the week prior to last week? In this case the 5/31/14 row?
If your dates are always a week apart, and you just want the second row you can use ROW_NUMBER():
SELECT Date
FROM ( SELECT Date,
RowNumber = ROW_NUMBER() OVER(ORDER BY Date DESC)
FROM [Db].[dbo].[btotals]
) AS d
WHERE d.RowNumber = 2;
Otherwise you can use the following to get the saturday of 2 weeks ago:
SELECT DATEADD(DAY, -((DATEPART(WEEKDAY, GETDATE()) + ##DATEFIRST) % 7) - 7, CAST(GETDATE() AS DATE));
Then select your first date that is on or after that:
SELECT TOP 1 Date
FROM [Db].[dbo].[btotals]
WHERE Date >= DATEADD(DAY, -((DATEPART(WEEKDAY, GETDATE()) + ##DATEFIRST) % 7) - 7, CAST(GETDATE() AS DATE))
ORDER BY Date;
This should also work, if you are trying to select the second date, Though Gareth's approach of using ROW_NUNMBER is a better one.
SELECT TOP 1 *
FROM (
SELECT TOP 2 *
FROM [Db].[dbo].[btotals]
ORDER BY [Date] DESC
) as X
ORDER BY Date ASC
Another approach:
SELECT TOP 1 *
FROM [Db].[dbo].[btotals]
WHERE [Date] < (SELECT MAX([Date]) FROM [Db].[dbo].[btotals])
ORDER BY [Date] DESC