SQL Server: Group results on time interval - sql-server

I've got an MS SQL Server table that records our plant's alarm events with a row for each alarm and a datetime column to capture when the alarm happened.
We run our plant in 12 hour shifts (6 am to 6pm, 6pm to 6am) and I need to figure out how many alarms we're getting each shift. How do I group my results to get that?
The original table looks something like this:
DateTime Alarm Name
2010-01-05 14:32:22 Overpressure
2010-01-05 21:32:59 Underspeed
2010-01-06 05:58:13 Underspeed
2010-01-06 06:02:46 Machine Current Fault
And we need to group the results something like this:
Date Shift Count
2010-01-05 Day 1
2010-01-05 Night 2
2010-01-06 Day 1
Note that if alarms happen between 6 pm on say Jan 5th and 6 am on Jan 6th, they all get counted as Night Shift from Jan 5th.
Any advice?

In this solution, I work out the shift start/end times by subtracting 6 hours from the event time.
DECLARE #t TABLE
([DateTime] DATETIME
,[Alarm Name] VARCHAR(30)
)
INSERT #t
SELECT '2010-01-05 14:32:22','Overpressure'
UNION SELECT '2010-01-05 21:32:59','Underspeed'
UNION SELECT '2010-01-06 05:58:13','Underspeed'
UNION SELECT '2010-01-06 06:02:46','Machine Current Fault'
SELECT CONVERT(CHAR(10),DATEADD(hh,-6,[DateTime]),120) AS date
,CASE WHEN DATEPART(hh,DATEADD(hh,-6,[DateTime])) < 12
THEN 'day'
ELSE 'night'
END AS shift
,COUNT(1) AS cnt
FROM #t
GROUP BY CONVERT(CHAR(10),DATEADD(hh,-6,[DateTime]),120)
,CASE WHEN DATEPART(hh,DATEADD(hh,-6,[DateTime])) < 12
THEN 'day'
ELSE 'night'
END
order by 1,2

Related

To get the previous month's last date till last second

I want to get last month's last date (whether 30 or 31) and time till last second, whenever the query is executed.
Eg. 11/30/2015 11:59:59 PM
So I have a query like
SELECT DATEADD(ss, (60*60*24)-1, DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), -1))
It solves my problem. But what is the difference between the query written above and the one below, when I change the DATEDIFF part and replace 0 with 1?
SELECT DATEADD(ss, (60*60*24)-1, DATEADD(MONTH, DATEDIFF(MONTH, 1, GETDATE()), -1))
Will both of these queries gives the same result whenever they are run, or which should I consider as the permanent solution?
Do NOT do this; attempt to get the "last second" of the last day of the previous month
I make this bold statement on the assumption you are attempting to use BETWEEN and you are concerned with the accuracy of something like this:
select sum(value) from Atable
where [Adate] BETWEEN '20151201' AND '21051231 23:59:59'
But the complexity of arriving at the last point in time on the last day of any month is solved so easily by using the first day of the next month instead. All that you also need to do is drop the use of BETWEEN. Like this:
select sum(value) from Atable
where [Adate] >= '20151201' and [Adate] < '21060101'
LESS THAN "the first day of the this month"
That is how you solve your conundrum.
& by the way: The precision (accuracy) of smalldatetime, datetime and datetime2 all differ, all the more reason not to use BETWEEN.
see "Be careful about rounding errors." at http://sqlmag.com/t-sql/t-sql-best-practices-part-2
Specifically, do this:
DateLogged < SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0)
This will be 100% accurate for date, smalldatetime, datetime and datetime2 columns.
Here is another attempt to explain why LESS THAN [the_next_day_at_00:00:00+0000000] is accurate and using 22:59:59 is NOT accurate. Please take note of the sample data accuracy
SQL Fiddle
MS SQL Server 2014 Schema Setup:
Query 1:
DECLARE #Tbl TABLE
( [ID] int identity(1,1)
, [DT_a] datetime
, [DT_b] datetime
, [DT_c] datetime2
)
INSERT INTO #Tbl
([Dt_a], [Dt_b], [Dt_c])
VALUES
(
'20151231 23:59:59'
, '20151231 23:59:59.997'
, '20151231 23:59:59.9999999'
)
select
'where [DT_b] <= 20151231 23:59:59' as FilterString
, max([Dt_a]) as [Dt_a]
, max([Dt_b]) as [Dt_b]
, max([Dt_c]) as [Dt_c]
from #Tbl
where [DT_b] <= '20151231 23:59:59'
UNION ALL
select
'where [DT_b] < 20160101'
, max([Dt_a]) as [Dt_a]
, max([Dt_b]) as [Dt_b]
, max([Dt_c]) as [Dt_c]
from #Tbl
where [DT_b] < '20160101'
Results:
| FilterString | Dt_a | Dt_b | Dt_c |
|-----------------------------------|----------------------------|----------------------------|-----------------------------|
| where [DT_b] <= 20151231 23:59:59 | (null) | (null) | (null) |
| where [DT_b] < 20160101 | December, 31 2015 23:59:59 | December, 31 2015 23:59:59 | 2015-12-31 23:59:59.9999999 |
Data accuracy
smalldatetime: one minute
datetime: rounded to increments of .000, .003, or .007 seconds
datetime2: 100 nanoseconds
To avoid possible errors from rounding by time units DO NOT USE <= 23:59:59
Instead use LESS THAN [the_next_day]
AND, as a consequence AVOID USING BETWEEN for date ranges.
See this link for how DATEDIFF is used or should be used in SQL Server. The 2nd argument, the one which does not seem to make a difference in your case, is supposed to be the start date which is subtracted from the end date (getdate()) to get the difference and then converted to months. I would try and use this function the typical way and provide a proper start date.
Also below is an alternative way of getting the same result
SELECT DATEADD(ss, -1, '01/' + CONVERT(VARCHAR, DATEPART(MONTH, getdate())) + '/' + CONVERT(VARCHAR, DATEPART(YEAR, getdate())));
It is because of DATEDIFF( MONTH, 0, GETDATE()) Function
If you use an integer as the second argument, this is interpreted as the number of days since 1900-01-01 regardless of the Interval you are using in the datediff function.
For eg:
SELECT YEAR(0), MONTH(0), DAY(0);
year month day
1900 1 1
Now if I Increment 0 to 1 in year, month, day
SELECT YEAR(1), MONTH(1), DAY(1);
year month day
1900 1 2
Now if I Increment values to 365,
SELECT YEAR(365), MONTH(365), DAY(365);
year month day
1901 1 1
You can see the Year got incremented by 1.
there are many ways to find out the previous month's last date. Here is the one I am using.
SELECT DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE()),0))
Well it would be expected that substracting one millisecond to the first day of the current month you would get the last millisecond of the previous month but it doesn't work that whay, with datediff millisecond,-1 you still get the first day of the month you have to do datediff millisecond,-2 to reach 997 milliseconds, no way to get 999 nor 998.(without using text).
select dateadd(MILLISECOND,-2,dateadd(month, datediff(month, 0, getdate()), 0))
And you get 2020-01-31 23:59:59.997
To get last second of current month use:
SELECT DATEADD(MILLISECOND, -10, CAST (EOMONTH(GETDATE()) AS DATETIME))
and you get:
2021-12-30 23:59:59.000
Explanation: takes begin of next month (2021-12-31) and convert to datetime (2021-12-31 00:00:00.000) then takes 1 second to get (2021-12-30 23:59:59.000)

MSSQL Date Range Report - Count of days per month

I have a table of instances that have a Start Date and an End Date column. Here is a simple example:
ID StartDate EndDate
1 1/8/2015 1/10/2015
2 1/8/2015 1/15/2015
3 2/6/2015 3/2/2015
4 1/6/2015 2/20/2015
5 3/18/2015 4/2/2015
I'm trying to write a query to find out how many unique days occur for a given month, but some of the instances overlap and span multiple months which is making it difficult. The results I want would look something like this:
Month # of days
January 26 (earliest is ID 4 starting 1/6)
February 28 (entire month because of ID 3 and 4)
March 16 (2 days from ID 3, 14 days from ID 5)
April 2 (first 2 days of the month from ID 5)
May 0
Any help would be greatly appreciated. Thanks!!
J,
Please check my SQL script below.
Before you run the script you will realize that I've used a SQL Dates table actually a SQL function which returns a temporary dates table.
You can find the source codes at given tutorial
I also used multiple CTE queries
;with dates as (
select
cast(date as date) date
from [dbo].[DateTable]('1/1/2015','12/31/2015')
), cte as (
select
distinct date
from instances, dates
where dates.date between instances.startdate and instances.enddate
)
select
year(date) year, month(date) month, count(*) dayscount
from cte
group by year(date), month(date)
By the way the March returns 16 days, 2 from one and 14 from other.
I hope the Select statement is useful,
The problem is too complicated;
In my opinion, you need to write a function that counts the number of "unique" days of ranges mentioned in the records.
I didn't write the function, but the design of this new function, "num", is like this:
1- It should get the month and year (named aMonth and aYear).
2- It Finds all records that have at least a day in aYear/aMonth:
(month(startDate)=aMonth and year(startDate)=aYear)
or
(month(endDate)=aMonth and year(endDate)=aYear)
or
(
((month(startDate)<aMonth and year(startDate)=aYear) or (year(startDate)<aYear))
and
((month(endDate)>aMonth and year(endDate)=aYear) or (year(endDate)>aYear))
)
3- Over these records, it should open a cursor, and process the records one by one.
4- While processing each records of the cursor, you can count the days of the month and store them in an array (or 28-31 character string of 0/1, for example).
5- count the number of 1's of this array (or string) and return it.
Having written this function ("num"), The high level of the answer will be like this:
Select 'January', dbo.num(1, 2015) as days
union all
Select 'February', dbo.num(2, 2015) as days
union all
Select 'March', dbo.num(3, 2015) as days
union all
Select 'April', dbo.num(4, 2015) as days
union all
Select 'May', dbo.num(5, 2015) as days
union all
Select 'June', dbo.num(6, 2015) as days
union all
Select 'July', dbo.num(7, 2015) as days
union all
Select 'August', dbo.num(8, 2015) as days
union all
Select 'September', dbo.num(9, 2015) as days
union all
Select 'October', dbo.num(10, 2015) as days
union all
Select 'November', dbo.num(11, 2015) as days
union all
Select 'December', dbo.num(12, 2015) as days
If you count the days for the same year only, you can try this. I only build the code for two months but it's easy to extend it.
SELECT
(SELECT SUM(CASE WHEN sdate>='2015-2-1' OR edate<'2015-1-1' THEN 0
WHEN edate>='2015-2-1' THEN datediff(day, sdate, '2015-2-1')
ELSE datediff(day,sdate,edate) END)
FROM a1)
AS Jan_Days,
(SELECT SUM(CASE WHEN sdate>='2015-3-1' OR edate<'2015-2-1' THEN 0
WHEN edate>='2015-3-1' THEN datediff(day, sdate, '2015-3-1')
ELSE datediff(day,sdate,edate) END)
from a1 )
AS Feb_Days,
...
It's far from efficient. It will be more efficient to use a script or stored procedure running through your records and calculate the results.

MS SQL - Find if someone has worked more than 5 times in a week - looking for overtime

I would like to know if I select a static range of dates (May 1 thru June 30 for example) and then tell me if anyone has more than 5 calendar entries in one week (week1, week2, week3, week4). If easier it could be by selecting a week number in place of range of dates and then showing anyone working more than 5 times in week1 for example for the static range of dates.
This will tell me approximately if anyone has overtime scheduled.
EmpCalendar table (relevant columns in bold shown) (bullets are sample rows)
Cal_ID, user_id, days_date, WeekNumber
1, 34, 2015-04-01, Week1
3, 34, 2015-04-02, Week1
5, 34, 2015-04-03, Week1
7, 34, 2015-04-04, Week1
8, 34, 2015-04-05, Week1
9, 34, 2015-04-06, Week1
So in the above table we see that the Employee with user_id '34' has worked 6 times on WeekNumber of 'Week1'. I need it to return something like:
Tom Thumb (user_id = 34) worked 6 times in Week1 or within dates falling in the same week. Something to that effect. I am using ColdFusion 8 and MS SQL 2008.
Simple group by (assuming week numbers are not duplicated - depends on how they are assigned in the table):
Select UserID, WeekNumber
, count(distinct Days_Date) as DaysWorked
From EmpCalendar
--optional, if you want to limit the dates you're searching
where days_date between #startDate and #endDate
--not optional
group by UserID, WeekNumber
having count(distinct Days_Date) >= 5
Week1 can be duplicated within different years or months, so correct way of doing this is grouping by year and month together with week:
Select UserID, Year(Date), Month(Date), DATEPART( wk, Date), Count(*) As Days
From Table
Where Date Between #StartDate And #EndDate
Group by UserID, Year(Date), Month(Date), DATEPART( wk, Date)
Having Count(*) > 5

DateTime in sql only comparing the time

ID DateTime Code
---------- -------------- ----------
58 2015-01-01 20:00:00 1111
58 2015-01-11 10:00:00 8523
58 2015-01-11 03:00:00 4555
58 2015-01-19 00:01:00 8888
9 2015-01-01 20:00:00 4444
how do i count the number of codes for a specific ID ignoring which date it is but it must be between 20:00:00 and 06:00:00
select count(code) as count from table 1 where ID='58' and DateTime between '20:00:00' and '06:00:00'
the expected output would be
count
3
SELECT count(code) as count
FROM table1
WHERE
ID='58' and
(CAST(DateTime as time) >= '20:00'
or CAST(DateTime as time) <= '06:00')
EDIT: John, I understand the issue. Here is a full solution to handle those cases:
In order to use variables:
DECLARE #HourBegin time = '07:00'
DECLARE #HourEnd time = '17:30'
SELECT count(code) as count
FROM table1
WHERE
ID='58' and
(CAST(DateTime as time) between #HourBegin and #HourEnd or
((CAST(DateTime as time) <= #HourEnd or
CAST(DateTime as time) >= #HourBegin) and
#HourBegin > #HourEnd)
)
Almost the same as previous answer, but with hours it looks nicer for me and might be you need DISTINCT code
SELECT count(DISTINCT code) as count
FROM table1
WHERE
ID='58' and
(DATEPART(HOUR,DateTime) >= 20
or DATEPART(HOUR,DateTime) < 6)
UPDATED: changed from <= 6 to < 6
Update
This answer applies to MySQL.
When I started writing the answer, the question was tagged mysql and sql-server. The OP edited it in the meantime.
This query should do what you want on MySQL.
SELECT count(code) AS `count`
FROM `table 1`
WHERE ID='58'
AND TIME(`DateTime`) NOT BETWEEN '06:00:01' AND '19:59:59'
The MySQL function TIME() extracts only the time component from a DATETIME value.
On version 5.7, MySQL added support for fractional seconds (up to 6 digits) on DATETIME columns. The query above will include the entries having time greater than 06:00:00 but smaller than 06:00:01 (events that happened during the first second after 6 AM sharp).
For MySQL 5.7 and newer, the correct query is:
SELECT count(code) AS `count`
FROM `table 1`
WHERE ID='58'
AND (TIME(`DateTime`) <= '06:00:00' OR '20:00:00' <= TIME(`DateTime`))
I don't know about SQL Server.

Get record based on year in oracle

I am creating a query to give number of days between two days based on year. Actually I have below type of date range
From Date: TO_DATE('01-Jun-2011','dd-MM-yyyy')
To Date: TO_DATE('31-Dec-2013','dd-MM-yyyy')
My Result should be:
Year Number of day
------------------------------
2011 XXX
2012 XXX
2013 XXX
I've tried below query
WITH all_dates AS
(SELECT start_date + LEVEL - 1 AS a_date
FROM
(SELECT TO_DATE ('21/03/2011', 'DD/MM/YYYY') AS start_date ,
TO_DATE ('25/06/2013', 'DD/MM/YYYY') AS end_date
FROM dual
)
CONNECT BY LEVEL <= end_date + 1 - start_date
)
SELECT TO_CHAR ( TRUNC (a_date, 'YEAR') , 'YYYY' ) AS YEAR,
COUNT (*) AS num_days
FROM all_dates
WHERE a_date - TRUNC (a_date, 'IW') < 7
GROUP BY TRUNC (a_date, 'YEAR')
ORDER BY TRUNC (a_date, 'YEAR') ;
I got exact output
Year Number of day
------------------------------
2011 286
2012 366
2013 176
My question is if i use connect by then query execution takes long time as i have millions of records in table and hence i don't want to use connect by clause
connect by clause is creating virtual rows against the particular record.
Any help or suggestion would be greatly appreciated.
From your vague expected results I think you want the number of records between those dates, not the number of days; but it's rather unclear. Since you refer to a table in the question I assume you want something related to the table data, not simply days between two dates which wouldn't depend on a table at all. (I have no idea what the connect by clause reference means though). This should give you that, if it is what you want:
select extract(year from date_field), count(*)
from t42
where date_field >= to_date('01-Jun-2011', 'DD-MON-YYYY')
and date_field < to_date('31-Dec-2013') + interval '1' day
group by extract(year from date_field)
order by extract(year from date_field);
The where clause is as you'd expect between two dates; I've assumed there might be times in your date field (i.e. not all at midnight) and that you want to count all records on the last date in your range. Then it's grouping and counting based on the year for each record.
SQL Fiddle.
If you want the number of days that have records within the range, then you can just vary the count slightly:
select extract(year from date_field), count(distinct trunc(date_field))
...
SQL Fiddle.
you can use the below function to reduce the number of virtual rows by considering only the years in between.You can check the SQLFIDDLE to check the performance.
First consider only the number of days between start date and the year end of that year or
End date if it is in same year
Then consider the years in between from next year of start date to the year before the end date year
Finally consider the number of days from start of end date year to end date
Hence instead of iterating for all the days between start date and end date we need to iterate only the years
WITH all_dates AS
(SELECT (TO_CHAR(START_DATE,'yyyy') + LEVEL - 1) YEARS_BETWEEN,start_date,end_date
FROM
(SELECT TO_DATE ('21/03/2011', 'DD/MM/YYYY') AS start_date ,
TO_DATE ('25/06/2013', 'DD/MM/YYYY') AS end_date
FROM dual
)
CONNECT BY LEVEL <= (TO_CHAR(end_date,'yyyy')) - (TO_CHAR(start_date,'yyyy')-1)
)
SELECT DECODE(TO_CHAR(END_DATE,'yyyy'),YEARS_BETWEEN,END_DATE
,to_date('31-12-'||years_between,'dd-mm-yyyy'))
- DECODE(TO_CHAR(START_DATE,'yyyy'),YEARS_BETWEEN,START_DATE
,to_date('01-01-'||years_between,'dd-mm-yyyy'))+1,years_between
FROM ALL_DATES;
In Oracle you can perform Addition and Substraction to dates like this...
SELECT
TO_DATE('31-Dec-2013','dd-MM-yyyy') - TO_DATE('01-Jun-2011','dd-MM-yyyy')
DAYS FROM DUAL;
it will return day difference between two dates....
select to_date(2011, 'yyyy'), to_date(2012, 'yyyy'), to_date(2013, 'yyyy')
from dual;
TO_DATE(2011,'Y TO_DATE(2012,'Y TO_DATE(2013,'Y
--------------- --------------- ---------------
01-MAY-11 01-MAY-12 01-MAY-13
select to_char(date_field,'yyyy'), count(*)
from your_table
where date_field between to_date('01-Jun-2011', 'DD-MON-YYYY')
and to_date('31-Dec-2013 23:59:59', 'DD-MON-YYYY hh24:mi:ss')
group by to_char(date_field,'yyyy')
order by to_char(date_field,'yyyy');

Resources