Find overlapping time ranges - tsql

I'm surprised this hasn't come up yet.
In T-SQL, I need to find the intervals (defined by startDateTime and endDateTime) that overlap with daily interval (say 9am-5pm).
For example, with table:
CREATE TABLE [dbo].[Interval](
[startDateTime] [datetime] NOT NULL,
[endDateTime] [datetime] NOT NULL
)
Solution would be the procedure that returns only overlapping intervals:
CREATE PROCEDURE FindIntervals
-- Add the parameters for the stored procedure here
#from varchar(5) = '9:00',
#to varchar(5) = '17:00'
AS
BEGIN
select * from Interval
where ...
END
GO
EDIT:
Example intervals:
Sep 7 2011 8:00 AM - Sep 7 2011 8:30 PM
Sep 7 2011 11:00 AM - Sep 7 2011 1:00 PM
Sep 7 2011 1:00 PM - Sep 7 2011 6:00 PM
Sep 9 2011 8:00 AM - Sep 9 2011 8:30 PM
Sep 9 2011 11:00 AM - Sep 9 2011 1:00 PM
Sep 9 2011 1:00 PM - Sep 9 2011 6:00 PM
So, for given interval "nine to five", 2, 3, 5 and 6 should be returned, as they overlap the given input.
But,
Sep 9 2011 8:00 AM - Sep 10 2011 8:30 PM
also fits, because it includes entire day.
Please, I need help with matching string and datetime values in T-SQL, not abstract "less then"/"greater then" solutions.

declare #Interval table
(
startDateTime datetime,
endDateTime datetime
)
insert into #Interval values
('2011-09-07T08:00:00', '2011-09-07T08:30:00'),
('2011-09-07T11:00:00', '2011-09-07T13:00:00'),
('2011-09-07T13:00:00', '2011-09-07T18:00:00'),
('2011-09-09T08:00:00', '2011-09-09T08:30:00'),
('2011-09-09T11:00:00', '2011-09-09T13:00:00'),
('2011-09-09T13:00:00', '2011-09-09T18:00:00'),
('2011-09-09T08:00:00', '2011-09-10T08:30:00')
declare #from varchar(5) = '09:00'
declare #to varchar(5) = '17:00'
;with L(MinDate, MaxDate) as
(
select dateadd(day, datediff(day, 0, min(startDateTime)), 0),
dateadd(day, datediff(day, 0, max(endDateTime)), 0)
from #Interval
),
D(fromTime, endTime) as
(
select dateadd(day, Number.number, L.MinDate)+cast(#from as datetime),
dateadd(day, Number.number, L.MinDate)+cast(#to as datetime)
from L
inner join master..spt_values as Number
on Number.number <= datediff(day, L.MinDate, L.MaxDate)
where Number.type = 'P'
)
select I.startDateTime,
I.endDateTime
from #Interval as I
where exists (select *
from D
where I.startDateTime < D.endTime and
I.endDateTime > D.fromTime)
Result:
startDateTime endDateTime
----------------------- -----------------------
2011-09-07 11:00:00.000 2011-09-07 13:00:00.000
2011-09-07 13:00:00.000 2011-09-07 18:00:00.000
2011-09-09 11:00:00.000 2011-09-09 13:00:00.000
2011-09-09 13:00:00.000 2011-09-09 18:00:00.000
2011-09-09 08:00:00.000 2011-09-10 08:30:00.000
If you expect to have a date range of more than 2048 days you need to replace master..spt_values with a numbers table. Make sure the numbers table starts with 0.
SQL Server 2008 version
;with L(MinDate, MaxDate) as
(
select cast(min(startDateTime) as date),
cast(max(endDateTime) as date)
from #Interval
),
D(fromTime, endTime) as
(
select dateadd(day, Number.number, L.MinDate)+cast(#from as datetime),
dateadd(day, Number.number, L.MinDate)+cast(#to as datetime)
from L
inner join master..spt_values as Number
on Number.number <= datediff(day, L.MinDate, L.MaxDate)
where Number.type = 'P'
)
select I.startDateTime,
I.endDateTime
from #Interval as I
where exists (select *
from D
where I.startDateTime < D.endTime and
I.endDateTime > D.fromTime)

Related

Group query results by month and year in postgresql with emply month sum

Based on this answer by Burak Arslan
SELECT date_trunc('month', txn_date) AS txn_month, sum(amount) as monthly_sum
FROM yourtable
GROUP BY txn_month
Is there a way to get months that have no results to show in the query?
So let's say I have :
id transDate Product Qty
1234 04/12/2019 ABCD 2
1245 04/05/2019 ABCD 1
1231 02/07/2019 ABCD 6
I also need to the the third Month returns with a 0 value
MonthYear totalQty
02/2019 6
03/2019 0
04/2019 3
Thanks,
---- UPDATE ---
Here is the final query that that gets last 24 months from the current date. with year and month ready for any charts.
Thanks to #a_horse_with_no_name
SELECT
--ONLY USE THE NEXT LINE IF YOU NEED TO HAVE THE ID IN YOUR RESULT
CASE WHEN t."ItemId" IS NULL THEN 10607 ELSE t."ItemId" END AS "ItemId",
TO_CHAR(y."transactionDate", 'yyyy-mm-dd') AS txn_month,
TO_CHAR(y."transactionDate", 'yyyy') AS "Year",
TO_CHAR(y."transactionDate", 'Mon') AS "Month",
-coalesce(SUM(t."transactionQty"),0) AS "TotalSold"
FROM generate_series(
TO_CHAR(CURRENT_DATE - INTERVAL '24 month', 'yyyy-mm-01')::date ,
TO_CHAR(CURRENT_DATE, 'yyyy-mm-01')::date,
INTERVAL '1 month') as y("transactionDate")
LEFT JOIN "ItemTransactions" AS t
ON date_trunc('month', t."transactionDate") = y."transactionDate"
AND t."ItemTransactionTypeId" = 1
AND t."ItemId" = 10607
GROUP BY txn_month, "Year", "Month", t."ItemId"
ORDER BY txn_month ASC;
EXEMPLE OUTPUT
ItemId txn_month Year Month TotalSold
10607 2018-03-01 2018 Mar 2
10607 2018-04-01 2018 Apr 0
10607 2018-05-01 2018 May 8
10607 2018-06-01 2018 Jun 12
10607 2018-07-01 2018 Jul 6
10607 2018-08-01 2018 Aug 4
10607 2018-09-01 2018 Sep 6
10607 2018-10-01 2018 Oct 8
10607 2018-11-01 2018 Nov 4
10607 2018-12-01 2018 Dec 0
10607 2019-01-01 2019 Jan 2
10607 2019-02-01 2019 Feb 3
10607 2019-03-01 2019 Mar 4
10607 2019-04-01 2019 Apr 1
10607 2019-05-01 2019 May 4
10607 2019-06-01 2019 Jun 3
10607 2019-07-01 2019 Jul 5
10607 2019-08-01 2019 Aug 6
10607 2019-09-01 2019 Sep 6
10607 2019-10-01 2019 Oct 6
10607 2019-11-01 2019 Nov 3
10607 2019-12-01 2019 Dec 0
10607 2020-01-01 2020 Jan 4
10607 2020-02-01 2020 Feb 2
10607 2020-03-01 2020 Mar 0
Left join to a list of months:
SELECT t.txn_month,
coalesce(sum(yt.amount),0) as monthly_sum
FROM generate_series(date '2019-02-01', date '2019-04-01', interval '1 month') as t(txn_month)
left join yourtable yt on date_trunc('month', yt.transdate) = t.txn_month
GROUP BY t.txn_month
Online example
In your actual query you need to move the conditions from the WHERE clause to the JOIN condition. Putting them into the WHERE clause turns the outer join back into an inner join:
SELECT t."ItemId",
y."transactionDate" AS txn_month,
-coalesce(SUM(t."transactionQty"),0) AS "TotalSold"
FROM generate_series(date '2018-01-01', date '2020-04-01', INTERVAL '1 month') as y("transactionDate")
LEFT JOIN "ItemTransactions" AS t
ON date_trunc('month', t."transactionDate") = y."transactionDate"
AND t."ItemTransactionTypeId" = 1
AND t."ItemId" = 10606
-- this WHERE clause isn't really needed because of the date values provided to generate_series()
WHERE AND y."transactionDate" >= NOW() - INTERVAL '2 year'
GROUP BY txn_month, t."ItemId"
ORDER BY txn_month DESC;

How to sort attendance date along with the month?

Attendance is sorting according to date, that is fine, but I want to sort date along with the month name January should come at the bottom, and December at the top.
Table
Attendance Date
---------------
26 Feb 2018
19 Dec 2018
18 Dec 2018
14 Dec 2018
12 June 2018
7 Dec 2018
5 Feb 2018
Query
select distinct
(select ARRAY_TO_STRING(ARRAY_AGG(ARRAY[to_char(t1.l_time,'HH12:mi AM')]::text), ',')
from
(select (al1.create_time AT TIME ZONE 'UTC+5:30')::time as l_time
from users.access_log as al1
where al1.user_id = al.user_id
and al1.login_status = 1
and al1.create_time::date = al.create_time::date
order by al1.create_time::time ASC
) as t1
) as login_time,
(select ARRAY_TO_STRING(ARRAY_AGG(ARRAY[to_char(t2.o_time,'HH:mi AM')]::text), ',')
from
(select (al2.create_time AT TIME ZONE 'UTC+5:30')::time as o_time
from users.access_log as al2
where al2.user_id = al.user_id
and al2.login_status = 0
and al2.create_time::date = al.create_time::date
order by al2.create_time::time ASC
) as t2
) as logout_time,
al.create_time::date
from users.access_log as al
where al.user_id = ?;
Attendance is sorting according to date, that is fine, but I want to sort date along with the month name January should come at the bottom, and December at the top.

Date Range search - tSQL using nvarchar

Apologies for not using the correct type (date). Poor choice using nvarchar, but I cannot convert at this stage.
To the question:
I want to be able to search for data in a certain date range, e.g., 10.01.16 -> 19.02.16
However, it seems to bring back only the first two digits worth of data, so everything between 10 and 19 regardless of month and year.
My query is as follows:
SELECT ID, Day, Date FROM oneHr$
WHERE date >= CONVERT(NVARCHAR, '10.01.16', 4)
AND date <= CONVERT(NVARCHAR , '19.02.16', 4)
ORDER BY Date ASC
Any ideas? Help very much appreciated and thanks in advance.
This is what is being returned:
ID Day Date
--------------------
943 fri 10.02.15
746 mon 10.02.16
234 tue 10.03.15
835 fri 10.04.15
988 tue 10.05.15
487 wed 11.01.16
343 wed 11.02.15
874 mon 12.01.16
663 thu 12.01.15
198 tue 12.02.15
775 wed 13.01.16
993 thu 14.01.15
375 fri 15.03.15
337 wed 16.12.15
784 tue 17.11.15
777 mon 18.08.15
252 thu 19.01.16
664 wed 19.02.15
UPDATE
So, I've changed Date to be of type datetime and all looking good. However, I'm trying to define a range rather than hard code it and it isn't working. Any ideas?
set #date1 = '2016-01-01 00:00:00' -- Date1 (start range)
set #date2 = '2016-01-10 00:00:00' -- Date2 (end range)
/* Not Working */
select * from oneHr$
where Date >= #date1
and Date <= #date2
order by ID
/* Working */
select * from oneHr$
where Date >= '2016-01-01 00:00:00'
and Date <= '2016-01-10 00:00:00'
order by ID
Why not do something like this?
SELECT ID, Day, Date FROM oneHr$
WHERE CONVERT(DATE, date, 4) >= #Date1
AND CONVERT(DATE, date, 4) <= #Date2
ORDER BY Date ASC
Then you won't have to convert your inputs to nvarchar at all.

PostgreSQL - GROUP subsequent rows

I have a table which contains some records ordered by date.
And I want to get start and end dates for each subsequent group (grouped by some criteria e.g.position).
Example:
create table tbl (id int, date timestamp without time zone,
position int);
insert into tbl values
( 1 , '2013-12-01', 1),
( 2 , '2013-12-02', 2),
( 3 , '2013-12-03', 2),
( 4 , '2013-12-04', 2),
( 5 , '2013-12-05', 3),
( 6 , '2013-12-06', 3),
( 7 , '2013-12-07', 2),
( 8 , '2013-12-08', 2)
Of course if I simply group by position I will get wrong result as positions could be the same for different groups:
SELECT POSITION, min(date) MIN, max(date) MAX
FROM tbl GROUP BY POSITION
I will get:
POSITION MIN MAX
1 December, 01 2013 00:00:00+0000 December, 01 2013 00:00:00+0000
3 December, 05 2013 00:00:00+0000 December, 06 2013 00:00:00+0000
2 December, 02 2013 00:00:00+0000 December, 08 2013 00:00:00+0000
But I want:
POSITION MIN MAX
1 December, 01 2013 00:00:00+0000 December, 01 2013 00:00:00+0000
2 December, 02 2013 00:00:00+0000 December, 04 2013 00:00:00+0000
3 December, 05 2013 00:00:00+0000 December, 06 2013 00:00:00+0000
2 December, 07 2013 00:00:00+0000 December, 08 2013 00:00:00+0000
I found a solution for MySql which uses variables and I could port it but I believe PostgreSQL can do it in some smarter way using its advanced features like window functions.
I'm using PostgreSQL 9.2
There is probably more elegant solution but try this:
WITH tmp_tbl AS (
SELECT *,
CASE WHEN lag(position,1) OVER(ORDER BY id)=position
THEN position
ELSE ROW_NUMBER() OVER(ORDER BY id)
END AS grouping_col
FROM tbl
)
, tmp_tbl2 AS(
SELECT position,date,
CASE WHEN lag(position,1)OVER(ORDER BY id)=position
THEN lag(grouping_col,1) OVER(ORDER BY id)
ELSE ROW_NUMBER() OVER(ORDER BY id)
END AS grouping_col
FROM tmp_tbl
)
SELECT POSITION, min(date) MIN, max(date) MAX
FROM tmp_tbl2 GROUP BY grouping_col,position
There are some complete answers on Stackoverflow for that, so I'll not repeat them in detail, but the principle of it is to group the records according to the difference between:
The row number when ordered by the date (via a window function)
The difference between the dates and a static date of reference.
So you have a series such as:
rownum datediff diff
1 1 0 ^
2 2 0 | first group
3 3 0 v
4 5 1 ^
5 6 1 | second group
6 7 1 v
7 9 2 ^
8 10 2 v third group

linq to sql merge dates into groups

I have a simple sql query like so:
SELECT dt AS 'startDate'
, dt AS 'endDate'
FROM
WorkCalendar
WHERE
dt BETWEEN dateadd(yy, datediff(yy, 0, getdate()), 0) AND dateadd(MILLISECOND, -3, dateadd(YEAR, datediff(YEAR, 0, getdate()) + 1, 0))
AND isWorkDay = 0
This returns all dates from my table containing days free from work in current year.
This is sample output:
startDate endDate
2012-01-01 00:00:00 2012-01-01 00:00:00
2012-01-06 00:00:00 2012-01-06 00:00:00
2012-01-07 00:00:00 2012-01-07 00:00:00
2012-01-08 00:00:00 2012-01-08 00:00:00
2012-01-14 00:00:00 2012-01-14 00:00:00
2012-01-15 00:00:00 2012-01-15 00:00:00
2012-01-21 00:00:00 2012-01-21 00:00:00
2012-01-22 00:00:00 2012-01-22 00:00:00
What I would like to do is to group near dates like so:
startDate endDate
2012-01-01 00:00:00 2012-01-01 00:00:00
2012-01-06 00:00:00 2012-01-08 00:00:00
2012-01-14 00:00:00 2012-01-15 00:00:00
2012-01-21 00:00:00 2012-01-22 00:00:00
If I have 2 or more days that are one by another I would like to join them into groups.
I would like this done with linq to sql as it will be simpler for me to use in webservice, but simple sql will do the trick :)
Got something like this:
WITH d (d1)
AS (SELECT dt
FROM workcalendar
WHERE dt BETWEEN Dateadd(yy, Datediff(yy, 0, Getdate()), 0) AND
Dateadd(millisecond, -3,
Dateadd(year, Datediff(year,
0,
Getdate()) + 1
, 0))
AND isworkday = 0)
SELECT Z1.d1 AS startDate,
Z2.d1 AS endDate
FROM (SELECT Row_number()
OVER (
ORDER BY A.d1) AS 'ID',
A.d1
FROM d AS A
WHERE NOT EXISTS (SELECT *
FROM d AS C
WHERE A.d1 = Dateadd(d, 1, C.d1))) AS Z1,
(SELECT Row_number()
OVER (
ORDER BY A.d1) AS 'ID',
A.d1
FROM d AS A
WHERE NOT EXISTS (SELECT *
FROM d AS C
WHERE A.d1 = Dateadd(d, -1, C.d1))) AS Z2
WHERE Z1.id = Z2.id
But any comments and optimization are welcome :)