Find nearest date to start and end of the month - sql

Table contains daily snapshots of specific parameter, but data can be missing for some days. Task is to calculate amount per month, for this sake we need values on start/end of the month, if data is missing, we need pairs of nearest dates i.e:
[Time] Value
2015-04-28 00:00:00.000 76127
2015-05-03 00:00:00.000 76879
2015-05-22 00:00:00.000 79314
2015-06-07 00:00:00.000 81443
Currently i use following code:
select
*
from(
select
[Time],
Value,
ROW_NUMBER() over (partition by CASE WHEN [Time] < '2015-05-01' THEN 1 ELSE 0 END order by abs(DATEDIFF(DAY, '2015-05-01', [Time]))) as rn2,
ROW_NUMBER() over (partition by CASE WHEN [Time] > '2015-05-01' THEN 1 ELSE 0 END order by abs(DATEDIFF(DAY, [Time], '2015-05-01'))) as rn3,
ROW_NUMBER() over (partition by CASE WHEN [Time] < '2015-05-31' THEN 1 ELSE 0 END order by abs(DATEDIFF(DAY, '2015-05-31', [Time]))) as rn4,
ROW_NUMBER() over (partition by CASE WHEN [Time] > '2015-05-31' THEN 1 ELSE 0 END order by abs(DATEDIFF(DAY, [Time], '2015-05-31'))) as rn5,
DATEDIFF(DAY, '2015-05-01', [Time]) as doff,
DATEDIFF(DAY, '2015-05-31', [Time]) as doff2
from
ValueTable
where
[Time] between '2015-04-01' and '2015-06-30'
) r
where
doff = 0 or doff2 = 0 or (doff != 0 and rn2 = 1 and rn3 = 1) or (doff2 != 0 and rn4 = 1 and rn5 = 1)
Is there any more efficient way to do it?

The following code is going to look more complicated because it is longer. However, it should be very fast, because it can make very good use of an index on ValueTable([Time]).
The idea is to look for exact matches. If there are no exact matches, then find the first and last records before and after the dates. This requires union all on six subqueries, but each should make optimal use of an index:
with exact_first as (
select t.*
from ValueTable t
where [Time] = '2015-05-01'
),
exact_last as (
select t.*
from ValueTable t
where [Time] = '2015-05-01'
)
(select ef.*
from exact_first ef
) union all
(select top 1 t.*
from ValueTable t
where [Time] < '2015-05-01' and
not exists (select 1 from exact_first ef2)
order by [Time]
) union all
(select top 1 t.*
from ValueTable t
where [Time] > '2015-05-01' and
not exists (select 1 from exact_first ef2)
order by [Time] desc
) union all
(select el.*
from exact_last el
) union all
(select top 1 t.*
from ValueTable t
where [Time] < '2015-05-31' and
not exists (select 1 from exact_last ef2)
order by [Time]
) union all
(select top 1 t.*
from ValueTable t
where [Time] > '2015-05-31' and
not exists (select 1 from exact_last ef2)
order by [Time] desc;
)

Related

Selecting 1 row per day closest to 4am? [duplicate]

This question already has answers here:
Get top 1 row of each group
(19 answers)
Closed 6 years ago.
We're currently working on a query for a report that returns a series of data. The customer has specified that they want to receive 5 rows total, with the data from the previous 5 days (as defined by a start date and an end date variable). For each day, they want the data from the row that's closest to 4am.
I managed to get it to work for a single day, but I certainly don't want to union 5 separate select statements simply to fetch these values. Is there any way to accomplish this via CTEs?
select top 1
'W' as [RecordType]
, [WellIdentifier] as [ProductionPtID]
, t.Name as [Device Name]
, t.RecordDate --convert(varchar, t.RecordDate, 112) as [RecordDate]
, TubingPressure as [Tubing Pressure]
, CasingPressure as [Casing Pressure]
from #tTempData t
Where cast (t.recorddate as time) = '04:00:00.000'
or datediff (hh,'04:00:00.000',cast (t.recorddate as time)) < -1.2
order by Name, RecordDate desc
assuming that the #tTempData only contains the previous 5 days records
SELECT *
FROM
(
SELECT *, rn = row_number() over
(
partition by convert(date, recorddate)
order by ABS ( datediff(minute, convert(time, recorddate) , '04:00' )
)
FROM #tTempData
)
WHERE rn = 1
You can use row_number() like this to get the top 5 last days most closest to 04:00
SELECT TOP 5 * FROM (
select t.* ,
ROW_NUMBER() OVER(PARTITION BY t.recorddate
ORDER BY abs(datediff (minute,'04:00:00.000',cast (t.recorddate as time))) rnk
from #tTempData t)
WHERE rnk = 1
ORDER BY recorddate DESC
You can use row_number() for this purpose:
select t.*
from (select t.*,
row_number() over (partition by cast(t.recorddate as date)
order by abs(datediff(ms, '04:00:00.000',
cast(t.recorddate as time)
))
) seqnum
from #tTempData t
) t
where seqnum = 1;
You can add an appropriate where clause in the subquery to get the dates that you are interested in.
Try something like this:
select
'W' as [RecordType]
, [WellIdentifier] as [ProductionPtID]
, t.Name as [Device Name]
, t.RecordDate --convert(varchar, t.RecordDate, 112) as [RecordDate]
, TubingPressure as [Tubing Pressure]
, CasingPressure as [Casing Pressure]
from #tTempData t
Where exists
(select 1 from #tTempData t1 where
ABS(datediff (hh,'04:00:00.000',cast (t.recorddate as time))) <
ABS(datediff (hh,'04:00:00.000',cast (t1.recorddate as time)))
and GETDATE(t.RecordDate) = GETDATE(t1.RecordDate)
)dt
and t.RecordDate between YOURDATERANGE
order by Name, RecordDate desc;

How can I find a value that doesn't exist in a table?

I have a simplified table called Bookings that has two columns BookDate and BookSlot. The BookDate column will have dates only (no time) and the BookSlot column will contain the time of the day in intervals of 30 minutes from 0 to 1410 inclusive. (i.e. 600 = 10:00am)
How can I find the first slot available in the future (not booked) without running through a loop?
Here is the table definition and test data:
Create Table Bookings(
BookDate DateTime Not Null,
BookSlot Int Not Null
)
Go
Insert Into Bookings(BookDate,BookSlot) Values('2014-07-01',0);
Insert Into Bookings(BookDate,BookSlot) Values('2014-07-01',30);
Insert Into Bookings(BookDate,BookSlot) Values('2014-07-01',60);
Insert Into Bookings(BookDate,BookSlot) Values('2014-07-01',630);
Insert Into Bookings(BookDate,BookSlot) Values('2014-07-02',60);
Insert Into Bookings(BookDate,BookSlot) Values('2014-07-02',90);
Insert Into Bookings(BookDate,BookSlot) Values('2014-07-02',120);
I want a way to return the first available slot that is not in the table and that is in the future (based on server time).
Based on above test data:
If the current server time was 1st Jul, 00:10am, the result should be 1st Jul, 90min (01:30am).
If the current server time was 2nd Jul, 01:05am, the result should be 2nd Jul, 150min (02:30am).
If there are no bookings in the future, the function would simply return the closest half-hour in the future.
--
SQL Fiddle for this is here:
http://sqlfiddle.com/#!6/0e93d/1
Below is one method that will allow bookings up to 256 days in the future, and allow for an empty Booking table. I assume you are using SQL Server 2005 since your BookDate is dateTime instead of date.
In any case, you might consider storing the slots as a complete datetime instead of separate columns. That will facilitate queries and improve performance.
DECLARE #now DATETIME = '2014-07-01 00:10:00';
WITH T4
AS (SELECT N
FROM (VALUES(0),
(0),
(0),
(0),
(0),
(0),
(0),
(0)) AS t(N)),
T256
AS (SELECT Row_number()
OVER(
ORDER BY (SELECT 0)) - 1 AS n
FROM T4 AS a
CROSS JOIN T4 AS b
CROSS JOIN T4 AS c),
START_DATE
AS (SELECT Dateadd(DAY, Datediff(DAY, '', #now), '') AS start_date),
START_TIME
AS (SELECT Dateadd(MINUTE, Datediff(MINUTE, '', #now) / 30 * 30, '') AS
start_time),
DAILY_INTERVALS
AS (SELECT N * 30 AS interval
FROM T256
WHERE N < 48)
SELECT TOP (1) Dateadd(DAY, future_days.N, START_DATE) AS BookDate,
DAILY_INTERVALS.INTERVAL AS BookSlot
FROM START_DATE
CROSS APPLY START_TIME
CROSS APPLY DAILY_INTERVALS
CROSS APPLY T256 AS future_days
WHERE Dateadd(MINUTE, DAILY_INTERVALS.INTERVAL,
Dateadd(DAY, future_days.N, START_DATE)) > START_TIME
AND NOT EXISTS(SELECT *
FROM DBO.BOOKINGS
WHERE BOOKDATE = START_DATE
AND BOOKSLOT = DAILY_INTERVALS.INTERVAL)
ORDER BY BOOKDATE,
BOOKSLOT;
See this SQL Fiddle
It's a bit complicated but try this:
WITH DATA
AS (SELECT *,
Row_number()
OVER (
ORDER BY BOOKDATE, BOOKSLOT) RN
FROM BOOKINGS)
SELECT CASE
WHEN T.BOOKSLOT = 1410 THEN Dateadd(DAY, 1, BOOKDATE)
ELSE BOOKDATE
END Book_Date,
CASE
WHEN T.BOOKSLOT = 1410 THEN 0
ELSE BOOKSLOT + 30
END Book_Slot
FROM (SELECT TOP 1 T1.*
FROM DATA T1
LEFT JOIN DATA t2
ON t1.RN = T2.RN - 1
WHERE t2.BOOKSLOT - t1.BOOKSLOT > 30
OR ( t1.BOOKDATE != T2.BOOKDATE
AND ( t2.BOOKSLOT != 0
OR t1.BOOKSLOT != 630 ) )
OR t2.BOOKSLOT IS NULL)T
Here is the SQL fiddle example.
Explanation
This solution contains 2 parts:
Comparing each line to the next and checking for a gap (can be done easier in SQL 2012)
Adding a half an hour to create the next slot, this includes moving to the next day if needed.
Edit
Added TOP 1 in the query so that only the first slot is returned as requested.
Update
Here is the updated version including 2 new elements (getting current date+ time and dealing with empty table):
DECLARE #Date DATETIME = '2014-07-01',
#Slot INT = 630
DECLARE #time AS TIME = Cast(Getdate() AS TIME)
SELECT #Slot = Datepart(HOUR, #time) * 60 + Round(Datepart(MINUTE, #time) / 30,
0) * 30
+ 30
SET #Date = Cast(Getdate() AS DATE)
;WITH DATA
AS (SELECT *,
Row_number()
OVER (
ORDER BY BOOKDATE, BOOKSLOT) RN
FROM BOOKINGS
WHERE BOOKDATE > #Date
OR ( BOOKDATE = #Date
AND BOOKSLOT >= #Slot ))
SELECT TOP 1 BOOK_DATE,
BOOK_SLOT
FROM (SELECT CASE
WHEN RN = 1
AND NOT (#slot = BOOKSLOT
AND #Date = BOOKDATE) THEN #Date
WHEN T.BOOKSLOT = 1410 THEN Dateadd(DAY, 1, BOOKDATE)
ELSE BOOKDATE
END Book_Date,
CASE
WHEN RN = 1
AND NOT (#slot = BOOKSLOT
AND #Date = BOOKDATE) THEN #Slot
WHEN T.BOOKSLOT = 1410 THEN 0
ELSE BOOKSLOT + 30
END Book_Slot,
1 AS ID
FROM (SELECT TOP 1 T1.*
FROM DATA T1
LEFT JOIN DATA t2
ON t1.RN = T2.RN - 1
WHERE t2.BOOKSLOT - t1.BOOKSLOT > 30
OR ( t1.BOOKDATE != T2.BOOKDATE
AND ( t2.BOOKSLOT != 0
OR t1.BOOKSLOT != 1410 ) )
OR t2.BOOKSLOT IS NULL)T
UNION
SELECT #date AS bookDate,
#slot AS BookSlot,
2 ID)X
ORDER BY X.ID
Play around with the SQL fiddle and let me know what you think.
In SQL Server 2012 and later, you can use the lead() function. The logic is a bit convoluted because of all the boundary conditions. I think this captures it:
select top 1
(case when BookSlot = 1410 then BookDate else BookDate + 1 end) as BookDate,
(case when BookSlot = 1410 then 0 else BookSlot + 30 end) as BookSlot
from (select b.*,
lead(BookDate) over (order by BookDate) as next_dt,
lead(BookSlot) over (partition by BookDate order by BookSlot) as next_bs
from bookings b
) b
where (next_bs is null and BookSlot < 1410 or
next_bs - BookSlot > 30 or
BookSlot = 1410 and (next_dt <> BookDate + 1 or next_dt = BookDate and next_bs <> 0)
)
order by BookDate, BookSlot;
Using a tally table to generate a list of originally available booking slots out 6 weeks (adjustable below):
declare #Date as date = getdate();
declare #slot as int = 30 * (datediff(n,#Date,getdate()) /30);
with
slots as (
select (ROW_NUMBER() over (order by s)-1) * 30 as BookSlot
from(
values (1),(1),(1),(1),(1),(1),(1),(1) -- 4 hour block
)slots(s)
cross join (
values (1),(1),(1),(1),(1),(1) -- 6 blocks of 4 hours each day
)QuadHours(t)
)
,days as (
select (ROW_NUMBER() over (order by s)-1) + getdate() as BookDate
from (
values (1),(1),(1),(1),(1),(1),(1) -- 7 days in a week
)dayList(s)
cross join (
-- set this to number of weeks out to allow bookings to be made
values (1),(1),(1),(1),(1),(1) -- allow 6 weeks of bookings at a time
)weeks(t)
)
,tally as (
select
cast(days.BookDate as date) as BookDate
,slots.BookSlot as BookSLot
from slots
cross join days
)
select top 1
tally.BookDate
,tally.BookSlot
from tally
left join #Bookings book
on tally.BookDate = book.BookDate
and tally.BookSlot = book.BookSlot
where book.BookSlot is null
and ( tally.BookDate > #Date or tally.BookSlot > #slot )
order by tally.BookDate,tally.BookSlot;
go
try this:
SELECT a.bookdate, ((a.bookslot/60.)+.5) * 60
FROM bookings a LEFT JOIN bookings b
ON a.bookdate=b.bookdate AND (a.bookslot/60.)+.50=b.bookslot/60.
WHERE b.bookslot IS null

Using T-SQL, how do I generate a result that shows a range of dates

Using SQL Server, how do I generate a result set that shows a range of dates, like so:
StartDate EndDate
01/01/2014 01/04/2014
01/08/2014 01/11/2014
01/14/2014 01/15/2014
The original data had the dates in this format:
ColumnA DateColumn
blah 01/01/2014
blah 01/02/2014
blah 01/03/2014
blah 01/04/2014
blah 01/08/2014
blah 01/09/2014
blah 01/10/2014
blah 01/11/2014
blah 01/14/2014
blah 01/15/2014
Currently, I have a bunch of queries that does this, but I'm wondering if I can do something in less code:
SELECT ROW_NUMBER() OVER(ORDER BY DateColumn) AS rownum,
DateColumn
INTO #main
FROM MyTable
SELECT m1.DateColumn AS TBegin,
m2.DateColumn AS TEnd,
COALESCE(DATEDIFF(day, m2.TimePk, m1.TimePk), 0) AS Gap
INTO #Gap
FROM #main m1
LEFT OUTER JOIN #main m2
ON m1.rownum = m2.rownum + 1
ORDER BY m1.DateColumn
SELECT ROW_NUMBER() OVER(ORDER BY i_id, TBegin) AS rownum,
TBegin
INTO #Begin
FROM #Gap
WHERE Gap <> 1
ORDER BY TBegin
SELECT ROW_NUMBER() OVER(ORDER BY i_id, TEnd) AS rownum,
TEnd
INTO #End
FROM (
SELECT TEnd
FROM #Gap
WHERE Gap > 1
UNION
SELECT MAX(TBegin)
FROM #Gap
) as t
ORDER BY TEnd
SELECT b.TBegin,
e.TEnd
FROM #Begin b
INNER JOIN #End e
ON b.i_id = e.i_id
AND b.rownum = e.rownum
ORDER BY b.TBegin
Any ideas on how to simplify or approach this in an entirely different way?
My approach to these is to identify the first date that has no date preceding it. This is the beginning of a group. Then I take the cumulative sum of that as a group identifier, and do the aggregation.
SQL Server 2008 doesn't have lag or cumulative sums, so I use correlated subqueries for this:
with mt as (
select t.*,
(case when (select top 1 t2.dateColumn
from MyTable t2
where t2.ColumnA = t.ColumnA and
t2.dateColumn < t.dateColumn
order by t2.dateColumn desc
) = dateadd(day, -1, t.datecolumn)
then 0
else 1
end) as IsStart
from MyTable t
),
mtcum as (
select mt.*,
(select sum(mt2.IsStart)
from mt mt2
where mt2.ColumnA = mt.ColumnA and
mt2.dateColumn <= mt.DateColumn
) as grpId
from mt
)
select ColumnA, min(dateColumn) as StartDate, max(dateColumn) as EndDate
from mtcum
group by ColumnA, grpId;
EDIT:
An easier way of approach this is with the observation that the difference between a sequence of dates and a sequence of numbers is constant.
select columnA, min(dateColumn) as StartDate, max(dateColumn) as EndDate
from (select mt.*, row_number() over (partition by ColumnA order by datecolumn) as seqnum
from mytable mt
) t
group by columnA, dateadd(day, - seqnum, datecolumn);
This will work for you. It's still fairly complex though. It uses inner queries to find the first date that is after a gap for each date. This way all days belonging to the same group of dates can be grouped together.
select MIN(DateColumn) StartDate, MAX(DateColumn) EndDate from
(select X.DateColumn, MIN(Y.DateColumn) MinOverGap from
(select DateColumn, ROW_NUMBER() OVER (ORDER BY DateColumn) RowNumber
from MyTable) X
left join
(select DateColumn, ROW_NUMBER() OVER (ORDER BY DateColumn) RowNumber
from MyTable) Y
on DATEADD(d, Y.RowNumber - 1, X.DateColumn) <> DATEADD(d, X.RowNumber -1, Y.DateColumn) AND X.DateColumn < Y.DateColumn
group by x.DateColumn) grouped
group by MinOverGap
order by 1

SQL query to return data corresponding to all values of a column except for the min value of that column

I have a table with the following columns:
userid, datetime, type
Sample data:
userid datetime type
1 2013-08-01 08:10:00 I
1 2013-08-01 08:12:00 I
1 2013-08-01 08:12:56 I
I need to fetch data for only two rows other than the row with min(datetime)
my query to fetch data for min(datetime) is :
SELECT
USERID, MIN(CHECKTIME) as ChkTime, CHECKTYPE, COUNT(*) AS CountRows
FROM
T1
WHERE
MONTH(CONVERT(DATETIME, CHECKTIME)) = MONTH(DATEADD(MONTH, -1,
CONVERT(DATE, GETDATE())))
AND YEAR(CONVERT(DATETIME, CHECKTIME)) = YEAR(GETDATE()) AND USERID=35
AND CHECKTYPE='I'
GROUP BY
CONVERT(DATE, CHECKTIME), USERID, CHECKTYPE
HAVING
COUNT(*) > 1
a lil help'll be much appreciated..thnx
Maybe something like this will help you:
WITH CTE AS
(
SELECT *, ROW_NUMBER() OVER (PARTITION BY userid ORDER BY checktime) RN
FROM dbo.T1
WHERE CHECKTYPE = 'I'
--add your conditions here
)
SELECT * FROM CTE
WHERE RN > 1
Using CTE and ROW_NUMBER() function this will select all rows except min(date) for each user.
SQLFiddle DEMO
SELECT * FROM YOURTABLE A
INNER JOIN
(SELECT USERID,TYPE,MIN(datetime) datetime FROM YOURTABLE GROUP BY USERID,TYPE )B
ON
A.USERID=B.USERID AND
A.TYPE=B.TYPE
WHERE A.DATETIME<>B.DATETIME

Sum the timegap if consecutive timestamps are same for 10 min

i am using the below query to find the null values of a column and get the starttime and endtime of the null values using the below query for some 30,000 rows
SELECT
yt1.[timestamp] AS StartTime,
MIN(yt2.[timestamp]) AS EndTime,
DATEDIFF(MINUTE, yt1.[timestamp], MIN(yt2.[timestamp])) AS DifferenceInMinutes
FROM
Sheet1$ yt1
LEFT JOIN Sheet1$ yt2 ON yt1.[timestamp] < yt2.[timestamp]
WHERE
yt1.TWSPD IS NULL
GROUP BY yt1.[timestamp]
The output is
Start time Endtime DifferenceInMinutes
2012-05-18 20:47:03.000 2012-05-18 20:57:04.000 10
2012-05-18 20:57:04.000 2012-05-18 21:07:04.000 10
2012-05-21 18:25:26.000 2012-05-21 18:35:26.000 10
2012-06-07 17:36:28.000 2012-06-07 17:46:28.000 10
2012-06-07 17:46:28.000 2012-06-07 17:56:28.000 10
2012-06-07 17:56:28.000 2012-06-07 18:06:28.000 10
And for example now i need the output as (removed some row to display better)
Start time Endtime DifferenceInMinutes
2012-05-18 20:47:03.000 2012-05-18 21:07:04.000 20
2012-05-21 18:25:26.000 2012-05-21 18:35:26.000 10
2012-06-07 17:36:28.000 2012-06-07 18:06:28.000 30
The timestamp is for every 10 min, if the null values for consecutive 10min timegap should be added and the starttime and endtime should be displayed as from the first null to last null of consecutive timestamp. Hope the question is clear. Please let me know if i am not clear. Please help
SELECT
yt1.[timestamp] AS StartTime,
MIN(yt2.[timestamp]) AS EndTime,
DATEDIFF(MINUTE, yt1.[timestamp], MIN(yt2.[timestamp])) AS DifferenceInMinutes
into #tmp1
FROM
Sheet1$ yt1
LEFT JOIN Sheet1$ yt2 ON yt1.[timestamp] < yt2.[timestamp]
WHERE
yt1.TWSPD IS NULL
GROUP BY yt1.[timestamp]
Select t1.*
into #tmp2
from #tmp1 t1
left join #tmp1 t2 on t1.Starttime=t2.Endtime
where t2.Endtime is null
Declare #rcn int
Select #rcn=1
While #rcn>0
begin
Update #tmp2 set #tmp2.Endtime=t.endTime,#tmp2.DifferenceInMinutes=#tmp2.DifferenceInMinutes+t.DifferenceInMinutes
from #tmp1 t
where t.Starttime=#tmp2.Endtime
select #rcn=##Rowcount
end
select * from #tmp2
Drop Table #tmp1
Drop Table #tmp2
If you want to query your original table to have grouped output - you can do this:
;with
CTE_start
as
(
select T.timestamp, row_number() over(order by T.timestamp) as RowNum
from temp1 as T
where
not exists
(
select *
from temp1 as TT
where TT.timestamp < T.timestamp and TT.timestamp >= dateadd(mi, -11, T.timestamp)
)
),
CTE_end
as
(
select T.timestamp, row_number() over(order by T.timestamp) as RowNum
from temp1 as T
where
not exists
(
select *
from temp1 as TT
where TT.timestamp > T.timestamp and TT.timestamp <= dateadd(mi, 11, T.timestamp)
)
)
select
s.timestamp as [Start time],
e.timestamp as [End time],
datediff(mi, s.timestamp, e.timestamp) as [DifferenceInMinutes]
from CTE_start as s
inner join CTE_end as e on e.RowNum = s.RowNum
SQL FIDDLE EXAMPLE
Another good one, but you have to copy your data into temporary (variable) table
declare #tmp table (timestamp datetime, RowNum int primary key)
insert into #tmp
select T.timestamp, row_number() over(order by T.timestamp) as RowNum
from temp1 as T
;with CTE
as
(
select T.timestamp, T.RowNum, 1 as GroupNum
from #tmp as T
where RowNum = 1
union all
select
T.timestamp, T.RowNum,
C.GroupNum + case when datediff(mi, C.timestamp, T.timestamp) >= 11 then 1 else 0 end
from #tmp as T
inner join CTE as C on C.RowNum + 1 = T.RowNum
)
select
min(C.timestamp) as [Start time],
max(C.timestamp) as [End time],
datediff(mi, min(C.timestamp), max(C.timestamp)) as [DifferenceInMinutes]
from CTE as C
group by C.GroupNum
SQL FIDDLE EXAMPLE