Make a CTE from result of others CTE - sql

I have several joined CTE. Something like:
;With CT1 AS(SELECT ..)
, CT2 AS(select)
SELECT *.T1,*T2 FROM CT1 T1 INNER JOIN CT2 T2 WHERE (some Condition ) GROUP BY (F1,F2, etc)
Now I need to join the result of this query to another CTE. What’s the best way? Can I make a CTE with the result of this Query? Any help would be greatly appreciated.

You can keep creating new CTEs based on previously defined ones. They may joined or otherwise combined, subject to the rules for CTEs.
; with
ArabicRomanConversions as (
select *
from ( values
( 0, '', '', '', '' ), ( 1, 'I', 'X', 'C', 'M' ), ( 2, 'II', 'XX', 'CC', 'MM' ), ( 3, 'III', 'XXX', 'CCC', 'MMM' ), ( 4, 'IV', 'XL', 'CD', '?' ),
( 5, 'V', 'L', 'D', '?' ), ( 6, 'VI', 'LX', 'DC', '?' ), ( 7, 'VII', 'LXX', 'DCC', '?' ), ( 8, 'VIII', 'LXXX', 'DCCC', '?' ), ( 9, 'IX', 'XC', 'CM', '?' )
) as Placeholder ( Arabic, Ones, Tens, Hundreds, Thousands )
),
Numbers as (
select 1 as Number
union all
select Number + 1
from Numbers
where Number < 3999 ),
ArabicAndRoman as (
select Number as Arabic,
( select Thousands from ArabicRomanConversions where Arabic = Number / 1000 ) +
( select Hundreds from ArabicRomanConversions where Arabic = Number / 100 % 10 ) +
( select Tens from ArabicRomanConversions where Arabic = Number / 10 % 10 ) +
( select Ones from ArabicRomanConversions where Arabic = Number % 10 ) as Roman
from Numbers ),
Squares as (
select L.Arabic, L.Roman, R.Arabic as Square, R.Roman as RomanSquare
from ArabicAndRoman as L inner join
ArabicAndRoman as R on R.Arabic = L.Arabic * L.Arabic
where L.Arabic < 16 ),
Cubes as (
select S.Arabic, S.Roman, S.Square, S.RomanSquare, A.Arabic as Cube, A.Roman as RomanCube
from Squares as S inner join
ArabicAndRoman as A on A.Arabic = S.Square * S.Arabic )
select *
from Cubes
order by Arabic
option ( MaxRecursion 3998 )

This is a format I have used a few times where a temp table is used to buffer one complex CTE which is output and then used again from temp with a second CTE.
It is useful if you need 2 result sets or if the complete CTE as one massive statement causes speed issues (breaking it up can be a huge performance improvement in some cases)
-- I do this "DROP" because in some cases where query is executed over and
-- over sometimes the object is not cleared before next transaction.
BEGIN TRY DROP TABLE #T_A END TRY BEGIN CATCH END CATCH;
WITH A AS (
SELECT 'A' AS Name, 1 as Value
UNION ALL SELECT 'B', 2
)
SELECT *
INTO #T_A
FROM A;
SELECT *
FROM #T_A ; -- Generate First Output Table
WITH B AS (
SELECT 'A' AS Name, 234 as Other
UNION ALL SELECT 'B', 456
)
-- Generate second result set from Temp table.
SELECT B.*, A. Value
FROM B JOIN #T_A AS A ON A.Name=B.Name
This produces a 2 table result set. Which is also handy in .NET filling a DataSet.

Related

Query rows where first_name contains at least 2 vowels, and the number of occurences of each vowel is equal

I have the following problem: Show all rows in table where column first_name contains at least 2 vowels (a, e, i, o, u), and the number of occurences of each vowel is the same.
Valid example: Alexander, "e" appears 2 times, "a" appears 2 times. That is coreect.
Invalid example: Jonathan, it has 2 vowels (a, o), but "o" appears once, and "a" appears twice, the number of occurences is not equal.
I've solved this problem by calculating each vowel, and then verify every case (A E, A I, A O etc. Shortly, each combination of 2, 3, 4, 5). With that solution, I have a very long WHERE. Is there any shorter way and more elegant and simple?
This is how I solved it in TSQL in MS SQL Server 2019.
I know its not exactly what you wanted. Just an interesting thing to try. Thanks for that.
DROP TABLE IF EXISTS #Samples
SELECT n.Name
INTO #Samples
FROM
(
SELECT 'Ravi' AS Name
UNION
SELECT 'Tim'
UNION
SELECT 'Timothe'
UNION
SELECT 'Ian'
UNION
SELECT 'Lijoo'
UNION
SELECT 'John'
UNION
SELECT 'Jami'
) AS n
SELECT g.Name,
IIF(MAX (g.Repeat) = MIN (g.Repeat) AND SUM (g.Appearance) >= 2, 'Valid', 'Invalid') AS Validity
FROM
(
SELECT v.value,
s.Name,
SUM (LEN (s.Name) - LEN (REPLACE (s.Name, v.value, ''))) AS Repeat,
SUM (IIF(s.Name LIKE '%' + v.value + '%', 1, 0)) AS Appearance
FROM STRING_SPLIT('a,e,i,o,u', ',') AS v
CROSS APPLY #Samples AS s
GROUP BY v.value,
s.Name
) AS g
WHERE g.Repeat > 0
GROUP BY g.Name
Output
we can replace STRING_SPLIT with a temp table for supporting lower versions
DROP TABLE IF EXISTS #Vowels
SELECT C.Vowel
INTO #Vowels
FROM
(
SELECT 'a' AS Vowel
UNION
SELECT 'e'
UNION
SELECT 'i'
UNION
SELECT 'o'
UNION
SELECT 'u'
) AS C
SELECT g.Name,
IIF(MAX (g.Repeat) = MIN (g.Repeat) AND SUM (g.Appearance) >= 2, 'Valid', 'Invalid') AS Validity
FROM
(
SELECT v.Vowel,
s.Name,
SUM (LEN (s.Name) - LEN (REPLACE (s.Name, v.Vowel, ''))) AS Repeat,
SUM (IIF(s.Name LIKE '%' + v.Vowel + '%', 1, 0)) AS Appearance
FROM #Vowels AS v
CROSS APPLY #Samples AS s
GROUP BY v.Vowel,
s.Name
) AS g
WHERE g.Repeat > 0
GROUP BY g.Name
From Oracle 12, you can use:
SELECT name
FROM table_name
CROSS JOIN LATERAL(
SELECT 1
FROM (
-- Step 2: Count the frequency of each vowel
SELECT letter,
COUNT(*) As frequency
FROM (
-- Step 1: Find all the vowels
SELECT REGEXP_SUBSTR(LOWER(name), '[aeiou]', 1, LEVEL) AS letter
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT(LOWER(name), '[aeiou]')
)
GROUP BY letter
)
-- Step 3: Filter out names where the number of vowels are
-- not equal or the vowels do not occur at least twice
-- and there are not at least 2 different vowels.
HAVING MIN(frequency) >= 2
AND MIN(frequency) = MAX(frequency)
AND COUNT(*) >= 2
);
Which, for the sample data:
CREATE TABLE table_name (name) AS
SELECT 'Alexander' FROM DUAL UNION ALL
SELECT 'Johnaton' FROM DUAL UNION ALL
SELECT 'Anna' FROM DUAL;
Outputs:
NAME
Alexander
db<>fiddle here

SQL hierarchy count totals report

I'm creating a report with SQL server 2012 and Report Builder which must show the total number of Risks at a high, medium and low level for each Parent Element.
Each Element contains a number of Risks which are rated at a certain level. I need the total for the Parent Elements. The total will include the number of all the Child Elements and also the number the Element itself may have.
I am using CTEs in my query- the code I have attached isn't working (there are no errors - it's just displaying the incorrect results) and I'm not sure that my logic is correct??
Hopefully someone can help. Thanks in advance.
My table structure is:
ElementTable
ElementTableId(PK) ElementName ElementParentId
RiskTable
RiskId(PK) RiskName RiskRating ElementId(FK)
My query:
WITH cte_Hierarchy(ElementId, ElementName, Generation, ParentElementId)
AS (SELECT ElementId,
NAME,
0,
ParentElementId
FROM Extract.Element AS FirtGeneration
WHERE ParentElementId IS NULL
UNION ALL
SELECT NextGeneration.ElementId,
NextGeneration.NAME,
Parent.Generation + 1,
Parent.ElementId
FROM Extract.Element AS NextGeneration
INNER JOIN cte_Hierarchy AS Parent
ON NextGeneration.ParentElementId = Parent.ElementId),
CTE_HighRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS HighRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'High'
GROUP BY r.ElementId),
CTE_LowRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS LowRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'Low'
GROUP BY r.ElementId),
CTE_MedRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS MedRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'Medium'
GROUP BY r.ElementId)
SELECT rd.ElementId,
rd.ElementName,
rd.ParentElementId,
Generation,
HighRisk,
MedRisk,
LowRisk
FROM cte_Hierarchy rd
LEFT OUTER JOIN CTE_HighRisk h
ON rd.ElementId = h.ElementId
LEFT OUTER JOIN CTE_MedRisk m
ON rd.ElementId = m.ElementId
LEFT OUTER JOIN CTE_LowRisk l
ON rd.ElementId = l.ElementId
WHERE Generation = 1
Edit:
Sample Data
ElementTableId(PK) -- ElementName -- ElementParentId
1 ------------------- Main --------------0
2 --------------------Element1-----------1
3 --------------------Element2 ----------1
4 --------------------SubElement1 -------2
RiskId(PK) RiskName RiskRating ElementId(FK)
a -------- Financial -- High ----- 2
b -------- HR --------- High ----- 3
c -------- Marketing -- Low ------- 2
d -------- Safety -----Medium ----- 4
Sample Output:
Element Name High Medium Low
Main ---------- 2 ---- 1 -------1
Here is your sample tables
SELECT * INTO #TABLE1
FROM
(
SELECT 1 ElementTableId, 'Main' ElementName ,0 ElementParentId
UNION ALL
SELECT 2,'Element1',1
UNION ALL
SELECT 3, 'Element2',1
UNION ALL
SELECT 4, 'SubElement1',2
)TAB
SELECT * INTO #TABLE2
FROM
(
SELECT 'a' RiskId, 'Fincancial' RiskName,'High' RiskRating ,2 ElementId
UNION ALL
SELECT 'b','HR','High',3
UNION ALL
SELECT 'c', 'Marketing','Low',2
UNION ALL
SELECT 'd', 'Safety','Medium',4
)TAB
We are finding the children of a parent, its count of High,Medium and Low and use cross join to show parent with all the combinations of its children's High,Medium and Low
UPDATE
The below variable can be used to access the records dynamically.
DECLARE #ElementTableId INT;
--SET #ElementTableId = 1
And use the above variable inside the query
;WITH CTE1 AS
(
SELECT *,0 [LEVEL] FROM #TABLE1 WHERE ElementTableId = #ElementTableId
UNION ALL
SELECT E.*,e2.[LEVEL]+1 FROM #TABLE1 e
INNER JOIN CTE1 e2 on e.ElementParentId = e2.ElementTableId
AND E.ElementTableId<>#ElementTableId
)
,CTE2 AS
(
SELECT E1.*,E2.*,COUNT(RiskRating) OVER(PARTITION BY RiskRating) CNT
from CTE1 E1
LEFT JOIN #TABLE2 E2 ON E1.ElementTableId=E2.ElementId
)
,CTE3 AS
(
SELECT DISTINCT T1.ElementName,C2.RiskRating,C2.CNT
FROM #TABLE1 T1
CROSS JOIN CTE2 C2
WHERE T1.ElementTableId = #ElementTableId
)
SELECT *
FROM CTE3
PIVOT(MIN(CNT)
FOR RiskRating IN ([High], [Medium],[Low])) AS PVTTable
SQL FIDDLE
RESULT
UPDATE 2
I am updating as per your new requirement
Here is sample table in which I have added extra data to test
SELECT * INTO #ElementTable
FROM
(
SELECT 1 ElementTableId, 'Main' ElementName ,0 ElementParentId
UNION ALL
SELECT 2,'Element1',1
UNION ALL
SELECT 3, 'Element2',1
UNION ALL
SELECT 4, 'SubElement1',2
UNION ALL
SELECT 5, 'Main 2',0
UNION ALL
SELECT 6, 'Element21',5
UNION ALL
SELECT 7, 'SubElement21',6
UNION ALL
SELECT 8, 'SubElement22',7
UNION ALL
SELECT 9, 'SubElement23',7
)TAB
SELECT * INTO #RiskTable
FROM
(
SELECT 'a' RiskId, 'Fincancial' RiskName,'High' RiskRating ,2 ElementId
UNION ALL
SELECT 'b','HR','High',3
UNION ALL
SELECT 'c', 'Marketing','Low',2
UNION ALL
SELECT 'd', 'Safety','Medium',4
UNION ALL
SELECT 'e' , 'Fincancial' ,'High' ,5
UNION ALL
SELECT 'f','HR','High',6
UNION ALL
SELECT 'g','HR','High',6
UNION ALL
SELECT 'h', 'Marketing','Low',7
UNION ALL
SELECT 'i', 'Safety','Medium',8
UNION ALL
SELECT 'j', 'Safety','High',8
)TAB
I have written the logic in query
;WITH CTE1 AS
(
-- Here you will find the level of every elements in the table
SELECT *,0 [LEVEL]
FROM #ElementTable WHERE ElementParentId = 0
UNION ALL
SELECT ET.*,CTE1.[LEVEL]+1
FROM #ElementTable ET
INNER JOIN CTE1 on ET.ElementParentId = CTE1.ElementTableId
)
,CTE2 AS
(
-- Filters the level and find the major parant of each child
-- ie, 100->150->200, here the main parent of 200 is 100
SELECT *,CTE1.ElementTableId MajorParentID,CTE1.ElementName MajorParentName
FROM CTE1 WHERE [LEVEL]=1
UNION ALL
SELECT CTE1.*,CTE2.MajorParentID,CTE2.MajorParentName
FROM CTE1
INNER JOIN CTE2 on CTE1.ElementParentId = CTE2.ElementTableId
)
,CTE3 AS
(
-- Since each child have columns for main parent id and name,
-- you will get the count of each element corresponding to the level you have selected directly
SELECT DISTINCT CTE2.MajorParentName,RT.RiskRating ,
COUNT(RiskRating) OVER(PARTITION BY MajorParentID,RiskRating) CNT
FROM CTE2
JOIN #RiskTable RT ON CTE2.ElementTableId=RT.ElementId
)
SELECT MajorParentName, ISNULL([High],0)[High], ISNULL([Medium],0)[Medium],ISNULL([Low],0)[Low]
FROM CTE3
PIVOT(MIN(CNT)
FOR RiskRating IN ([High], [Medium],[Low])) AS PVTTable
SQL FIDDLE

How do I combine 2 records with a single field into 1 row with 2 fields (Oracle 11g)?

Here's a sample data
record1: field1 = test2
record2: field1 = test3
The actual output I want is
record1: field1 = test2 | field2 = test3
I've looked around the net but can't find what I'm looking for. I can use a custom function to get it in this format but I'm trying to see if there's a way to make it work without resorting to that.
thanks a lot
You need to use pivot:
with t(id, d) as (
select 1, 'field1 = test2' from dual union all
select 2, 'field1 = test3' from dual
)
select *
from t
pivot (max (d) for id in (1, 2))
If you don't have the id field you can generate it, but you will have XML type:
with t(d) as (
select 'field1 = test2' from dual union all
select 'field1 = test3' from dual
), t1(id, d) as (
select ROW_NUMBER() OVER(ORDER BY d), d from t
)
select *
from t1
pivot xml (max (d) for id in (select id from t1))
There are several ways to approach this - google pivot rows to columns. Here is one set of answers: http://www.dba-oracle.com/t_converting_rows_columns.htm

Find overlapping sets of data in a table

I need to identify duplicate sets of data and give those sets who's data is similar a group id.
id threshold cost
-- ---------- ----------
1 0 9
1 100 7
1 500 6
2 0 9
2 100 7
2 500 6
I have thousands of these sets, most are the same with different id's. I need find all the like sets that have the same thresholds and cost amounts and give them a group id. I'm just not sure where to begin. Is the best way to iterate and insert each set into a table and then each iterate through each set in the table to find what already exists?
This is one of those cases where you can try to do something with relational operators. Or, you can just say: "let's put all the information in a string and use that as the group id". SQL Server seems to discourage this approach, but it is possible. So, let's characterize the groups using:
select d.id,
(select cast(threshold as varchar(8000)) + '-' + cast(cost as varchar(8000)) + ';'
from data d2
where d2.id = d.id
for xml path ('')
order by threshold
) as groupname
from data d
group by d.id;
Oh, I think that solves your problem. The groupname can serve as the group id. If you want a numeric id (which is probably a good idea, use dense_rank():
select d.id, dense_rank() over (order by groupname) as groupid
from (select d.id,
(select cast(threshold as varchar(8000)) + '-' + cast(cost as varchar(8000)) + ';'
from data d2
where d2.id = d.id
for xml path ('')
order by threshold
) as groupname
from data d
group by d.id
) d;
Here's the solution to my interpretation of the question:
IF OBJECT_ID('tempdb..#tempGrouping') IS NOT NULL DROP Table #tempGrouping;
;
WITH BaseTable AS
(
SELECT 1 id, 0 as threshold, 9 as cost
UNION SELECT 1, 100, 7
UNION SELECT 1, 500, 6
UNION SELECT 2, 0, 9
UNION SELECT 2, 100, 7
UNION SELECT 2, 500, 6
UNION SELECT 3, 1, 9
UNION SELECT 3, 100, 7
UNION SELECT 3, 500, 6
)
, BaseCTE AS
(
SELECT
id
--,dense_rank() over (order by threshold, cost ) as GroupId
,
(
SELECT CAST(TblGrouping.threshold AS varchar(8000)) + '/' + CAST(TblGrouping.cost AS varchar(8000)) + ';'
FROM BaseTable AS TblGrouping
WHERE TblGrouping.id = BaseTable.id
ORDER BY TblGrouping.threshold, TblGrouping.cost
FOR XML PATH ('')
) AS MultiGroup
FROM BaseTable
GROUP BY id
)
,
CTE AS
(
SELECT
*
,DENSE_RANK() OVER (ORDER BY MultiGroup) AS GroupId
FROM BaseCTE
)
SELECT *
INTO #tempGrouping
FROM CTE
-- SELECT * FROM #tempGrouping;
UPDATE BaseTable
SET BaseTable.GroupId = #tempGrouping.GroupId
FROM BaseTable
INNER JOIN #tempGrouping
ON BaseTable.Id = #tempGrouping.Id
IF OBJECT_ID('tempdb..#tempGrouping') IS NOT NULL DROP Table #tempGrouping;
Where BaseTable is your table, and and you don't need the CTE "BaseTable", because you have a data table.
You may need to take extra-precautions if your threshold and cost fields can be NULL.

Find all intersections of all sets of ranges in PostgreSQL

I'm looking for an efficient way to find all the intersections between sets of timestamp ranges. It needs to work with PostgreSQL 9.2.
Let's say the ranges represent the times when a person is available to meet. Each person may have one or more ranges of times when they are available. I want to find all the time periods when a meeting can take place (ie. during which all people are available).
This is what I've got so far. It seems to work, but I don't think it's very efficient, since it considers one person's availability at a time.
WITH RECURSIVE td AS
(
-- Test data. Returns:
-- ["2014-01-20 00:00:00","2014-01-31 00:00:00")
-- ["2014-02-01 00:00:00","2014-02-20 00:00:00")
-- ["2014-04-15 00:00:00","2014-04-20 00:00:00")
SELECT 1 AS entity_id, '2014-01-01'::timestamp AS begin_time, '2014-01-31'::timestamp AS end_time
UNION SELECT 1, '2014-02-01', '2014-02-28'
UNION SELECT 1, '2014-04-01', '2014-04-30'
UNION SELECT 2, '2014-01-15', '2014-02-20'
UNION SELECT 2, '2014-04-15', '2014-05-05'
UNION SELECT 3, '2014-01-20', '2014-04-20'
)
, ranges AS
(
-- Convert to tsrange type
SELECT entity_id, tsrange(begin_time, end_time) AS the_range
FROM td
)
, min_max AS
(
SELECT MIN(entity_id), MAX(entity_id)
FROM td
)
, inter AS
(
-- Ranges for the lowest ID
SELECT entity_id AS last_id, the_range
FROM ranges r
WHERE r.entity_id = (SELECT min FROM min_max)
UNION ALL
-- Iteratively intersect with ranges for the next higher ID
SELECT entity_id, r.the_range * i.the_range
FROM ranges r
JOIN inter i ON r.the_range && i.the_range
WHERE r.entity_id > i.last_id
AND NOT EXISTS
(
SELECT *
FROM ranges r2
WHERE r2.entity_id < r.entity_id AND r2.entity_id > i.last_id
)
)
-- Take the final set of intersections
SELECT *
FROM inter
WHERE last_id = (SELECT max FROM min_max)
ORDER BY the_range;
I created the tsrange_interception_agg aggregate
create function tsrange_interception (
internal_state tsrange, next_data_values tsrange
) returns tsrange as $$
select internal_state * next_data_values;
$$ language sql;
create aggregate tsrange_interception_agg (tsrange) (
sfunc = tsrange_interception,
stype = tsrange,
initcond = $$[-infinity, infinity]$$
);
Then this query
with td (id, begin_time, end_time) as
(
values
(1, '2014-01-01'::timestamp, '2014-01-31'::timestamp),
(1, '2014-02-01', '2014-02-28'),
(1, '2014-04-01', '2014-04-30'),
(2, '2014-01-15', '2014-02-20'),
(2, '2014-04-15', '2014-05-05'),
(3, '2014-01-20', '2014-04-20')
), ranges as (
select
id,
row_number() over(partition by id) as rn,
tsrange(begin_time, end_time) as tr
from td
), cr as (
select r0.tr tr0, r1.tr as tr1
from ranges r0 cross join ranges r1
where
r0.id < r1.id and
r0.tr && r1.tr and
r0.id = (select min(id) from td)
)
select tr0 * tsrange_interception_agg(tr1) as interseptions
from cr
group by tr0
having count(*) = (select count(distinct id) from td) - 1
;
interseptions
-----------------------------------------------
["2014-02-01 00:00:00","2014-02-20 00:00:00")
["2014-01-20 00:00:00","2014-01-31 00:00:00")
["2014-04-15 00:00:00","2014-04-20 00:00:00")
If you have a fixed number of entities you want to cross reference, you can use a cross join for each of them, and build the intersection (using the * operator on ranges).
Using a cross join like this is probably less efficient, though. The following example has more to do with explaining the more complex example below.
WITH td AS
(
SELECT 1 AS entity_id, '2014-01-01'::timestamp AS begin_time, '2014-01-31'::timestamp AS end_time
UNION SELECT 1, '2014-02-01', '2014-02-28'
UNION SELECT 1, '2014-04-01', '2014-04-30'
UNION SELECT 2, '2014-01-15', '2014-02-20'
UNION SELECT 2, '2014-04-15', '2014-05-05'
UNION SELECT 4, '2014-01-20', '2014-04-20'
)
,ranges AS
(
-- Convert to tsrange type
SELECT entity_id, tsrange(begin_time, end_time) AS the_range
FROM td
)
SELECT r1.the_range * r2.the_range * r3.the_range AS r
FROM ranges r1
CROSS JOIN ranges r2
CROSS JOIN ranges r3
WHERE r1.entity_id=1 AND r2.entity_id=2 AND r3.entity_id=4
AND NOT isempty(r1.the_range * r2.the_range * r3.the_range)
ORDER BY r
In this case a multiple cross join is probably less efficient because you don't actually need to have all the possible combinations of every range in reality, since isempty(r1.the_range * r2.the_range) is enough to make isempty(r1.the_range * r2.the_range * r3.the_range) true.
I don't think you can avoid going through each person's availability at time, since you want them all to be meet anyway.
What may help is to build the set of intersections incrementally, by cross joining each person's availability to the previous subset you've calculated using another recursive CTE (intersections in the example below). You then build the intersections incrementally and get rid of the empty ranges, both stored arrays:
WITH RECURSIVE td AS
(
SELECT 1 AS entity_id, '2014-01-01'::timestamp AS begin_time, '2014-01-31'::timestamp AS end_time
UNION SELECT 1, '2014-02-01', '2014-02-28'
UNION SELECT 1, '2014-04-01', '2014-04-30'
UNION SELECT 2, '2014-01-15', '2014-02-20'
UNION SELECT 2, '2014-04-15', '2014-05-05'
UNION SELECT 4, '2014-01-20', '2014-04-20'
)
,ranges AS
(
-- Convert to tsrange type
SELECT entity_id, tsrange(begin_time, end_time) AS the_range
FROM td
)
,ranges_arrays AS (
-- Prepare an array of all possible intervals per entity
SELECT entity_id, array_agg(the_range) AS ranges_arr
FROM ranges
GROUP BY entity_id
)
,numbered_ranges_arrays AS (
-- We'll join using pos+1 next, so we want continuous integers
-- I've changed the example entity_id from 3 to 4 to demonstrate this.
SELECT ROW_NUMBER() OVER () AS pos, entity_id, ranges_arr
FROM ranges_arrays
)
,intersections (pos, subranges) AS (
-- We start off with the infinite range.
SELECT 0::bigint, ARRAY['[,)'::tsrange]
UNION ALL
-- Then, we unnest the previous intermediate result,
-- cross join it against the array of ranges from the
-- next row in numbered_ranges_arrays (joined via pos+1).
-- We take the intersection and remove the empty array.
SELECT r.pos,
ARRAY(SELECT x * y FROM unnest(r.ranges_arr) x CROSS JOIN unnest(i.subranges) y WHERE NOT isempty(x * y))
FROM numbered_ranges_arrays r
INNER JOIN intersections i ON r.pos=i.pos+1
)
,last_intersections AS (
-- We just really want the result from the last operation (with the max pos).
SELECT subranges FROM intersections ORDER BY pos DESC LIMIT 1
)
SELECT unnest(subranges) r FROM last_intersections ORDER BY r
I'm not sure whether this is likely to perform better, unfortunately. You'd probably need a larger dataset to have meaningful benchmarks.
OK, I wrote and tested this in TSQL but it should run or at least be close enough for you to translate back, it's all fairly vanilla constructs. Except maybe the between, but that can be broken into a < clause and a > clause. (thanks #Horse)
WITH cteSched AS ( --Schedule for everyone
-- Test data. Returns:
-- ["2014-01-20 00:00:00","2014-01-31 00:00:00")
-- ["2014-02-01 00:00:00","2014-02-20 00:00:00")
-- ["2014-04-15 00:00:00","2014-04-20 00:00:00")
SELECT 1 AS entity_id, '2014-01-01' AS begin_time, '2014-01-31' AS end_time
UNION SELECT 1, '2014-02-01', '2014-02-28'
UNION SELECT 1, '2014-04-01', '2014-04-30'
UNION SELECT 2, '2014-01-15', '2014-02-20'
UNION SELECT 2, '2014-04-15', '2014-05-05'
UNION SELECT 3, '2014-01-20', '2014-04-20'
), cteReq as ( --List of people to schedule (or is everyone in Sched required? Not clear, doesn't hurt)
SELECT 1 as entity_id UNION SELECT 2 UNION SELECT 3
), cteBegins as (
SELECT distinct begin_time FROM cteSched as T
WHERE NOT EXISTS (SELECT entity_id FROM cteReq as R
WHERE NOT EXISTS (SELECT * FROM cteSched as X
WHERE X.entity_id = R.entity_id
AND T.begin_time BETWEEN X.begin_time AND X.end_time ))
) SELECT B.begin_time, MIN(S.end_time ) as end_time
FROM cteBegins as B cross join cteSched as S
WHERE B.begin_time between S.begin_time and S.end_time
GROUP BY B.begin_time
-- NOTE: This assume users do not have schedules that overlap with themselves! That is, nothing like
-- John is available 2014-01-01 to 2014-01-15 and 2014-01-10 to 2014-01-20.
EDIT: Add output from above (when executed on SQL-Server 2008R2)
begin_time end_time
2014-01-20 2014-01-31
2014-02-01 2014-02-20
2014-04-15 2014-04-20