Related
I have a SQL table in the below format. Every ID has 2 entries in the table for a particular date.
Input:
ID date rownum subid value1 value2
A 200911 1 X 10 20
A 200911 2 Y 15 25
B 201001 2 S 60 35
B 201001 1 R 40 50
I want to write a SQL query to change this to the below format, so that every ID/date combination has only 1 entry as shown below. The rownum is already included and the values should be represented so that rownnum 1 is displayed first and then the value with rownum second as shown below.
Output:
ID date row1subid row1value1 row1value2 row2subid row2value1 row2value2
A 200911 X 10 20 Y 15 25
B 201001 R 40 50 S 60 35
Let me know if something is not clear.
Thanks for all your help!
Here is what you need to do as a SQL Fiddle.
And for reference:
CREATE TABLE TestData
([ID] varchar(1),
[date] int,
[rownum] int,
[subid] varchar(1),
[value1] int,
[value2] int)
;
INSERT INTO TestData
([ID], [date], [rownum], [subid], [value1], [value2])
VALUES
('A', 200911, 1, 'X', 10, 20),
('A', 200911, 2, 'Y', 15, 25),
('B', 201001, 2, 'S', 60, 35),
('B', 201001, 1, 'R', 40, 50)
;
SELECT A.ID, A.date, A.rownum, A.subid, A.value1, A.value2, B.value1 AS r2value1, B.value2 AS r2value2
FROM TestData AS A
INNER JOIN TestData B ON A.id = B.id AND B.rownum = 2
WHERE A.rownum = 1
I have two tables JOB and EMP; structure and values are like this:
CREATE TABLE JOB
(
JOBID SMALLINT UNIQUE NOT NULL,
JOBNAME CHAR(15)
);
CREATE TABLE EMP
(
EMPID SMALLINT,
JOBID SMALLINT,
SAL SMALLINT,
CITYID SMALLINT,
YEAR SMALLINT,
STATUS CHAR(1)
);
INSERT INTO JOB(JOBID, JOBNAME)
VALUES (1, 'DEVELOPMENT'),
(2, 'DEVELOPMENT'),
(3, 'TESTING'),
(4, 'TESTING'),
(7, 'TESTING'),
(9, 'RESEARCH'),
(8, 'HR');
INSERT INTO EMP (EMPID , JOBID, SAL, CITYID, YEAR, STATUS)
VALUES (100, 1, 1000, 10, 2015, 'A'),
(200, 2, 2000, 10, 2015, 'A'),
(300, 1, 2500, 20, 2015, 'A'),
(400, 3, 1000, 10, 2016, 'A'),
(500, 6, 3000, 10, 2015, 'E'),
(600, 8, 1000, 30, 2015, 'A'),
(700, 8, 2000, 10, 2015, 'E'),
(800, 9, 1500, 10, 2015, 'A');
I want to display all jobname count and avg salaries; for the jobname if jobid's not exists then display 0
For the given input cityid , YEAR and STATUS ( Emp table), take all jobid for each jobname (from job table ) and match in Emp table, if exists display count (count of jobid present in Emp table) and avgsal else 0 for count and avgsal. And Sal is calculated based on Status. If Status is 'A' Sal goes to Status-A-Sal else Status-E-Sal. And for each matched i.e non zero record put 'X' in another field
Output should be like this for Cityid's 10 and 20 year 2015. Results should be displayed first for Status 'A' and then Status 'E'. Added status type field in result .
Cityid Status-type jobname count STATUS sal
--------------------------------------------------------------
10 STATUSA development 2 X 1500
10 STATUSA TESTING 0 0
10 STATUSA RESEARCH 1 X 1500
10 STATUSA HR 0 0
10 total 3 0
10 STATUSE development 0 0
10 STATUSE TESTING 0 0
10 STATUSE RESEARCH 0 0
10 STATUSE HR 1 X 2000
10 total 1 2000
20 STATUSA development 1 X 2500
20 STATUSA TESTING 0 0
20 STATUSA RESEARCH 0 0
20 STATUSA HR 0 0
20 total 1 2500
20 STATUSE development 0 0
20 STATUSE TESTING 0 0
20 STATUSE RESEARCH 0 0
20 STATUSE HR 0 0
20 total 0 0
How to bring the results one status after another ?
I've tried like this but its throwing
SELECT C.CITYID AS CITYID,
CASE WHEN P.STATUS ='A' THEN 'STATUSA' ELSE 'STATUSE' END AS STATUS_TYPE ,
COALESCE(J.JOBNAME, 'TOTAL') AS JOBNAME,
COUNT(CASE WHEN P.STATUS ='A' THEN P.CITYID END ) AS COUNT ,
COALESCE(AVG(CAST(CASE WHEN P.STATUS = 'A' THEN P.SAL END AS DECIMAL(13,2)))/12 , 0) AS "AVG SAL",
COUNT(CASE WHEN P.STATUS ='E' THEN P.CITYID END ) AS COUNT ,
COALESCE(AVG(CAST(CASE WHEN P.STATUS = 'E' THEN P.SAL END AS DECIMAL(13,2)))/12 , 0) AS "AVG SAL"
FROM JOB1 J
CROSS JOIN
(SELECT DISTINCT CITYID
FROM EMP1 B WHERE CITYID = 10
) C
LEFT JOIN EMP1 P ON P.JOBID = J.JOBID
AND P.CITYID = C.CITYID and
YEAR = 2015
GROUP BY ROLLUP(C.CITYID, J.JOBNAME );
ERROR:
Column 'EMP1.STATUS' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
IF OBJECT_ID('tempdb..#JOB') IS NOT NULL
DROP TABLE #JOB
CREATE TABLE #JOB
(
JOBID SMALLINT UNIQUE NOT NULL,
JOBNAME CHAR(15)
);
IF OBJECT_ID('tempdb..#EMP') IS NOT NULL
DROP TABLE #EMP
CREATE TABLE #EMP
(
EMPID SMALLINT,
JOBID SMALLINT,
SAL SMALLINT,
CITYID SMALLINT,
YEAR SMALLINT,
STATUS CHAR(1)
);
INSERT INTO #JOB(JOBID, JOBNAME)
VALUES (1, 'DEVELOPMENT'),
(2, 'DEVELOPMENT'),
(3, 'TESTING'),
(4, 'TESTING'),
(7, 'TESTING'),
(9, 'RESEARCH'),
(8, 'HR');
INSERT INTO #EMP (EMPID , JOBID, SAL, CITYID, YEAR, STATUS)
VALUES (100, 1, 1000, 10, 2015, 'A'),
(200, 2, 2000, 10, 2015, 'A'),
(300, 1, 2500, 20, 2015, 'A'),
(400, 3, 1000, 10, 2016, 'A'),
(500, 6, 3000, 10, 2015, 'E'),
(600, 8, 1000, 30, 2015, 'A'),
(700, 8, 2000, 10, 2015, 'E'),
(800, 9, 1500, 10, 2015, 'A');
;with cteJobDict as (
select
distinct
j.JOBNAME
from
#JOB j
)
,cteStatusDict as(
select
distinct STATUS
from
#EMP e
),cteCityDict as (
select
distinct CITYID
from
#EMP
)
,cteJobStatusCityMatrix as(
select
*
from
cteJobDict
cross apply cteStatusDict
cross apply cteCityDict
)
,cteEmpWithJobName as (
select
e.*
,j.JOBNAME
from
#EMP e
join #JOB j on j.JOBID=e.JOBID
), cteData as (
SELECT
m.CITYID
,CASE WHEN m.STATUS ='A' THEN 'STATUSA' ELSE 'STATUSE' end as [Status-type]
,CASE WHEN m.STATUS ='A' THEN 1 ELSE 3 end as [Status-order]
,m.JOBNAME
,count(distinct e.EMPID) count
,iif(count(distinct e.EMPID)>0,'X','') status
,isnull(avg(e.sal),0) sal
FROM
cteJobStatusCityMatrix m
left join cteEmpWithJobName e on e.CITYID=m.CITYID and e.STATUS=m.STATUS and e.JOBNAME=m.JOBNAME and e.YEAR=2015
where
m.CITYID in (10,20)
group by
m.CITYID
,m.STATUS
,m.JOBNAME
union
SELECT
m.CITYID
,'total' as [Status-type]
,CASE WHEN m.STATUS ='A' THEN 2 ELSE 4 end as [Status-order]
,null
,count(distinct e.EMPID) count
,iif(count(distinct e.EMPID)>0,'X','') status
,isnull(avg(e.sal),0) sal
FROM
cteJobStatusCityMatrix m
left join cteEmpWithJobName e
on e.CITYID=m.CITYID
and e.STATUS=m.STATUS
and e.JOBNAME=m.JOBNAME
and e.YEAR=2015 -- here goes year
where
m.CITYID in (10,20) -- here goes cityid
group by
m.CITYID
,m.STATUS)
select
CITYID
,[Status-type]
,JOBNAME
,count
,status
,sal
from
cteData
order by
CITYID
,[Status-order]
I want to select the rows which satisfy both conditions of the same column. Below is the table schema.
Security Table
Id RoleId CompId SecurityToken Accesstype
1 1 10 abc 2
2 1 10 xyz 2
3 12 10 abc 2
4 16 12 abc 2
5 16 12 xyz 2
6 30 13 abc 2
7 1 10 efg 2
8 1 10 lmn 0
I want "All rows for each RoleID/CompID combination where accesstype = 2 and there is both a row containing securitytoken "abc" and a row containing SecurityToken "xyz" for that role/compID combination"
Output should be
Id RoleId CompId SecurityToken Accesstype
1 1 10 abc 2
2 1 10 xyz 2
4 16 12 abc 2
5 16 12 xyz 2
I believe the following query will produce the desired output:
SELECT *
FROM testdata
WHERE Accesstype = 2
AND SecurityToken IN ('abc', 'xyz')
AND EXISTS (
SELECT 1
FROM testdata AS tmp
WHERE RoleId = testdata.RoleId
AND CompId = testdata.CompId
AND Accesstype = testdata.AccessType
AND SecurityToken IN ('abc', 'xyz')
HAVING COUNT(DISTINCT SecurityToken) = 2
)
SQL Fiddle
In order to eliminate sets that contain extra security tokens (such as efg and lmn) change the WHERE and HAVING clause to:
WHERE RoleId = testdata.RoleId
AND CompId = testdata.CompId
AND Accesstype = testdata.AccessType
HAVING COUNT(DISTINCT SecurityToken) = 2
AND COUNT(DISTINCT SecurityToken) = COUNT(CASE WHEN SecurityToken IN ('abc', 'xyz') THEN 1 END)
CREATE TABLE #Table1
([Id] int, [RoleId] int, [CompId] int, [SecurityToken] varchar(3), [Accesstype] int)
;
INSERT INTO #Table1
([Id], [RoleId], [CompId], [SecurityToken], [Accesstype])
VALUES
(1, 1, 10, 'abc', 2),
(2, 1, 10, 'xyz', 2),
(3, 12, 10, 'abc', 2),
(4, 16, 12, 'abc', 2),
(5, 16, 12, 'xyz', 2),
(6, 30, 13, 'abc', 2)
;
WITH cte AS (
SELECT *,ROW_NUMBER() OVER (PARTITION BY [ROLEID],[COMPID] ORDER BY ID) AS RN FROM #TABLE1
),
COUNTED AS (
SELECT
*,
COUNT(*) OVER (PARTITION BY [ROLEID],[COMPID]) AS CNT
FROM cte
)
SELECT
[ID], [ROLEID], [COMPID], [SECURITYTOKEN], [ACCESSTYPE]
FROM COUNTED
WHERE CNT >= 2
output
ID ROLEID COMPID SECURITYTOKEN ACCESSTYPE
1 1 10 abc 2
2 1 10 xyz 2
4 16 12 abc 2
5 16 12 xyz 2
or
WITH CTE AS
(
SELECT
*,
COUNT(*) OVER (PARTITION BY [ROLEID],[COMPID]) AS CNT
FROM #TABLE1)
SELECT [ID], [ROLEID], [COMPID], [SECURITYTOKEN], [ACCESSTYPE] FROM CTE WHERE CNT>=2
One method uses exists:
select t.*
from t
where t.Accesstype = 2 and
t.securityToken in ('abc', 'xyz') and
exists (select 1
from t t2
where t2.RoleId = t.RoleId and
t2.CompId = t.CompId and
t2.Accesstype = t.AccessType and
t2.SecurityToken in ('abc', 'xyz') and
t2.SecrityToken <> t.SecurityToken
);
Perhaps a simpler method uses window functions:
select t.*
from (select t.*,
min(securitytoken) over (partition by roleid, compid) as min_st,
min(securitytoken) over (partition by roleid, compid) as max_st
from t
where t.Accesstype = 2 and
t.SecurityToken in ('abc', 'xyz')
) t
where minsecuritytoken = 'abc' and
maxsecuritytoken = 'xyz;
I've seen several questions/answers on how to recursively query a self-referencing table, but I am struggling to apply the answers I've found to aggregate up to each parent, grandparent, etc. regardless of where the item sits in the hierarchy.
Need to get an average salary for each department including hierarchy.
It means department should include the average salary of each sub-department and so on.
I've got nex db schema:
CREATE TABLE Employee
(
Id INT NOT NULL ,
Name VARCHAR(200) NOT NULL ,
Department_Id INT NOT NULL ,
PRIMARY KEY ( Id )
);
CREATE TABLE Department
(
Id INT NOT NULL ,
DepartmentName VARCHAR(200) NOT NULL ,
Parent_Id INT ,
PRIMARY KEY ( Id )
);
CREATE TABLE Salary
(
Id INT NOT NULL ,
Date DATETIME NOT NULL ,
Amount INT NOT NULL ,
Employee_Id INT NOT NULL ,
PRIMARY KEY ( Id )
);
I've tried something like that but it includes only 1st level of a hierarchy.
SELECT d.Id ,
d.DepartmentName ,
( SELECT AVG(s.Amount)
FROM dbo.Department dd
LEFT JOIN dbo.Department sdd ON dd.Id = sdd.Parent_Id
JOIN dbo.Employee e ON e.Department_Id = sdd.Id
OR e.Department_Id = dd.Id
JOIN dbo.Salary s ON s.Employee_Id = e.Id
WHERE dd.Id = d.Id
) AS avg_dep_salary
FROM dbo.Department d
WHERE d.Parent_Id IS NULL;
How can get an average salary of all levels?
EDIT: Added some inserts
INSERT INTO Employee
( Id, Name, Department_Id )
VALUES ( 1, 'Peter', 1 ),
( 2, 'Alex', 1 ),
( 3, 'Sam', 2 ),
( 4, 'James', 2 ),
( 5, 'Anna', 3 ),
( 6, 'Susan', 3 ),
( 7, 'Abby', 4 ),
( 8, 'Endy', 4 );
INSERT INTO Department
( Id, DepartmentName, Parent_Id )
VALUES ( 1, 'IT', NULL ),
( 2, 'HR', NULL),
( 3, 'SubIT', 1 ),
( 4, 'SubSubIT', 3 );
INSERT INTO Salary
( Id, Date, Amount, Employee_Id )
VALUES ( 1, '2013-01-09 16:03:50.003', 3000, 1 ),
( 2, '2013-01-11 16:03:50.003', 5000, 2 ),
( 3, '2013-01-09 16:03:50.003', 2000, 3 ),
( 4, '2013-01-11 16:03:50.003', 1000, 4 ),
( 5, '2013-01-09 16:03:50.003', 4000, 5 ),
( 6, '2013-01-11 16:03:50.003', 6000, 6 ),
( 7, '2013-01-09 16:03:50.003', 7000, 7 ),
( 8, '2013-01-13 16:03:50.003', 9000, 8 );
Expected result is:
Department | Average_Salary
__________________________________
IT | ( X1 + X2 + X3 ) / 3
HR | ( Y1 ) / 1
SubIT | ( X2 + X3 ) / 2
SubSubIT | ( X3 ) / 1
Where:
X1 - Average salary of IT department
X2 - Average salary of SubIT department
X3 - Average salary of SubSubIT department
Y1 - Average
salary of HR department
Sample data
I've added few rows with a wider tree structure.
DECLARE #Employee TABLE
(
Id INT NOT NULL ,
Name VARCHAR(200) NOT NULL ,
Department_Id INT NOT NULL ,
PRIMARY KEY ( Id )
);
DECLARE #Department TABLE
(
Id INT NOT NULL ,
DepartmentName VARCHAR(200) NOT NULL ,
Parent_Id INT ,
PRIMARY KEY ( Id )
);
DECLARE #Salary TABLE
(
Id INT NOT NULL ,
Date DATETIME NOT NULL ,
Amount INT NOT NULL ,
Employee_Id INT NOT NULL ,
PRIMARY KEY ( Id )
);
INSERT INTO #Employee
( Id, Name, Department_Id )
VALUES
( 1, 'Peter', 1 ),
( 2, 'Alex', 1 ),
( 3, 'Sam', 2 ),
( 4, 'James', 2 ),
( 5, 'Anna', 3 ),
( 6, 'Susan', 3 ),
( 7, 'Abby', 4 ),
( 8, 'Endy', 4 ),
(10, 'e_A', 10),
(11, 'e_AB', 11),
(12, 'e_AC', 12),
(13, 'e_AD', 13),
(14, 'e_ACE', 14),
(15, 'e_ACF', 15),
(16, 'e_ACG', 16);
INSERT INTO #Department
( Id, DepartmentName, Parent_Id )
VALUES
( 1, 'IT', NULL ),
( 2, 'HR', NULL),
( 3, 'SubIT', 1 ),
( 4, 'SubSubIT', 3 ),
(10, 'A', NULL ),
(11, 'AB', 10),
(12, 'AC', 10),
(13, 'AD', 10),
(14, 'ACE', 12),
(15, 'ACF', 12),
(16, 'ACG', 12);
INSERT INTO #Salary
( Id, Date, Amount, Employee_Id )
VALUES
( 1, '2013-01-09 16:03:50.003', 3000, 1 ),
( 2, '2013-01-11 16:03:50.003', 5000, 2 ),
( 3, '2013-01-09 16:03:50.003', 2000, 3 ),
( 4, '2013-01-11 16:03:50.003', 1000, 4 ),
( 5, '2013-01-09 16:03:50.003', 4000, 5 ),
( 6, '2013-01-11 16:03:50.003', 6000, 6 ),
( 7, '2013-01-09 16:03:50.003', 7000, 7 ),
( 8, '2013-01-13 16:03:50.003', 9000, 8 ),
(10, '2013-01-13 16:03:50', 100, 10),
(11, '2013-01-13 16:03:50', 100, 11),
(12, '2013-01-13 16:03:50', 100, 12),
(13, '2013-01-13 16:03:50', 100, 13),
(14, '2013-01-13 16:03:50', 100, 14),
(15, '2013-01-13 16:03:50', 100, 15),
(16, '2013-01-13 16:03:50', 100, 16);
Query
WITH
CTE_Departments
AS
(
SELECT
D.Id
,D.Parent_Id
,D.DepartmentName
,SUM(Amount) AS DepartmentAmount
,COUNT(*) AS DepartmentCount
FROM
#Department AS D
INNER JOIN #Employee AS E ON E.Department_Id = D.Id
INNER JOIN #Salary AS S ON S.Employee_Id = E.Id
GROUP BY
D.Id
,D.Parent_Id
,D.DepartmentName
)
,CTE_Recursive
AS
(
SELECT
CTE_Departments.Id AS OriginalID
,CTE_Departments.DepartmentName AS OriginalName
,CTE_Departments.Id
,CTE_Departments.Parent_Id
,CTE_Departments.DepartmentName
,CTE_Departments.DepartmentAmount
,CTE_Departments.DepartmentCount
,1 AS Lvl
FROM CTE_Departments
UNION ALL
SELECT
CTE_Recursive.OriginalID
,CTE_Recursive.OriginalName
,CTE_Departments.Id
,CTE_Departments.Parent_Id
,CTE_Departments.DepartmentName
,CTE_Departments.DepartmentAmount
,CTE_Departments.DepartmentCount
,CTE_Recursive.Lvl + 1 AS Lvl
FROM
CTE_Departments
INNER JOIN CTE_Recursive ON CTE_Recursive.Id = CTE_Departments.Parent_Id
)
SELECT
OriginalID
,OriginalName
,SUM(DepartmentAmount) AS SumAmount
,SUM(DepartmentCount) AS SumCount
,SUM(DepartmentAmount) / SUM(DepartmentCount) AS AvgAmount
FROM CTE_Recursive
GROUP BY
OriginalID
,OriginalName
ORDER BY OriginalID
;
Result
+------------+--------------+-----------+----------+-----------+
| OriginalID | OriginalName | SumAmount | SumCount | AvgAmount |
+------------+--------------+-----------+----------+-----------+
| 1 | IT | 34000 | 6 | 5666 |
| 2 | HR | 3000 | 2 | 1500 |
| 3 | SubIT | 26000 | 4 | 6500 |
| 4 | SubSubIT | 16000 | 2 | 8000 |
| 10 | A | 700 | 7 | 100 |
| 11 | AB | 100 | 1 | 100 |
| 12 | AC | 400 | 4 | 100 |
| 13 | AD | 100 | 1 | 100 |
| 14 | ACE | 100 | 1 | 100 |
| 15 | ACF | 100 | 1 | 100 |
| 16 | ACG | 100 | 1 | 100 |
+------------+--------------+-----------+----------+-----------+
Run the query step-by-step, CTE-by-CTE to understand how it works.
CTE_Departments gives total amount and number of people for each department.
CTE_Recursive recursively generates child rows for each department, while keeping the OriginalID - the ID of the department where the recursion started.
Final query simply groups everything by this OriginalID.
Here is one way
with avg_per_dep as (
select
[Month] = eomonth(s.date), d.Id, d.DepartmentName
, avgDep = avg(s.Amount * 1.0)
from
Salary s
join Employee e on s.Employee_Id = e.Id
join Department d on e.Department_Id = d.Id
group by d.Id, d.DepartmentName, eomonth(s.date)
)
, rcte as (
select
i = Id, Id
, list = cast(',' + cast(Id as varchar(10)) + ',' as varchar(max))
, step = 1
from
Department
union all
select
a.i, b.Id, cast(a.list + cast(b.Id as varchar(10)) + ',' as varchar(max))
, step + 1
from
rcte a
join Department b on a.Id = b.Parent_Id
)
select
d.DepartmentName, c.[Month]
, Average_Salary = avg(c.avgDep)
from
(
select
top 1 with ties i, list
from
rcte
order by row_number() over (partition by i order by step desc)
) t
join avg_per_dep c on t.list like '%,' + cast(c.Id as varchar(10)) + ',%'
join Department d on t.i = d.Id
group by t.i, d.DepartmentName, c.[Month]
Output
DepartmentName [Month] Average_Salary
---------------------------------------------
IT 2013-01-31 5666.666666
HR 2013-01-31 1500.000000
SubIT 2013-01-31 6500.000000
SubSubIT 2013-01-31 8000.000000
Idea:
Calculate average salary per department
Get a list of departments with all childs with recursive CTE.
Join two table and calculate avg with childs
You could also use below query to get the expected result
WITH Department_Path
AS (SELECT Id, CAST(CONCAT('#', Id, '#') AS VARCHAR(255)) AS Path
FROM Department
WHERE Parent_Id IS NULL
UNION ALL
SELECT Child.Id, CAST(CONCAT(Parent.Path, Child.Id, '#') AS VARCHAR(255)) AS Path
FROM Department Child
INNER JOIN Department_Path Parent
ON Parent.Id = Child.Parent_Id)
SELECT Department.Id,
Department.DepartmentName,
AVG(Salary.Amount) As Average_Salary,
COUNT(Employee.Id) AS Employee_Count
FROM Department
INNER JOIN Department_Path
ON CHARINDEX(CONCAT('#', Department.Id, '#'), Department_Path.Path) > 0
INNER JOIN Employee
ON Employee.Department_Id = Department_Path.Id
INNER JOIN Salary
ON Salary.Employee_Id = Employee.Id
GROUP BY Department.Id,
Department.DepartmentName;
The idea is that each employee is belong to a list of hierarchy departments. For each department, we could retrieve all employee who belong to it and then calculate the average salary.
I have two sets of interval data I.E.
Start End Type1 Type2
0 2 L NULL
2 5 L NULL
5 7 L NULL
7 10 L NULL
2 3 NULL S
3 5 NULL S
5 8 NULL S
11 12 NULL S
What I'd like to do is merge these sets into one. This seems possible by utilising an islands and gaps solution but due to the non-continuous nature of the intervals I'm not sure how to go about applying it... The output I'm expecting would be:
Start End Type1 Type2
0 2 L NULL
2 3 L S
3 5 L S
5 7 L S
7 8 L S
8 10 L NULL
11 12 NULL S
Anyone out there done something like this before??? Thanks!
Create script below:
CREATE TABLE Table1
([Start] int, [End] int, [Type1] varchar(4), [Type2] varchar(4))
;
INSERT INTO Table1
([Start], [End], [Type1], [Type2])
VALUES
(0, 2, 'L', NULL),
(2, 3, NULL, 'S'),
(2, 5, 'L', NULL),
(3, 5, NULL, 'S'),
(5, 7, 'L', NULL),
(5, 8, NULL, 'S'),
(7, 10, 'L', NULL),
(11, 12, NULL, 'S')
;
I assume that Start is inclusive, End is exclusive and given intervals do not overlap.
CTE_Number is a table of numbers. Here it is generated on the fly. I have it as a permanent table in my database.
CTE_T1 and CTE_T2 expand each interval into the corresponding number of rows using a table of numbers. For example, interval [2,5) generates rows with Values
2
3
4
This is done twice: for Type1 and Type2.
Results for Type1 and Type2 are FULL JOINed together on Value.
Finally, a gaps-and-islands pass groups/collapses intervals back.
Run the query step-by-step, CTE-by-CTE and examine intermediate results to understand how it works.
Sample data
I added few rows to illustrate a case when there is a gap between values.
DECLARE #Table1 TABLE
([Start] int, [End] int, [Type1] varchar(4), [Type2] varchar(4))
;
INSERT INTO #Table1 ([Start], [End], [Type1], [Type2]) VALUES
( 0, 2, 'L', NULL),
( 2, 3, NULL, 'S'),
( 2, 5, 'L', NULL),
( 3, 5, NULL, 'S'),
( 5, 7, 'L', NULL),
( 5, 8, NULL, 'S'),
( 7, 10, 'L', NULL),
(11, 12, NULL, 'S'),
(15, 20, 'L', NULL),
(15, 20, NULL, 'S');
Query
WITH
e1(n) AS
(
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
) -- 10
,e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b) -- 10*10
,e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2) -- 10*100
,CTE_Numbers
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY n) AS Number
FROM e3
)
,CTE_T1
AS
(
SELECT
T1.[Start] + CA.Number - 1 AS Value
,T1.Type1
FROM
#Table1 AS T1
CROSS APPLY
(
SELECT TOP(T1.[End] - T1.[Start]) CTE_Numbers.Number
FROM CTE_Numbers
ORDER BY CTE_Numbers.Number
) AS CA
WHERE
T1.Type1 IS NOT NULL
)
,CTE_T2
AS
(
SELECT
T2.[Start] + CA.Number - 1 AS Value
,T2.Type2
FROM
#Table1 AS T2
CROSS APPLY
(
SELECT TOP(T2.[End] - T2.[Start]) CTE_Numbers.Number
FROM CTE_Numbers
ORDER BY CTE_Numbers.Number
) AS CA
WHERE
T2.Type2 IS NOT NULL
)
,CTE_Values
AS
(
SELECT
ISNULL(CTE_T1.Value, CTE_T2.Value) AS Value
,CTE_T1.Type1
,CTE_T2.Type2
,ROW_NUMBER() OVER (ORDER BY ISNULL(CTE_T1.Value, CTE_T2.Value)) AS rn
FROM
CTE_T1
FULL JOIN CTE_T2 ON CTE_T2.Value = CTE_T1.Value
)
,CTE_Groups
AS
(
SELECT
Value
,Type1
,Type2
,rn
,ROW_NUMBER() OVER
(PARTITION BY rn - Value, Type1, Type2 ORDER BY Value) AS rn2
FROM CTE_Values
)
SELECT
MIN(Value) AS [Start]
,MAX(Value) + 1 AS [End]
,Type1
,Type2
FROM CTE_Groups
GROUP BY rn-rn2, Type1, Type2
ORDER BY [Start];
Result
+-------+-----+-------+-------+
| Start | End | Type1 | Type2 |
+-------+-----+-------+-------+
| 0 | 2 | L | NULL |
| 2 | 8 | L | S |
| 8 | 10 | L | NULL |
| 11 | 12 | NULL | S |
| 15 | 20 | L | S |
+-------+-----+-------+-------+
A step-by-step way is:
-- Finding all break points
;WITH breaks AS (
SELECT Start
FROM yourTable
UNION
SELECT [End]
FROM yourTable
) -- Finding Possible Ends
, ends AS (
SELECT Start
, (SELECT Min([End]) FROM yourTable WHERE yourTable.Start = breaks.Start) End1
, (SELECT Max([End]) FROM yourTable WHERE yourTable.Start < breaks.Start) End2
FROM breaks
) -- Finding periods
, periods AS (
SELECT Start,
CASE
WHEN End1 > End2 And End2 > Start THEN End2
WHEN End1 IS NULL THEN End2
ELSE End1
END [End]
FROM Ends
WHERE NOT(End1 IS NULL AND Start = End2)
) -- Generating results
SELECT p.Start, p.[End], Max(Type1) Type1, Max(Type2) Type2
FROM periods p, yourTable t
WHERE p.start >= t.Start AND p.[End] <= t.[End]
GROUP BY p.Start, p.[End];
In above query some situations may not fit at analyzing all of them, you can improve it as you want ;).
First getting all the numbers of start and end via a Union.
Then joining those numbers on both the 'L' and 'S' records.
Uses a table variable for the test.
DECLARE #Table1 TABLE (Start int, [End] int, Type1 varchar(4), Type2 varchar(4));
INSERT INTO #Table1 (Start, [End], Type1, Type2)
VALUES (0, 2, 'L', NULL),(2, 3, NULL, 'S'),(2, 5, 'L', NULL),(3, 5, NULL, 'S'),
(5, 7, 'L', NULL),(5, 8, NULL, 'S'),(7, 10, 'L', NULL),(11, 12, NULL, 'S');
select
n.Num as Start,
(case when s.[End] is null or l.[End] <= s.[End] then l.[End] else s.[End] end) as [End],
l.Type1,
s.Type2
from
(select Start as Num from #Table1 union select [End] from #Table1) n
left join #Table1 l on (n.Num >= l.Start and n.Num < l.[End] and l.Type1 = 'L')
left join #Table1 s on (n.Num >= s.Start and n.Num < s.[End] and s.Type2 = 'S')
where (l.Start is not null or s.Start is not null)
order by Start, [End];
Output:
Start End Type1 Type2
0 2 L NULL
2 3 L S
3 5 L S
5 7 L S
7 8 L S
8 10 L NULL
11 12 NULL S