Pivot Time Between into a Matrix - sql-server

How can I pivot a start, end, start break, and end break times into a matrix that looks like the below? Ideally, I'd like to accomplish this in SQL Server.
Essentially, I'm trying to visualize a timesheet (start time, end time, start break, end break), and pivot across the day (broken down by 15-minute increments) as 1 or 0.
Start Time
End Time
Break Start
Break Start
0:00
0:15
0:30
0:45
1:00
1:15
1:30
1:45
2:00
2:15
0:15
2:00
0:45
1:15
0
1
1
0
0
1
1
1
0
0

Revised version that handles time sheet entries that don't start and end exactly on the 15min intervals.
-- Create test timesheet data
DROP TABLE IF EXISTS #Timesheets
GO
CREATE TABLE #Timesheets(ID INT, StartTime TIME, EndTime TIME, BreakStart TIME, Breakend TIME)
INSERT INTO #Timesheets(ID, StartTime,EndTime,BreakStart,Breakend)
VALUES
(1,'00:15','02:00','00:45','01:15'),
(2,'08:03','16:50','12:05','13:00'),
(3,'09:00','10:15','09:35','09:55')
-- Recursively create a set of time periods in 15mins intervals
DECLARE #StartTime TIME = '00:00'
DECLARE #EndTime TIME = DATEADD(mi,1425,#StartTime)
;WITH TimePeriods AS
(
SELECT #StartTime AS StartTime,DATEADD(mi,15,#StartTime) AS EndTime, CONVERT(VARCHAR(5),#StartTime,108) AS TimePeriod
UNION ALL
SELECT DATEADD(mi,15,StartTime),DATEADD(mi,30,StartTime), CONVERT(VARCHAR(5),DATEADD(mi,15,StartTime),108) FROM TimePeriods
WHERE StartTime < #EndTime
)
--select STRING_AGG( 'ISNULL('+QUOTENAME(Timeperiod)+',0) AS '+QUOTENAME(Timeperiod) ,',') from TimePeriods
-- Map shifts to the time periods
,Shifts AS
(
SELECT
tp.TimePeriod
,ts.*
FROM
TimePeriods tp
JOIN #Timesheets ts
ON
( -- Shifts
((ts.StartTime >= tp.StartTime AND ts.StartTime < tp.EndTime OR ts.EndTime > tp.StartTime AND ts.EndTime < tp.EndTime)
OR (tp.StartTime > ts.StartTime AND tp.StartTime < ts.EndTime)
OR (tp.EndTime > ts.StartTime AND tp.EndTime < ts.EndTime))
)
)
-- Map Breaks to the Time Periods
,Breaks AS
(
SELECT
tp.TimePeriod
,tb.*
FROM
TimePeriods tp
JOIN #Timesheets tb
ON
( -- Breaks
((tb.BreakStart >= tp.StartTime AND tb.BreakStart < tp.EndTime OR tb.BreakEnd > tp.StartTime AND tb.BreakEnd < tp.EndTime)
OR (tp.StartTime > tb.BreakStart AND tp.StartTime < tb.BreakEnd)
OR (tp.EndTime > tb.BreakStart AND tp.EndTime < tb.BreakEnd))
)
)
-- Join the Time Periods to both shifts and breaks, create an Active field which deducts breaks from shifts
,TimeSheetPerPeriod AS
(
select
s.*
,CASE WHEN ISNULL(s.id,0)-ISNULL(b.id,0)>0 THEN 1 ELSE 0 END AS IsActive
FROM TimePeriods tp
JOIN Shifts s ON tp.TimePeriod = s.TimePeriod
LEFT JOIN Breaks b ON tp.TimePeriod = b.TimePeriod
)
-- Pivot the data into the desired format
SELECT
id,StartTime,EndTime,BreakStart,Breakend
,ISNULL([00:00],0) AS [00:00],ISNULL([00:15],0) AS [00:15],ISNULL([00:30],0) AS [00:30],ISNULL([00:45],0) AS [00:45],ISNULL([01:00],0) AS [01:00],ISNULL([01:15],0) AS [01:15],ISNULL([01:30],0) AS [01:30],ISNULL([01:45],0) AS [01:45],ISNULL([02:00],0) AS [02:00],ISNULL([02:15],0) AS [02:15],ISNULL([02:30],0) AS [02:30],ISNULL([02:45],0) AS [02:45],ISNULL([03:00],0) AS [03:00],ISNULL([03:15],0) AS [03:15],ISNULL([03:30],0) AS [03:30],ISNULL([03:45],0) AS [03:45],ISNULL([04:00],0) AS [04:00],ISNULL([04:15],0) AS [04:15],ISNULL([04:30],0) AS [04:30],ISNULL([04:45],0) AS [04:45],ISNULL([05:00],0) AS [05:00],ISNULL([05:15],0) AS [05:15],ISNULL([05:30],0) AS [05:30],ISNULL([05:45],0) AS [05:45],ISNULL([06:00],0) AS [06:00],ISNULL([06:15],0) AS [06:15],ISNULL([06:30],0) AS [06:30],ISNULL([06:45],0) AS [06:45],ISNULL([07:00],0) AS [07:00],ISNULL([07:15],0) AS [07:15],ISNULL([07:30],0) AS [07:30],ISNULL([07:45],0) AS [07:45],ISNULL([08:00],0) AS [08:00],ISNULL([08:15],0) AS [08:15],ISNULL([08:30],0) AS [08:30],ISNULL([08:45],0) AS [08:45],ISNULL([09:00],0) AS [09:00],ISNULL([09:15],0) AS [09:15],ISNULL([09:30],0) AS [09:30],ISNULL([09:45],0) AS [09:45],ISNULL([10:00],0) AS [10:00],ISNULL([10:15],0) AS [10:15],ISNULL([10:30],0) AS [10:30],ISNULL([10:45],0) AS [10:45],ISNULL([11:00],0) AS [11:00],ISNULL([11:15],0) AS [11:15],ISNULL([11:30],0) AS [11:30],ISNULL([11:45],0) AS [11:45],ISNULL([12:00],0) AS [12:00],ISNULL([12:15],0) AS [12:15],ISNULL([12:30],0) AS [12:30],ISNULL([12:45],0) AS [12:45],ISNULL([13:00],0) AS [13:00],ISNULL([13:15],0) AS [13:15],ISNULL([13:30],0) AS [13:30],ISNULL([13:45],0) AS [13:45],ISNULL([14:00],0) AS [14:00],ISNULL([14:15],0) AS [14:15],ISNULL([14:30],0) AS [14:30],ISNULL([14:45],0) AS [14:45],ISNULL([15:00],0) AS [15:00],ISNULL([15:15],0) AS [15:15],ISNULL([15:30],0) AS [15:30],ISNULL([15:45],0) AS [15:45],ISNULL([16:00],0) AS [16:00],ISNULL([16:15],0) AS [16:15],ISNULL([16:30],0) AS [16:30],ISNULL([16:45],0) AS [16:45],ISNULL([17:00],0) AS [17:00],ISNULL([17:15],0) AS [17:15],ISNULL([17:30],0) AS [17:30],ISNULL([17:45],0) AS [17:45],ISNULL([18:00],0) AS [18:00],ISNULL([18:15],0) AS [18:15],ISNULL([18:30],0) AS [18:30],ISNULL([18:45],0) AS [18:45],ISNULL([19:00],0) AS [19:00],ISNULL([19:15],0) AS [19:15],ISNULL([19:30],0) AS [19:30],ISNULL([19:45],0) AS [19:45],ISNULL([20:00],0) AS [20:00],ISNULL([20:15],0) AS [20:15],ISNULL([20:30],0) AS [20:30],ISNULL([20:45],0) AS [20:45],ISNULL([21:00],0) AS [21:00],ISNULL([21:15],0) AS [21:15],ISNULL([21:30],0) AS [21:30],ISNULL([21:45],0) AS [21:45],ISNULL([22:00],0) AS [22:00],ISNULL([22:15],0) AS [22:15],ISNULL([22:30],0) AS [22:30],ISNULL([22:45],0) AS [22:45],ISNULL([23:00],0) AS [23:00],ISNULL([23:15],0) AS [23:15],ISNULL([23:30],0) AS [23:30],ISNULL([23:45],0) AS [23:45]
FROM
(
SELECT distinct *
FROM TimeSheetPerPeriod
) AS SourceTable
PIVOT
(
MAX(IsActive)
FOR TimePeriod IN ([00:00],[00:15],[00:30],[00:45],[01:00],[01:15],[01:30],[01:45],[02:00],[02:15],[02:30],[02:45],[03:00],[03:15],[03:30],[03:45],[04:00],[04:15],[04:30],[04:45],[05:00],[05:15],[05:30],[05:45],[06:00],[06:15],[06:30],[06:45],[07:00],[07:15],[07:30],[07:45],[08:00],[08:15],[08:30],[08:45],[09:00],[09:15],[09:30],[09:45],[10:00],[10:15],[10:30],[10:45],[11:00],[11:15],[11:30],[11:45],[12:00],[12:15],[12:30],[12:45],[13:00],[13:15],[13:30],[13:45],[14:00],[14:15],[14:30],[14:45],[15:00],[15:15],[15:30],[15:45],[16:00],[16:15],[16:30],[16:45],[17:00],[17:15],[17:30],[17:45],[18:00],[18:15],[18:30],[18:45],[19:00],[19:15],[19:30],[19:45],[20:00],[20:15],[20:30],[20:45],[21:00],[21:15],[21:30],[21:45],[22:00],[22:15],[22:30],[22:45],[23:00],[23:15],[23:30],[23:45])
) AS PivotTable

Related

SQL - Finding Gaps in Coverage

I am running this problem on SQL server
Here is my problem.
have something like this
Dataset A
FK_ID StartDate EndDate Type
1 10/1/2018 11/30/2018 M
1 12/1/2018 2/28/2019 N
1 3/1/2019 10/31/2019 M
I have a second data source I have no control over with data something like this:
Dataset B
FK_ID SpanStart SpanEnd Type
1 10/1/2018 10/15/2018 M
1 10/1/2018 10/25/2018 M
1 2/15/2019 4/30/2019 M
1 5/1/2019 10/31/2019 M
What I am trying to accomplish is to check to make sure every date within each TYPE M record in Dataset A has at least 1 record in Dataset B.
For example record 1 in Dataset A does NOT have coverage from 10/26/2018 through 11/30/2018. I really only care about when the coverage ends, in this case I want to return 10/26/2018 because it is the first date where the span has no coverage from Dataset B.
I've written a function that does this but it is pretty slow because it is cycling through each date within each M record and counting the number of records in Dataset B. It exits the loop when it finds the first one but I would really like to make this more efficient. I am sure I am not thinking about this properly so any suggestions anyone can offer would be helpful.
This is the section of code I'm currently running
else if #SpanType = 'M'
begin
set #CurrDate = #SpanStart
set #UncovDays = 0
while #CurrDate <= #SpanEnd
Begin
if (SELECT count(*)
FROM eligiblecoverage ec join eligibilityplan ep on ec.plandescription = ep.planname
WHERE ec.masterindividualid = #IndID
and ec.planbegindate <= #CurrDate and ec.planenddate >= #CurrDate
and ec.sourcecreateddate = #MaxDate
and ep.medicaidcoverage = 1) = 0
begin
SET #Result = concat('NON Starting ',format(#currdate, 'M/d/yyyy'))
BREAK
end
set #CurrDate = #CurrDate + 1
end
end
I am not married to having a function it just could not find a way to do this in queries that wasn't very very slow.
EDIT: Dataset B will never have any TYPEs except M so that is not a consideration
EDIT 2: The code offered by DonPablo does de-overlap the data but only in cases where there is an overlap at all. It reduces dataset B to:
FK_ID SpanStart SpanEnd Type
1 10/1/2018 10/25/2018 M
instead of
FK_ID SpanStart SpanEnd Type
1 10/1/2018 10/25/2018 M
1 2/15/2019 4/30/2019 M
1 5/1/2019 10/31/2019 M
I am still futzing around with it but it's a start.
I would approach this by focusing on B. My assumption is that any absent record would follow span_end in the table. So here is the idea:
Unpivot the dates in B (adding "1" to the end dates)
Add a flag if they are present with type "M".
Check to see if any not-present records are in the span for A.
Check the first and last dates as well.
So, this looks like:
with bdates as (
select v.dte,
(case when exists (select 1
from b b2
where v.dte between b2.spanstart and b2.spanend and
b2.type = 'M'
)
then 1 else 0
end) as in_b
from b cross apply
(values (spanstart), (dateadd(day, 1, spanend)
) v(dte)
where b.type = 'M' -- all we care about
group by v.dte -- no need for duplicates
)
select a.*,
(case when not exists (select 1
from b b2
where a.startdate between b2.spanstart and b2.spanend and
b2.type = 'M'
)
then 0
when not exists (select 1
from b b2
where a.enddate between b2.spanstart and b2.spanend and
b2.type = 'M'
)
when exists (select 1
from bdates bd
where bd.dte between a.startdate and a.enddate and
bd.in_b = 0
)
then 0
when exists (select 1
from b b2
where a.startdate between b2.spanstart and b2.spanend and
b2.type = 'M'
)
then 1
else 0
end)
from a;
What is this doing? Four validity checks:
Is the starttime valid?
Is the endtime valid?
Are any intermediate dates invalid?
Is there at least one valid record?
Start by framing the problem in smaller pieces, in a sequence of actions like I did in the comment.
See George Polya "How To Solve It" 1945
Then Google is your friend -- look at==> sql de-overlap date ranges into one record (over a million results)
UPDATED--I picked Merge overlapping dates in SQL Server
and updated it for our table and column names.
Also look at theory from 1983 Allen's Interval Algebra https://www.ics.uci.edu/~alspaugh/cls/shr/allen.html
Or from 2014 https://stewashton.wordpress.com/2014/03/11/sql-for-date-ranges-gaps-and-overlaps/
This is a primer on how to setup test data for this problem.
Finally determine what counts via Ranking the various pairs of A vs B --
bypass those totally Within, then work with earliest PartialOverlaps, lastly do the Precede/Follow items.
--from Merge overlapping dates in SQL Server
with SpanStarts as
(
select distinct FK_ID, SpanStart
from Coverage_B as t1
where not exists
(select * from Coverage_B as t2
where t2.FK_ID = t1.FK_ID
and t2.SpanStart < t1.SpanStart
and t2.SpanEnd >= t1.SpanStart)
),
SpanEnds as
(
select distinct FK_ID, SpanEnd
from Coverage_B as t1
where not exists
(select * from Coverage_B as t2
where t2.FK_ID = t1.FK_ID
and t2.SpanEnd > t1.SpanEnd
and t2.SpanStart <= t1.SpanEnd)
),
DeOverlapped_B as
(
Select FK_ID, SpanStart,
(select min(SpanEnd) from SpanEnds as e
where e.FK_ID = s.FK_ID
and SpanEnd >= SpanStart) as SpanEnd
from SpanStarts as s
)
Select * from DeOverlapped_B
Now we have something to feed into the next steps, and we can use the above as a CTE
======================================
with SpanStarts as
(
select distinct FK_ID, SpanStart
from Coverage_B as t1
where not exists
(select * from Coverage_B as t2
where t2.FK_ID = t1.FK_ID
and t2.SpanStart < t1.SpanStart
and t2.SpanEnd >= t1.SpanStart)
),
SpanEnds as
(
select distinct FK_ID, SpanEnd
from Coverage_B as t1
where not exists
(select * from Coverage_B as t2
where t2.FK_ID = t1.FK_ID
and t2.SpanEnd > t1.SpanEnd
and t2.SpanStart <= t1.SpanEnd)
),
DeOverlapped_B as
(
Select FK_ID, SpanStart,
(select min(SpanEnd) from SpanEnds as e
where e.FK_ID = s.FK_ID
and SpanEnd >= SpanStart) as SpanEnd
from SpanStarts as s
),
-- find A row's coverage
ACoverage as (
Select
a.*, b.SpanEnd, b.SpanStart,
Case
When SpanStart <= StartDate And StartDate <= SpanEnd
And SpanStart <= EndDate And EndDate <= SpanEnd
Then '1within' -- starts, equals, during, finishes
When EndDate < SpanStart
Or SpanEnd < StartDate
Then '3beforeAfter' -- preceeds, meets, preceeded, met
Else '2overlap' -- one or two ends hang over spanStart/End
End as relation
From Coverage_A a
Left Join DeOverlapped_B b
On a.FK_ID = b.FK_ID
Where a.Type = 'M'
)
Select
*
,Case
When relation1 = '2' And StartDate < SpanStart Then StartDate
When relation1 = '2' Then DateAdd(d, 1, SpanEnd)
When relation1 = '3' Then StartDate
End as UnCoveredBeginning
From (
Select
*
,SUBSTRING(relation,1,1) as relation1
,ROW_NUMBER() Over (Partition by A_ID Order by relation, SpanStart) as Rownum
from ACoverage
) aRNO
Where Rownum = 1
And relation1 <> '1'

Second level lookup with SQL statement

How do I write a SQL statement that does a second level lookup only if first is not matched. For example:
In the below query, if my SEDOLCode condition does not return a record, proceed to lookup with condition 2 with RICCode.
select
*, GETDATE()
from
Securities sec
where
sec.SEDOLCode = 'ABCDEF'
or sec.RICCode = '002815.SZ'
This query is returning two different records - for example:
1234 ABCDEF DUMY906.X
5675 EFTFS 002815.SZ
I am taking data from a file to update the Pricetable as below. I want to use SedolCode as primary lookup.
IF ##ROWCOUNT = 0
INSERT INTO dbo.Price (sec.SecurityID, ClosingPrice, UpdatedDate, UpdatedByUser, Priced)
SELECT
..., GETDATE()
FROM
Securities sec
WHERE
sec.SEDOLCode = #SedolCode
OR sec.RICCode = #RicCode
Try this the logic is basically if the sedolcode is found then it will only meet the first condition. Otherwise the count of that sedolcolde will be 0 and it will look at riccode.
select
*, GETDATE()
from
Securities sec
where
sec.sedolcode = 'ABCDEF'
OR ((SELECT COUNT(1) FROM securites WHERE sedolcode ='ABCDEF') = 0 AND sec.riccode = '002815.SZ')
Ah - reminds me of my FTSE days........
Match Sedol and Not Ric
or
Ric and Not Sedol and use myOrdering & TOP to get the first.
INSERT INTO dbo.Price
(
sec.SecurityID
, ClosingPrice
, UpdatedDate
, UpdatedByUser
, Priced
)
SELECT TOP 1 [specifiy fields to insert]
FROM
(
select 1 as myOrdering ...
, GETDATE()
from Securities sec
WHERE
(sec.RICCode = #RicCode AND sec.SEDOLCode != #SedolCode)
UNION
select 2 as myOrdering ...
, GETDATE()
from Securities sec
WHERE
(sec.RICCode = #RicCode AND sec.SEDOLCode != #SedolCode)
)SUB_Q ORDER BY myOrdering

T-SQL: AGGREGATE AND/OR CONDITIONAL SUBQUERY ON WHERE CLAUSE

Thanks in advance for your help on this. Here's the the scenario. I have two tables: Lots (stores lot info for food items) and Trans (stores inventory transactions on lot items).
I am trying to write a query that lists all the transactions that are older than 90 days based 3 conditions on the where clause somehow:
CurrentQty (on Lots table) > 0
If TransactionType = Shipment AND TransactionDate > 90 days (from current date) OR..
IF TransactionType = Receipt AND TransactionDate > 90 days (from current date)
Notes: for each lot, there could be many different transactions of the same type, as shown in the attached picture. There could be many shipments or receipts. I need to be able to select the MAX(TransactionDate) for a particular transaction type and check to see if it's over 90 days then show the record.
There will always be at least one of these two transaction types on the transaction table for every lot, either Shipment or Receipt. If there is no shipment transaction type on a particular lot, then I want to use the Max(TransactionDate) > 90 condition for the "receipt" transaction type.
I need to be able to evaluate all these conditions for each lot and it's particular transactions.
Below is the query I started to write, but then got stuck on how to structure the rest.
SELECT
LOTS.LOTNUMBER, TRANS.ITEMNUMBER, TRANS.DESCRIPTION,
TRANS.TRANSACTIONTYPE, TRANS.TRANSACTIONDATE, TRANS.WAREHOUSE,
TRANS.QUANTITY
FROM
LOTS
INNER JOIN
TRANS ON LOTS.LOTNUMBER = TRANS.LOTNUMBER
WHERE
LOTS.CURRENTQUANTITY > 0
SELECT
LOTS.LOTNUMBER, TRANS.ITEMNUMBER, TRANS.DESCRIPTION,
TRANS.TRANSACTIONTYPE, TRANS.TRANSACTIONDATE, TRANS.WAREHOUSE,
TRANS.QUANTITY
FROM
LOTS
INNER JOIN
(Select LotNumber, Max(TransactionDate) MaxTransDate From TRANS Group By LotNumber) Trans ON LOTS.LOTNUMBER = TRANS.LOTNUMBER
WHERE
LOTS.CURRENTQUANTITY > 0 And
DATEDIFF(d, TRANS.TRANSACTIONDATE, getdate()) > 90 And
(TRANS.TRANSACTIONTYPE = 'Reciept' Or
TRANS.TRANSACTIONTYPE = 'Shipment')
DECLARE #lots TABLE (
LotNumber NVARCHAR(10)
,ItemNum NVARCHAR(12)
,CurrentQty INT
)
DECLARE #trans TABLE (
TransNum INT
,ItemNum NVARCHAR(12)
,Description NVARCHAR(30)
,TransType NVARCHAR(10)
,TransDate DATE
,Warehouse NVARCHAR(5)
,Quantity INT
)
INSERT INTO #lots VALUES
('ABC','10-MAND-5879',925)
INSERT INTO #trans VALUES
(398741,'10-MAND-5879','10 Lb Mandarin Bag','Receipt','2016-01-01','IXCST',100)
,(973541,'10-MAND-5879','10 Lb Mandarin Bag','Shipment','2016-02-04','WTGS',365)
,(972547,'10-MAND-5879','10 Lb Mandarin Bag','Receipt','2016-02-29','GKWK',250)
,(632403,'10-MAND-5879','10 Lb Mandarin Bag','Shipment','2016-03-01','GWSJ',150)
,(692147,'10-MAND-5879','10 Lb Mandarin Bag','Shipment','2016-03-17','ABTK',25)
;WITH MaxShip
AS (
SELECT ItemNum, TransNum,
ROW_NUMBER () OVER (PARTITION BY ItemNum ORDER BY TransDate DESC) AS TransOrder
FROM #trans
WHERE TransType = 'Shipment'
),
MaxRcpt
AS (
SELECT ItemNum, TransNum,
ROW_NUMBER () OVER (PARTITION BY ItemNum ORDER BY TransDate DESC) AS TransOrder
FROM #trans
WHERE transtype = 'Receipt'
)
SELECT *
FROM #trans t
LEFT JOIN #lots l
ON t.ItemNum = l.ItemNum
JOIN MaxShip ms
ON ms.TransNum = t.TransNum
WHERE TransOrder = 1 AND CurrentQty > 0 AND DATEADD(dd,90,TransDate) < GETDATE()
UNION
SELECT *
FROM #trans t
LEFT JOIN #lots l
ON t.ItemNum = l.ItemNum
JOIN MaxRcpt mr
ON mr.TransNum = t.TransNum
WHERE TransOrder = 1 AND CurrentQty > 0 AND DATEADD(dd,90,TransDate) < GETDATE()

Finding Events During a Timeframe

Trying to figure out a query that could work for multiple situations. In a nutshell the data can be in one of twosituations. Lets say I'm looking for the record_id for events happened during a given time frame: 6/26/2012 10:00AM and 6/27/2012 11:00AM The records can look like this in the database:
Record Event Time
1 Start 6/26/2012 10:05AM
1 End 6/26/2012 10:45AM
2 Start 6/26/2012 09:55AM
2 End 6/26/2012 11:05AM
Getting record 1 is easy, just using the between function, but I'm stumbling trying to figure out a query to return both records 1 and 2.
Suggestions?
You can simply widen the range, but this may bring back different records than you intend.
SELECT RECORD, EVENT, TIME
FROM SO_RecordEvent AS R
WHERE TIme BETWEEN '6/26/2012 9:55AM' AND '6/26/2012 11:06AM'
This will return all records for anything that has either a start or end time within the range, including the associated records falling outside of the range (In other words records that only have one time in the range - but began before or ended after - it will still show the start or end time outside of it) and allow you you shorten your range.
;WITH X AS
(
SELECT RECORD,EVENT,TIME FROM SO_RecordEvent
)
SELECT R.RECORD,R.EVENT,R.TIME
FROM SO_RecordEvent AS R
INNER JOIN X ON R.Record = X.Record
WHERE X.TIme BETWEEN '6/26/2012 10:05AM' AND '6/26/2012 11:05AM'
GROUP BY R.RECORD,R.EVENT,R.TIME
But I think you may really want something like this, which truly gives you everything that was started during that time, even if it started AND ended outside of the range, as is your record 2 example.
EDIT
Changed logic - instead of addressing situations decided to think about it this way - anything that started within start and end, anything that ended within start and end, and anything that started before and ended after. I think this covers anything that runs during this time (starts before ends within, starts and ends within, starts within and ends after, and starts before and ends after)
SELECT X.RECORD,X.TIME AS STARTTIME,Y.TIME AS ENDTIME
FROM SO_RecordEvent AS X
INNER JOIN SO_RecordEvent Y ON Y.Record = X.Record AND Y.EVENT = 'END'
WHERE X.EVENT = 'START'
AND
((X.TIME >= '6/26/2012 10:00AM' AND X.TIME <= '6/26/2012 11:00AM')
OR (Y.TIME >= '6/26/2012 10:00AM' AND Y.TIME <= '6/26/2012 11:00AM')
OR (X.TIME <= '6/26/2012 10:00AM' AND Y.TIME >= '6/26/2012 11:00AM'))
Variables to play with:
DECLARE #START datetime, #END datetime
SET #START = '6/26/2012 10:00AM'
SET #END = '6/26/2012 11:00AM'
SELECT X.RECORD,X.TIME AS STARTTIME,Y.TIME AS ENDTIME
FROM SO_RecordEvent AS X
INNER JOIN SO_RecordEvent Y ON Y.Record = X.Record AND Y.EVENT = 'END'
WHERE X.EVENT = 'START'
AND
((X.TIME >= #START AND X.TIME <= #END)
OR (Y.TIME >= #START AND Y.TIME <= #END)
OR (X.TIME <= #START AND Y.TIME >= #END))
I'd probably join the table to itself - something like this:
SELECT *
FROM
Table leftside
JOIN Table rightside
ON leftside.Record = rightside.Record
WHERE
leftside.Event = 'Start'
and rightside.Event = 'End'
and [whatever time you want] >= leftside.Time
and [whatever time you want] <= rightside.Time
EDIT: Example to demonstrate
create table thingy (
record int,
event varchar(10),
time datetime )
insert thingy (record,event,time) values (1,'Start','6/26/2012 10:05AM')
insert thingy (record,event,time) values (1,'End','6/26/2012 10:45AM ')
insert thingy (record,event,time) values (2,'Start','6/26/2012 09:55AM')
insert thingy (record,event,time) values (2,'End','6/26/2012 11:05AM')
DECLARE #mytime datetime
SET #mytime = '6/26/2012 9:58AM'
SELECT *
FROM
thingy leftside
JOIN thingy rightside
ON leftside.Record = rightside.Record
WHERE
leftside.Event = 'Start'
and rightside.Event = 'End'
and #mytime >= leftside.Time
and #mytime <= rightside.Time
SET #mytime = '6/26/2012 10:10AM'
SELECT *
FROM
thingy leftside
JOIN thingy rightside
ON leftside.Record = rightside.Record
WHERE
leftside.Event = 'Start'
and rightside.Event = 'End'
and #mytime >= leftside.Time
and #mytime <= rightside.Time
Seems to give what I would expect - 1 row for the first query, 2 for the second.
Get record #s of all events that fall within the given range:
SELECT Record
FROM atable
WHERE Time BETWEEN #StartTime AND #EndTime
Now just get all rows whose record #s match those in the above query's result set, i.e. like this:
SELECT *
FROM atable
WHERE Record IN (
SELECT Record
FROM atable
WHERE Time BETWEEN #StartTime AND #EndTime
)
The simplest way to do this would be using the following:
WITH recs AS (
SELECT Record, MIN(tme) AS startTime, MAX(tme) AS endTime
FROM records
GROUP BY Record
)
SELECT * FROM
records
WHERE startTime >= #eventsAfter
AND endTime <= #eventsBefore
(this is how I interpreted your question, at least)

Query runs quickly in Oracle SQL Developer, but slowly in SSRS 2008 R2

It's that simple: a query that runs in just a few seconds in SQL Developer connecting to Oracle 11g takes 15-25 minutes in SSRS 2008 R2. I haven't tried other versions of SSRS. So far I'm doing all the report execution from VS 2008.
I'm using the OLE DB Provider "OraOLEDB.Oracle.1" which in the past has seemed to give me better results than using the Oracle provider.
Here's what I've been able to determine so far:
• The delay is during the DataSet execution phase and has nothing to do with the result set or rendering time. (Proving by selecting the same rowset directly from a table I inserted it to.)
• SSRS itself is not hung up. It is truly waiting for Oracle which is where the delay is (proven by terminating the DB session from the Oracle side, which resulted in a prompt error in SSRS about the session being killed).
• I have tried direct queries with parameters in the form :Parameter. Very early versions of my query that were more simple worked okay for direct querying, but it seemed like past a certain complexity, the query would start taking forever from SSRS.
• I then switched to executing an SP that inserts the query results to a table or global temp table. This helped for a little while, getting me farther than direct querying, but again, it almost seems like increased query complexity or length eventually broke this method, too. Note: running a table-populating SP works because with option "use single transaction" checked in the DataSource options, DataSets are then run in the order of their appearance in the rdl file. DataSets that return no Fields are still run, as long as all their parameters are satisfied.
• I just tried a table-returning function and this still made no improvement, even though direct calls with literal parameters in SQL Developer return in 1-5 seconds.
• The database in question does not have statistics. It is part of a product created by a vendor and we have not had the time or management buy-in to create/update statistics. I played with the DYNAMIC_SAMPLING hint to calculate statistics on the fly and got a better execution plan: without statistics the cost-based optimizer had been poorly using a LOOP join instead of a HASH join, causing similar many-minute execution times. Thus I put in query hints to force join order and also to cause it to use the strategic hash join, bringing the execution time back down to just seconds. I did not go back and try direct querying in SSRS using these execution hints.
• I got some help from our Oracle DBA who set up a trace (or whatever the Oracle equivalent is) and he was able to see things being run, but he hasn't found anything useful so far. Unfortunately his time is limited and we haven't been able to really dig in to find out what's being executed server-side. I don't have the experience to do this quickly or the time to study up on how to do this myself. Suggestions on what to do to determine what's going on would be appreciated.
My only hypotheses are:
• The query is somehow getting a bad execution plan. E.g., improperly using a LOOP join instead of a HASH join when there are tens of thousands of "left" or outer-loop rows rather than just a few hundred.
• SSRS could be submitting the parameters as nvarchar(4000) or something instead of something reasonable, and as Oracle SP & function parameters don't have length specifications but get their execution lengths from the query call, then some process such as parameter sniffing is messing up the execution plan as in the previous point.
• The query is somehow being rewritten by SSRS/the provider. I AM using a multi-valued parameter, but not as is: the parameter is being submitted as expression Join(Parameters!MultiValuedParameter.Value, ",") so it shouldn't need any rewriting. Just a simple binding and submitting. I don't see how this could be true in the SP and the function calls, but gosh, what else could it be?
I realize it is a very complicated and lengthy query, but it does exactly what I need. It runs in 1-5 seconds depending on how much data is asked for. Some of the reasons for the complexity are:
Properly handling the comma-separated cost center list parameter
Allowing the weekly breakdown to be optional and if included, ensuring all the weeks in a month are shown even if there is no data for them.
Showing "No Invoices" when appropriate.
Allowing a variable number of summary months.
Having an optional YTD total.
Including previous/historical comparison data means I can't simply use this month's vendors, I have to show all the vendors that will be in any historical column.
Anyway, so here's the query, SP version (though I don't think it will be much help).
create or replace
PROCEDURE VendorInvoiceSummary (
FromDate IN date,
ToDate IN date,
CostCenterList IN varchar2,
IncludeWeekly IN varchar2,
ComparisonMonths IN number,
IncludeYTD IN varchar2
)
AS
BEGIN
INSERT INTO InvoiceSummary (Mo, CostCenter, Vendor, VendorName, Section, TimeUnit, Amt)
SELECT
Mo,
CostCenter,
Vendor,
VendorName,
Section,
TimeUnit,
Amt
FROM (
WITH CostCenters AS (
SELECT Substr(REGEXP_SUBSTR(CostCenterList, '[^,]+', 1, LEVEL) || ' ', 1, 15) CostCenter
FROM DUAL
CONNECT BY LEVEL <= Length(CostCenterList) - Length(Replace(CostCenterList, ',', '')) + 1
), Invoices AS (
SELECT /*+ORDERED USE_HASH(D)*/
TRUNC(I.Invoice_Dte, 'YYYY') Yr,
TRUNC(I.Invoice_Dte, 'MM') Mo,
D.Dis_Acct_Unit CostCenter,
I.Vendor,
V.Vendor_VName,
CASE
WHEN I.Invoice_Dte >= FromDate AND I.Invoice_Dte < ToDate
THEN (TRUNC(I.Invoice_Dte, 'W') - TRUNC(I.Invoice_Dte, 'MM')) / 7 + 1
ELSE 0
END WkNum,
Sum(D.To_Base_Amt) To_Base_Amt
FROM
ICCompany C
INNER JOIN APInvoice I
ON C.Company = I.Company
INNER JOIN APDistrib D
ON C.Company = D.Company
AND I.Invoice = D.Invoice
AND I.Vendor = D.Vendor
AND I.Suffix = D.Suffix
INNER JOIN CostCenters CC
ON D.Dis_Acct_Unit = CC.CostCenter
INNER JOIN APVenMast V ON I.Vendor = V.Vendor
WHERE
D.Cancel_Seq = 0
AND I.Cancel_Seq = 0
AND I.Invoice_Dte >= Least(ADD_MONTHS(FromDate, -ComparisonMonths), TRUNC(FromDate, 'YYYY'))
AND I.Invoice_Dte < ToDate
AND V.Vendor_Group = '1 ' -- index help
GROUP BY
TRUNC(I.Invoice_Dte, 'YYYY'),
TRUNC(I.Invoice_Dte, 'MM'),
D.Dis_Acct_Unit,
I.Vendor,
V.Vendor_VName,
CASE
WHEN I.Invoice_Dte >= FromDate AND I.Invoice_Dte < ToDate
THEN (TRUNC(I.Invoice_Dte, 'W') - TRUNC(I.Invoice_Dte, 'MM')) / 7 + 1
ELSE 0
END
), Months AS (
SELECT ADD_MONTHS(Least(ADD_MONTHS(FromDate, -ComparisonMonths), TRUNC(FromDate, 'YYYY')), LEVEL - 1) Mo
FROM DUAL
CONNECT BY LEVEL <= MONTHS_BETWEEN(ToDate, Least(ADD_MONTHS(FromDate, -ComparisonMonths), TRUNC(FromDate, 'YYYY')))
), Sections AS (
SELECT 1 Section, 1 StartUnit, 5 EndUnit FROM DUAL
UNION ALL SELECT 2, 0, ComparisonMonths FROM DUAL
UNION ALL SELECT 3, 1, 1 FROM DUAL WHERE IncludeYTD = 'Y'
), Vals AS (
SELECT LEVEL - 1 TimeUnit
FROM DUAL
CONNECT BY LEVEL <= (SELECT Max(EndUnit) FROM Sections) + 1
), TimeUnits AS (
SELECT S.Section, V.TimeUnit
FROM
Sections S
INNER JOIN Vals V
ON V.TimeUnit BETWEEN S.StartUnit AND S.EndUnit
), Names AS (
SELECT DISTINCT
M.Mo,
Coalesce(I.Vendor, '0') Vendor,
Coalesce(I.Vendor_VName, 'No Paid Invoices') Vendor_VName,
Coalesce(I.CostCenter, ' ') CostCenter
FROM
Months M
LEFT JOIN Invoices I
ON Least(ADD_MONTHS(M.Mo, -ComparisonMonths), TRUNC(M.Mo, 'YYYY')) < I.Mo
AND M.Mo >= I.Mo
WHERE
M.Mo >= FromDate
AND M.Mo < ToDate
)
SELECT
N.Mo,
N.CostCenter,
N.Vendor,
N.Vendor_VName VendorName,
T.Section,
T.TimeUnit,
Sum(I.To_Base_Amt) Amt
FROM
Names N
CROSS JOIN TimeUnits T
LEFT JOIN Invoices I
ON N.CostCenter = I.CostCenter
AND N.Vendor = I.Vendor
AND (
(
T.Section = 1 -- Weeks for current month
AND N.Mo = I.Mo
AND T.TimeUnit = I.WkNum
) OR (
T.Section = 2 -- Summary months
AND ADD_MONTHS(N.Mo, -T.TimeUnit) = I.Mo
) OR (
T.Section = 3 -- YTD
AND I.Mo BETWEEN TRUNC(N.Mo, 'YYYY') AND N.Mo
)
)
WHERE
N.Mo >= FromDate
AND N.Mo < ToDate
AND NOT ( -- Only 4 weeks when a month is less than 28 days long
T.Section = 2
AND T.TimeUnit = 5
AND TRUNC(N.Mo + 28, 'MM') <> N.Mo
AND I.CostCenter IS NULL
) AND (
T.Section <> 1
OR IncludeWeekly = 'Y'
)
GROUP BY
N.Mo,
N.CostCenter,
N.Vendor,
N.Vendor_VName,
T.Section,
T.TimeUnit
) X;
COMMIT;
END;
UPDATE
Even after learning all about Oracle execution plans and hints (to translate my SQL Server knowledge), I still could not get the query to run quickly in SSRS until I made it run in two steps, first to put the real table results into a GLOBAL TEMPORARY TABLE and then second to extract the data from that. DYNAMIC_SAMPLING gave me a good execution plan, which I then copied using join and access hints. Here's the final SP (it couldn't be a function because in Oracle you can't do DML in a function when that function is called inside of a SELECT statement):
Sometimes I swear it was ignoring my join hints such as swap_join_inputs and no_swap_join_inputs but from my reading apparently Oracle only ignores hints when they can't actually be used or you're doing something wrong. Fortunately, the tables swapped appropriately (as in the case of USE_NL(CC) it reliably puts the CC table as the swapped, left input, even though it's joined last).
CREATE OR REPLACE
PROCEDURE VendorInvoicesSummary (
FromDate IN date,
ToDate IN date,
CostCenterList IN varchar2,
IncludeWeekly IN varchar2,
ComparisonMonths IN number,
IncludeYTD IN varchar2
)
AS
BEGIN
INSERT INTO InvoiceTemp (Yr, Mo, CostCenter, Vendor, WkNum, Amt) -- A global temporary table
SELECT /*+LEADING(C I D CC) USE_HASH(I D) USE_NL(CC)*/
TRUNC(I.Invoice_Dte, 'YYYY') Yr,
TRUNC(I.Invoice_Dte, 'MM') Mo,
D.Dis_Acct_Unit CostCenter,
I.Vendor,
CASE
WHEN I.Invoice_Dte >= FromDate AND I.Invoice_Dte < ToDate
THEN (TRUNC(I.Invoice_Dte, 'W') - TRUNC(I.Invoice_Dte, 'MM')) / 7 + 1
ELSE 0
END WkNum,
Sum(D.To_Base_Amt) To_Base_Amt
FROM
ICCompany C
INNER JOIN APInvoice I
ON C.Company = I.Company
INNER JOIN APDistrib D
ON C.Company = D.Company
AND I.Invoice = D.Invoice
AND I.Vendor = D.Vendor
AND I.Suffix = D.Suffix
INNER JOIN (
SELECT Substr(REGEXP_SUBSTR(CostCenterList, '[^,]+', 1, LEVEL) || ' ', 1, 15) CostCenter
FROM DUAL
CONNECT BY LEVEL <= Length(CostCenterList) - Length(Replace(CostCenterList, ',', '')) + 1
) CC ON D.Dis_Acct_Unit = CC.CostCenter
WHERE
D.Cancel_Seq = 0
AND I.Cancel_Seq = 0
AND I.Invoice_Dte >= Least(ADD_MONTHS(FromDate, -ComparisonMonths), TRUNC(FromDate, 'YYYY'))
AND I.Invoice_Dte < ToDate
GROUP BY
TRUNC(I.Invoice_Dte, 'YYYY'),
TRUNC(I.Invoice_Dte, 'MM'),
D.Dis_Acct_Unit,
I.Vendor,
CASE
WHEN I.Invoice_Dte >= FromDate AND I.Invoice_Dte < ToDate
THEN (TRUNC(I.Invoice_Dte, 'W') - TRUNC(I.Invoice_Dte, 'MM')) / 7 + 1
ELSE 0
END;
INSERT INTO InvoiceSummary (Mo, CostCenter, Vendor, VendorName, Section, TimeUnit, Amt)
SELECT
Mo,
CostCenter,
Vendor,
VendorName,
Section,
TimeUnit,
Amt
FROM (
WITH Months AS (
SELECT ADD_MONTHS(Least(ADD_MONTHS(FromDate, -ComparisonMonths), TRUNC(FromDate, 'YYYY')), LEVEL - 1) Mo
FROM DUAL
CONNECT BY LEVEL <= MONTHS_BETWEEN(ToDate, Least(ADD_MONTHS(FromDate, -ComparisonMonths), TRUNC(FromDate, 'YYYY')))
), Sections AS (
SELECT 1 Section, 1 StartUnit, 5 EndUnit FROM DUAL
UNION ALL SELECT 2, 0, ComparisonMonths FROM DUAL
UNION ALL SELECT 3, 1, 1 FROM DUAL WHERE IncludeYTD = 'Y'
), Vals AS (
SELECT LEVEL - 1 TimeUnit
FROM DUAL
CONNECT BY LEVEL <= (SELECT Max(EndUnit) FROM Sections) + 1
), TimeUnits AS (
SELECT S.Section, V.TimeUnit
FROM
Sections S
INNER JOIN Vals V
ON V.TimeUnit BETWEEN S.StartUnit AND S.EndUnit
), Names AS (
SELECT DISTINCT
M.Mo,
Coalesce(I.Vendor, '0') Vendor,
Coalesce(I.CostCenter, ' ') CostCenter
FROM
Months M
LEFT JOIN InvoiceTemp I
ON Least(ADD_MONTHS(M.Mo, -ComparisonMonths), TRUNC(M.Mo, 'YYYY')) <= I.Mo
AND I.Mo <= M.Mo
WHERE
M.Mo >= FromDate
AND M.Mo < ToDate
)
SELECT
N.Mo,
N.CostCenter,
N.Vendor,
Coalesce(V.Vendor_VName, 'No Paid Invoices') VendorName,
T.Section,
T.TimeUnit,
Sum(I.Amt) Amt
FROM
Names N
INNER JOIN APVenMast V ON N.Vendor = V.Vendor
CROSS JOIN TimeUnits T
LEFT JOIN InvoiceTemp I
ON N.CostCenter = I.CostCenter
AND N.Vendor = I.Vendor
AND (
(
T.Section = 1 -- Weeks for current month
AND N.Mo = I.Mo
AND T.TimeUnit = I.WkNum
) OR (
T.Section = 2 -- Summary months
AND ADD_MONTHS(N.Mo, -T.TimeUnit) = I.Mo
) OR (
T.Section = 3 -- YTD
AND I.Mo BETWEEN TRUNC(N.Mo, 'YYYY') AND N.Mo
)
)
WHERE
N.Mo >= FromDate
AND N.Mo < ToDate
AND V.Vendor_Group = '1 '
AND NOT ( -- Only 4 weeks when a month is less than 28 days long
T.Section = 2
AND T.TimeUnit = 5
AND TRUNC(N.Mo + 28, 'MM') <> N.Mo
AND I.CostCenter IS NULL
) AND (
T.Section <> 1
OR IncludeWeekly = 'Y'
)
GROUP BY
N.Mo,
N.CostCenter,
N.Vendor,
V.Vendor_VName,
T.Section,
T.TimeUnit
) X;
COMMIT;
END;
This has been a long, painful ride, but if there's one thing I've learned it's that working in a database without properly updated statistics (which I'm going to look into getting our DBA to add even though the vendor doesn't care about them) can be a real disaster for someone who wants to get things done in a reasonable amount of time.
Posting the query may help.
Your DBA should be able to identify the session in a view called v$session, and the columns EVENT and WAIT_CLASS should give an indication of what is happening on the Oracle end.
He would also be able to identify the SQL (SQL_ID from v$session) and use that in a SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(sql_id)) to determine the plan.
If it is a development/test instance, see if he will grant you permissions to do that yourself if he (or she) is busy.
I know this is old but we had a similar problem and had to set the nsl_sort to binary instead of binary_ci. People could try setting the session to binary: alter session set nls_sort=binary

Resources