Get ROWS as COLUMNS (SQL Server dynamic PIVOT query) - sql-server

I'm using MS SQL 2008 R2, have three tables with following schema:
Table 1: Contains workshift info for each worker
CREATE TABLE workshift (
[ws_id] [bigint] NOT NULL,
[start_date] [datetime] NOT NULL,
[end_date] [datetime] NOT NULL,
[worker_id] [bigint] NOT NULL
)
INSERT INTO workshift VALUES (1, '2012-08-20 08:30:00', '2012-08-20 14:30:00', 1)
INSERT INTO workshift VALUES (2, '2012-08-20 14:30:00', '2012-08-20 22:30:00', 2)
Table 2: Contains monetary denominations
CREATE TABLE currency_denom (
[cd_id] [decimal](7, 2) NOT NULL,
[name] [nchar](100) NOT NULL
)
INSERT INTO currency_denom VALUES (1, '100.00')
INSERT INTO currency_denom VALUES (2, '50.00')
INSERT INTO currency_denom VALUES (3, '20.00')
INSERT INTO currency_denom VALUES (4, '10.00')
INSERT INTO currency_denom VALUES (5, '5.00')
INSERT INTO currency_denom VALUES (6, '1.00')
Table 3: Contains the quantity of each denomination the worker has received in every workshift
CREATE TABLE currency_by_workshift (
[cd_id] [decimal](7, 2) NOT NULL,
[ws_id] [bigint] NOT NULL,
[qty] [int] NOT NULL
)
INSERT INTO currency_by_workshift VALUES (1, 1, 1)
INSERT INTO currency_by_workshift VALUES (2, 1, 2)
INSERT INTO currency_by_workshift VALUES (3, 1, 2)
INSERT INTO currency_by_workshift VALUES (2, 2, 3)
INSERT INTO currency_by_workshift VALUES (4, 2, 4)
INSERT INTO currency_by_workshift VALUES (5, 2, 2)
I need to get the currency_by_workshift values in columns instead of rows, along with the workshift values, that is:
workshift | workshift | workshift | 100.00 | 50.00 | 20.00 | 10.00 | 5.00 | 1.00
ws_id | start_date | end_date | | | | | |
1 | 2012-08-20 08:30:00 | 2012-08-20 14:30:00 | 1 | 2 | 2 | 0 | 0 | 0
2 | 2012-08-20 14:30:00 | 2012-08-20 22:30:00 | 0 | 2 | 0 | 4 | 2 | 0
I'm not able to use a case to count quantities for each currency denomination because they are configurable, if a new denomination is added, the query should be modified. Same applies if using PIVOT function, or I'm wrong?
How can I get the info that way?

What you are trying to do is called a PIVOT. There are two ways to do this, either with a Static Pivot or a Dynamic Pivot.
Static Pivot - is where you will hard-code the values of the rows to transform to columns (See SQL Fiddle with Demo):
select ws_id,
start_date,
end_date,
IsNull([100.00], 0) [100.00],
IsNull([50.00], 0) [50.00],
IsNull([20.00], 0) [20.00],
IsNull([10.00], 0) [10.00],
IsNull([5.00], 0) [5.00],
IsNull([1.00], 0) [1.00]
from
(
select ws.ws_id,
ws.start_date,
ws.end_date,
cd.name,
cbw.qty
from workshift ws
left join currency_by_workshift cbw
on ws.ws_id = cbw.ws_id
left join currency_denom cd
on cbw.cd_id = cd.cd_id
) x
pivot
(
sum(qty)
for name in ([100.00], [50.00], [20.00], [10.00], [5.00], [1.00])
) p
Dynamic pivot is where the columns are determined at run-time (see SQL Fiddle with Demo):
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#colsPivot AS NVARCHAR(MAX)
select #colsPivot =
STUFF((SELECT ', IsNull(' + QUOTENAME(rtrim(name)) +', 0) as ['+ rtrim(name)+']'
from currency_denom
GROUP BY name
ORDER BY cast(name as decimal(10, 2)) desc
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #cols = STUFF((SELECT distinct ', ' + QUOTENAME(name)
from currency_denom
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query
= 'SELECT ws_id, start_date, end_date,' + #colsPivot + ' from
(
select ws.ws_id,
ws.start_date,
ws.end_date,
cd.name,
cbw.qty
from workshift ws
left join currency_by_workshift cbw
on ws.ws_id = cbw.ws_id
left join currency_denom cd
on cbw.cd_id = cd.cd_id
) x
pivot
(
sum(qty)
for name in (' + #cols + ')
) p '
execute(#query)
Both versions will produce the same results.

#bluefeet provided a very good answer utilizing the built in PIVOT functionality. However, I frequently find the PIVOT and UNPIVOT nomenclature confusing and I have yet to encounter a situation where the same results can't be achieved with standard aggregations:
select w.ws_id, w.start_date, w.end_date,
[100.00] = isnull(sum(case when c.name='100.00' then cw.qty else null end), 0),
[50.00] = isnull(sum(case when c.name='50.00' then cw.qty else null end), 0),
[20.00] = isnull(sum(case when c.name='20.00' then cw.qty else null end), 0),
[10.00] = isnull(sum(case when c.name='10.00' then cw.qty else null end), 0),
[5.00] = isnull(sum(case when c.name='5.00' then cw.qty else null end), 0),
[1.00] = isnull(sum(case when c.name='1.00' then cw.qty else null end), 0)
from workshift w
join currency_by_workshift cw on w.ws_id=cw.ws_id
join currency_denom c on cw.cd_id=c.cd_id
group by w.ws_id, w.start_date, w.end_date
If you want to do a dynamic pivot, you only need to build a string of the pivot columns once:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols =
stuff(( select replace(',[#name] = isnull(sum(case when c.name=''#name'' then cw.qty else null end), 0)'
, '#name', rtrim(name))
from currency_denom
order by cd_id
for xml path(''), type
).value('.', 'nvarchar(max)')
,1,1,'')
select #query = '
select w.ws_id, w.start_date, w.end_date, '+#cols+'
from workshift w
join currency_by_workshift cw on w.ws_id=cw.ws_id
join currency_denom c on cw.cd_id=c.cd_id
group by w.ws_id, w.start_date, w.end_date
'
execute(#query)

Related

sqlserver get records count repeating

I have one doubt in sql server
get records based on installments
Table :productdetails
CREATE TABLE [dbo].[productdetails](
[productid] [int] NULL,
[Productrstartdate] [date] NULL,
[Productenddate] [date] NULL,
[EMIInstallment] [int] NULL
) ON [PRIMARY]
GO
INSERT [dbo].[productdetails] ([productid], [Productrstartdate], [Productenddate], [EMIInstallment]) VALUES (1, CAST(N'2020-10-02' AS Date), CAST(N'2024-10-02' AS Date), 5)
GO
INSERT [dbo].[productdetails] ([productid], [Productrstartdate], [Productenddate], [EMIInstallment]) VALUES (2, CAST(N'2020-02-10' AS Date), CAST(N'2021-02-10' AS Date), 2)
GO
INSERT [dbo].[productdetails] ([productid], [Productrstartdate], [Productenddate], [EMIInstallment]) VALUES (3, CAST(N'2019-01-10' AS Date), CAST(N'2019-01-10' AS Date), 1)
GO
INSERT [dbo].[productdetails] ([productid], [Productrstartdate], [Productenddate], [EMIInstallment]) VALUES (4, CAST(N'2019-01-18' AS Date), CAST(N'2021-01-18' AS Date), 3)
GO
based on above data i want output like below
Productid |Installmentdate |noofinstallmentcount
1 |2020-10-02 |1
1 |2021-10-02 |2
1 |2022-10-02 |3
1 |2023-10-02 |4
1 |2024-10-02 |5
2 |2020-02-10 |1
2 |2021-02-10 |2
3 |2019-01-10 |1
4 |2019-01-18 |1
4 |2020-01-18 |2
4 |2021-01-18 |3
i tried like below :
DECLARE #MINDATE DATE='2019-01-18'
DECLARE #COUNT INT=10
dECLARE #MAXDATE DATE='2024-10-02'
;WITH ABC
AS
(
SELECT productid ,#MINDATE CalendarDate ,1 as id from [dbo].[productdetails]
UNION ALL
SELECT a.productid ,DATEADD(YEAR,1,a.Productrstartdate ), 1 FROM [dbo].[productdetails] a
join [dbo].[productdetails] b on a.productid=b.productid
WHERE #MINDATE <#MAXDATE )
SELECT * FROM ABC
above query not given expected output
could you please tell me how to write query to achive this task in sql server .
You want a tally here. If you're not going to have a value (much) higher than 10, then just a VALUES clause will work:
SELECT pd.productid,
DATEADD(YEAR, V.I, pd.Productrstartdate) AS Installmentdate,
V.I + 1 AS noofinstallmentcount
FROM dbo.productdetails pd
JOIN (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9))V(I) ON pd.EMIInstallment > V.I;
If you need bigger numbers, just use a bigger tally:
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3) --1000 rows
SELECT pd.productid,
DATEADD(YEAR, T.I, pd.Productrstartdate) AS Installmentdate,
T.I + 1 AS noofinstallmentcount
FROM dbo.productdetails pd
JOIN Tally T ON pd.EMIInstallment > T.I;
You could create a temp table that mimics the actual productdetails table, then iterate through it, creating an output table that lists the product id with each installment date. Once you have that, you can use ROW_NUMBER() to count the installment numbers. Try this:
SELECT *
INTO #productdetails
FROM productdetails
CREATE TABLE #output (
productid int null
,installmentdate date null)
WHILE (SELECT COUNT(*) FROM #productdetails) > 0
BEGIN
DECLARE #yr int = 0
WHILE (SELECT TOP 1 DATEDIFF(year, DATEADD(year, #yr, Productrstartdate), productenddate) FROM #productdetails) >= 0
BEGIN
INSERT INTO #output (
productid
,installmentdate)
SELECT top 1
productid
,DATEADD(year, #yr, Productrstartdate) installmentdate
FROM #productdetails
SET #yr = #yr + 1
END
DELETE TOP (1) FROM #productdetails
END
SELECT *
,ROW_NUMBER() OVER (PARTITION BY ProductId ORDER BY InstallmentDate asc) NumberOfInstallmentCount
FROM #output
DROP TABLE #output, #productdetails

how to remove the duplicate rows when we used group by method?

i used the group by the method for distinct the two column values. But still i got the duplicate values in the column rows.
SELECT MRD_NO,RESOURCE_NAME,
Diagnosis =
STUFF((SELECT DISTINCT ', ' +
(case when Diagnosis is null and OTHER_DIAGONSIS is not null then OTHER_DIAGONSIS else Diagnosis end)
FROM EMR_master b
WHERE b.MRD_NO = a.MRD_NO
FOR XML PATH('')), 1, 2, '')
FROM EMR_master a
where
a.TREATMENT_CODE in ('CC','PO','SRE','REG')
group by MRD_NO,RESOURCE_NAME
order by RESOURCE_NAME,MRD_NO
my output contains the duplicate values in the MRD_NO column, tell me how to remove the duplicate values.
my output
MRD_NO | RESOURCE_NAME | Diagnosis
123 | james | retina
126 | peter | throat pain
129 | Murugan | fever
129 | william | fever
130 | william | retina
i need like this output
MRD_NO | RESOURCE_NAME | Diagnosis
123 | james | retina
126 | peter | throat pain
129 | Murugan | fever
130 | william | retina
note: i got duplicates MRD_NO 129 with two resource name(Murugan,william),
so i need to eliminate the william and get unique MRD_NO
If you don't want to much work around and getting desired output you can do with temptable
SELECT MRD_NO,RESOURCE_NAME,
STUFF((SELECT DISTINCT ', ' +
(case when Diagnosis is null and OTHER_DIAGONSIS is not null then OTHER_DIAGONSIS else Diagnosis end) as 'Diagnosis'
into #tmpEmrDetail
FROM EMR_master b
WHERE b.MRD_NO = a.MRD_NO
FOR XML PATH('')), 1, 2, '')
FROM EMR_master a
where
a.TREATMENT_CODE in ('CC','PO','SRE','REG')
group by MRD_NO,RESOURCE_NAME
order by RESOURCE_NAME,MRD_NO
select distinct * from #tmpEmrDetail
drop table #tmpEmrDetail
or
; WITH ctetbl AS
(
SELECT MRD_NO,RESOURCE_NAME,
Diagnosis =
STUFF((SELECT DISTINCT ', ' +
(case when Diagnosis is null and OTHER_DIAGONSIS is not null then OTHER_DIAGONSIS else Diagnosis end)
FROM EMR_master b
WHERE b.MRD_NO = a.MRD_NO
FOR XML PATH('')), 1, 2, '')
FROM EMR_master a
where
a.TREATMENT_CODE in ('CC','PO','SRE','REG')
group by MRD_NO,RESOURCE_NAME
)
SELECT *
FROM ctetbl order by RESOURCE_NAME,MRD_NO
Try to choose a column in DISTINCT operator:
SELECT MRD_NO,RESOURCE_NAME,
Diagnosis =
STUFF((SELECT DISTINCT b.MRD_NO + ', ' +
(case
when Diagnosis is null and OTHER_DIAGONSIS is not null
then OTHER_DIAGONSIS else Diagnosis end)
FROM EMR_master b
WHERE b.MRD_NO = a.MRD_NO
GROUP BY b.MRD_NO, b.RESOURCE_NAME
FOR XML PATH('')), 1, 2, '')
FROM EMR_master a
where
a.TREATMENT_CODE in ('CC','PO','SRE','REG')
group by MRD_NO,RESOURCE_NAME
order by RESOURCE_NAME,MRD_NO
E.g.:
DECLARE #table TABLE
(
EmpID int,
EmpName varchar(50),
DateOfJoin datetime,
DateOfLeaving DATETIME,
StatusName VARCHAR(50)
)
INSERT INTO #table
(
EmpID,
EmpName,
DateOfJoin,
DateOfLeaving,
StatusName
)
VALUES
(1, 'XYZ', '2015-10-01', '2017-09-26', 'De-ACTIVE')
,(2, 'ABC', '2018-01-01', NULL, 'ACTIVE')
,(3, 'XYZ', '2018-10-15', NULL, 'ACTIVE')
and query:
SELECT
hey = STUFF((
SELECT
DISTINCT t.EmpName + ', '
FROM #table t
FOR XML PATH('')), 1, 2, '')
FROM #table t
OUTPUT:
hey
C, XYZ,
C, XYZ,
C, XYZ,
UPDATE:
If you would like just to remove duplicates, then you should just use GROUP BY to field where you want to get rid of duplicates.
SELECT
MRD_NO
, RESOURCE_NAME
, Diagnosis
FROM YourTable
GROUP BY MRD_NO
, RESOURCE_NAME
, Diagnosis

SQL concat integers and group them with from to

I'm new to stackoverflow, but I'm stuck with my query.
I've got a SQL table whitch looks like this:
+-------+------------+
| col1 | col2 |
+-------+------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 4 |
| 1 | 6 |
+-------+------------+
I don't know how to get the following resultset:
+-------+------------+
| col1 |SerialNumber|
+-------|------------+
| 1 | 1 to 4, 6 |
+--------------------+
With XML Path i can get this:
+-------+------------+
| col1 |SerialNumber|
+-------|------------+
| 1 | 1,2,3,4,6, |
+--------------------+
This is my query for it:
SELECT DISTINCT O.Col1,
(SELECT CAST(P.Col2 As varchar(5)) + ',' AS [text()]
FROM #Test P
WHERE P.Col1 = O.Col1
ORDER BY P.Col1
FOR XML PATH('')) AS 'SerialNumber'
FROM #Test O
I'm sorry if my question was already asked. I'm also lacking Keywords for this topic.
Test data:
CREATE TABLE t(col1 int,col2 int)
INSERT t(col1,col2)VALUES
(1,1),(1,2),(1,3),(1,4),
(1,6),(1,7),(1,8),(1,9),
(1,11),
(1,13),
(2,3),(2,4),(2,5),
(2,7)
A variant with FOR XML PATH:
SELECT col1,col2,outVal
INTO #temp
FROM
(
SELECT
col1,
col2,
outVal,
ISNULL(LEAD(outVal)OVER(PARTITION BY col1 ORDER BY col2),'') nextOutVal
FROM
(
SELECT
col1,
col2,
CASE
WHEN col2-1=LAG(col2)OVER(PARTITION BY col1 ORDER BY col2) AND col2+1=LEAD(col2)OVER(PARTITION BY col1 ORDER BY col2)
THEN 'to'
ELSE CAST(col2 AS varchar(10))
END outVal
FROM t
) q
) q
WHERE outVal<>nextOutVal
ORDER BY col1,col2
SELECT
t1.col1,
REPLACE(STUFF(
(
SELECT ','+t2.outVal
FROM #temp t2
WHERE t2.col1=t1.col1
ORDER BY t2.col2
FOR XML PATH('')
),1,1,''),',to,',' to ') SerialNumber
FROM (SELECT DISTINCT col1 FROM #temp) t1
DROP TABLE #temp
A variant for SQL Server 2017 (with STRING_AGG):
SELECT
col1,
REPLACE(STRING_AGG(outVal,',')WITHIN GROUP(ORDER BY col2),',to,',' to ')
FROM
(
SELECT
col1,
col2,
outVal,
ISNULL(LEAD(outVal)OVER(PARTITION BY col1 ORDER BY col2),'') nextOutVal
FROM
(
SELECT
col1,
col2,
CASE
WHEN col2-1=LAG(col2)OVER(PARTITION BY col1 ORDER BY col2) AND col2+1=LEAD(col2)OVER(PARTITION BY col1 ORDER BY col2)
THEN 'to'
ELSE CAST(col2 AS varchar(10))
END outVal
FROM t
) q
) q
WHERE outVal<>nextOutVal
GROUP BY col1
Result:
col1 SerialNumber
1 1 to 4,6 to 9,11,13
2 3 to 5,7
Solution:
Another possible approach using CTE for start and end values for each sequence and group concatenation:
T-SQL:
-- Table creation
CREATE TABLE #ValuesTable (
Col1 int,
Col2 int
)
INSERT INTO #ValuesTable VALUES (1, 1)
INSERT INTO #ValuesTable VALUES (1, 2)
INSERT INTO #ValuesTable VALUES (1, 3)
INSERT INTO #ValuesTable VALUES (1, 4)
INSERT INTO #ValuesTable VALUES (1, 6)
INSERT INTO #ValuesTable VALUES (2, 1)
INSERT INTO #ValuesTable VALUES (2, 2)
INSERT INTO #ValuesTable VALUES (2, 3)
INSERT INTO #ValuesTable VALUES (2, 4)
INSERT INTO #ValuesTable VALUES (2, 6)
INSERT INTO #ValuesTable VALUES (2, 7);
INSERT INTO #ValuesTable VALUES (2, 10);
-- Find sequences
WITH
TableStart AS (
SELECT t.Col1, t.Col2, ROW_NUMBER() OVER (ORDER BY t.Col1, t.Col2) AS RN
FROM #ValuesTable t
LEFT JOIN #ValuesTable b ON (t.Col1 = b.Col1) AND (t.Col2 = b.Col2 + 1)
WHERE (b.Col2 IS NULL)
),
TableEnd AS (
SELECT t.Col1, t.Col2, ROW_NUMBER() OVER (ORDER BY t.Col1, t.Col2) AS RN
FROM #ValuesTable t
LEFT JOIN #ValuesTable b ON (t.Col1 = b.Col1) AND (t.Col2 = b.Col2 - 1)
WHERE (b.Col2 IS NULL)
),
TableSequences AS (
SELECT
TableStart.Col1 AS Col1,
CASE
WHEN (TableStart.Col2 <> TableEnd.Col2) THEN CONVERT(nvarchar(max), TableStart.Col2) + N' to ' + CONVERT(nvarchar(max), TableEnd.Col2)
ELSE CONVERT(nvarchar(max), TableStart.Col2)
END AS Sequence
FROM TableStart
LEFT JOIN TableEnd ON (TableStart.RN = TableEnd.RN)
)
-- Select with group concatenation
SELECT
t1.Col1,
(
SELECT t2.Sequence + N', '
FROM TableSequences t2
WHERE t2.Col1 = t1.Col1
ORDER BY t2.Col1
FOR XML PATH('')
) SerialNumber
FROM (SELECT DISTINCT Col1 FROM TableSequences) t1
Output:
Col1 SerialNumber
1 1 to 4, 6,
2 1 to 4, 6 to 7, 10,
Notes:
Tested on SQL Server 2005, 2012, 2016.

Tree data to flat data in SQL Server

I am facing the problem tree view to flat view, I have some data that has come from a tree, those are stored in a table that is presenting in my picture below left. I am saying the top level of tree is level1, and second level is level2 and so on. My expected result that is representing in my picture below right.
How can I convert my data to my expected result dynamically in SQL Server? We have to create dynamic column level1, level2, level3 and so on. Do you have any idea? Thanks.
Here is the sample data
IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL
DROP TABLE #TestData;
CREATE TABLE #TestData (
Id INT NOT NULL,
SomeName VARCHAR(3) NOT NULL,
ParentId INT NULL
);
INSERT #TestData (Id, SomeName, ParentId) VALUES
(1, 'O', NULL),
(2, 'D1', 1),
(3, 'D2', 1),
(4, 'S1', 2),
(5, 'S2', 2),
(6, 'S1', 3),
(7, 'SP1', 3);
SELECT * FROM #TestData td;
You can query as below:
;With Cte as ( --Recursive CTE for traversing tree
Select *, convert(varchar(max),[name]) as NameLevel, 1 as Levl from #TreeData where ParentId is Null
Union all
Select t.Id, t.[Name], t.[ParentId], concat(c.NameLevel,',',t.[name]) as NameLevel, c.Levl + 1 as Levl from Cte c
inner join #TreeData t on c.Id = t.ParentId
)
Select * from (
select c.Id, c.Levl, a.[value]
,RowN = row_number() over(partition by Id order by Levl) from cte c
cross apply string_split(c.NameLevel,',') a
) sq
pivot(max([value]) for RowN in([1],[2],[3])) p --Pivot for getting all data
Output as below:
+---+------+------+----+
| 1 | 2 | 3 | Id |
+---+------+------+----+
| O | NULL | NULL | 1 |
| O | D1 | NULL | 2 |
| O | D2 | NULL | 3 |
| O | D1 | S1 | 4 |
| O | D1 | S2 | 5 |
| O | D2 | S1 | 6 |
| O | D2 | SP1 | 7 |
+---+------+------+----+
Below the input table and data I used:
Create Table #TreeData(Id int, [name] varchar(10), ParentId int)
Insert into #TreeData(id, [name], ParentId) values
(1,'O', null)
,(2,'D1', 1)
,(3,'D2', 1)
,(4,'S1', 2)
,(5,'S2', 2)
,(6,'S1', 3)
,(7,'SP1', 3)
IF OBJECT_ID('tempdb..#TreeData', 'U') IS NOT NULL
DROP TABLE #TreeData;
CREATE TABLE #TreeData (
Id INT NOT NULL,
SomeName VARCHAR(3) NOT NULL,
ParentId INT NULL
);
INSERT #TreeData (Id, SomeName, ParentId) VALUES
(1, 'O', NULL),
(2, 'D1', 1),
(3, 'D2', 1),
(4, 'S1', 2),
(5, 'S2', 2),
(6, 'S1', 3),
(7, 'SP1', 3);
--SELECT * FROM #TestData td;
;With Cte as ( --Recursive CTE for traversing tree
Select Id,SomeName,ParentId, convert(varchar(max),[SomeName]) as NameLevel, 1 as Levl from #TreeData where ParentId is Null
Union all
Select t.Id, t.[SomeName], t.[ParentId], (c.NameLevel +','+ t.[SomeName]) as NameLevel, c.Levl + convert(int, 1) as Levl
from Cte c
inner join #TreeData t on c.Id = t.ParentId
)
--select * from cte
Select * from (
select c.Id, c.Levl, a.Items
,RowN = row_number() over(partition by Id order by Levl) from cte c
cross apply split(c.NameLevel,',') a
) sq
pivot(max([Items]) for RowN in([1],[2],[3])) p --Pivot for getting all data
The following should give you the requested results.
Note: This solution relies on the use of an iTVF called tfn_Tally, I'll post the code for that below my answer.
IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL
DROP TABLE #TestData;
CREATE TABLE #TestData (
Id INT NOT NULL,
SomeName VARCHAR(3) NOT NULL,
ParentId INT NULL
);
INSERT #TestData (Id, SomeName, ParentId) VALUES
(1, 'O', NULL),
(2, 'D1', 1),
(3, 'D2', 1),
(4, 'S1', 2),
(5, 'S2', 2),
(6, 'S1', 3),
(7, 'SP1', 3);
-- SELECT * FROM #TestData td;
--===========================================
--===========================================
IF OBJECT_ID('tempdb..#RecursionResults', 'U') IS NOT NULL
DROP TABLE #RecursionResults;
WITH
cte_Recursion AS (
SELECT
td.Id,
SomeName = CAST(CAST(td.SomeName AS BINARY(5)) AS VARBINARY(1000)),
--td.ParentId,
NodeLevel = 1
FROM
#TestData td
WHERE
td.ParentId IS NULL
UNION ALL
SELECT
td.Id,
SomeName = CAST(CONCAT(r.SomeName, CAST(td.SomeName AS BINARY(5))) AS VARBINARY(1000)),
NodeLevel = r.NodeLevel + 1
FROM
cte_Recursion r
JOIN #TestData td
ON r.Id = td.ParentId
)
SELECT
Id = ISNULL(r.Id, 0), r.SomeName, r.NodeLevel
INTO #RecursionResults
FROM
cte_Recursion r;
-- adding a clustered index Id eliminates the sort operation on final select.
ALTER TABLE #RecursionResults ADD PRIMARY KEY CLUSTERED (Id);
-----------------------------------------
DECLARE
#PivotCount INT = (SELECT MAX(rr.NodeLevel) FROM #RecursionResults rr),
#PivotCols VARCHAR(1000) = '',
#PivotCAV VARCHAR(8000) = '',
#sql VARCHAR(8000),
#Debug BIT = 0; -- set to 0 to execute, 1 to DeBug.
SELECT TOP (#PivotCount)
#PivotCols = CONCAT(#PivotCols, CHAR(13), CHAR(10), CHAR(9), 'L', t.n, '.Level_', t.n, ','),
#PivotCAV = CONCAT(#PivotCAV, CHAR(13), CHAR(10), CHAR(9), 'CROSS APPLY ( VALUES (CAST(SUBSTRING(rr.SomeName,', (t.n - 1) * 5 + 1, ', ', 5, ') AS VARCHAR(5))) ) L', t.n, ' (Level_', t.n, ')'
)
FROM
dbo.tfn_Tally(#PivotCount, 1) t;
SET #sql = CONCAT('
SELECT ',
STUFF(#PivotCols, 1, 1, ''), '
rr.Id
FROM
#RecursionResults rr',
#PivotCAV, '
ORDER BY
rr.Id;');
IF #Debug = 1
BEGIN
PRINT(#sql);
END;
ELSE
BEGIN
EXEC (#sql);
END;
Function code for tfn_Tally...
CREATE FUNCTION dbo.tfn_Tally
/* ============================================================================
07/20/2017 JL, Created. Capable of creating a sequense of rows
ranging from -10,000,000,000,000,000 to 10,000,000,000,000,000
============================================================================ */
(
#NumOfRows BIGINT,
#StartWith BIGINT
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
WITH
cte_n1 (n) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (n)), -- 10 rows
cte_n2 (n) AS (SELECT 1 FROM cte_n1 a CROSS JOIN cte_n1 b), -- 100 rows
cte_n3 (n) AS (SELECT 1 FROM cte_n2 a CROSS JOIN cte_n2 b), -- 10,000 rows
cte_n4 (n) AS (SELECT 1 FROM cte_n3 a CROSS JOIN cte_n3 b), -- 100,000,000 rows
cte_Tally (n) AS (
SELECT TOP (#NumOfRows)
(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1) + #StartWith
FROM
cte_n4 a CROSS JOIN cte_n4 b -- 10,000,000,000,000,000 rows
)
SELECT
t.n
FROM
cte_Tally t;
GO
HTH,
Jason

split record in multiple records on start/end date

I'm looking for a solution where I have to create a set of records from one record using data from another table. The table definition:
DECLARE A AS TABLE
(
AID BIGINT NOT NULL,
StartDate DATETIME NOT NULL,
EndDate DATETIME
)
DECLARE B AS TABLE
(
AID BIGINT NOT NULL,
StartDate DATETIME NOT NULL,
EndDate DATETIME NULL
)
The idea is that when A contains:
1 | 01-01-2010 | 01-02-2010
2 | 01-10-2010 | 31-10-2010
and B contains:
1 | 01-01-2010 | 15-01-2010
2 | 15-10-2010 | 20-10-2010
we recieve 5 records:
1 | 01-01-2010 | 15-01-2010
1 | 16-01-2010 | 01-02-2010
2 | 01-10-2010 | 15-10-2010
2 | 16-10-2010 | 20-10-2010
2 | 21-10-2010 | 31-10-2010
Currently we do this with a cursor on A and an inner loop cursor on B, we have to do this in SQLServer (TSQL or in worst case CLR)
Any ideas on how to write this as a select so that the overhead of the cursor disappears?
DECLARE #A TABLE (AID BIGINT NOT NULL, StartDate DATETIME NOT NULL, EndDate DATETIME)
DECLARE #B TABLE (AID BIGINT NOT NULL, StartDate DATETIME NOT NULL, EndDate DATETIME NULL)
SET DATEFORMAT dmy
INSERT #A VALUES (1 ,'01-01-2010','01-02-2010')
INSERT #A VALUES (2 ,'01-10-2010','31-10-2010')
INSERT #B VALUES (1 ,'01-01-2010','15-01-2010')
INSERT #B VALUES (2 ,'15-10-2010','20-10-2010')
;WITH transitions as
(
SELECT *, ROW_NUMBER() OVER (PARTITION BY AID ORDER BY startdate) Sequence
FROM (
SELECT A.AID, A.startdate
FROM #a A
UNION
SELECT A.AID, B.startdate + 1
FROM #A A
INNER JOIN #b B ON B.startdate > A.startdate AND B.startdate < A.enddate
UNION
SELECT A.AID, B.enddate + 1
FROM #A A
INNER JOIN #b B ON B.enddate > A.startdate AND B.enddate < A.enddate
UNION
SELECT A.AID, A.enddate + 1
FROM #a A
WHERE A.enddate > A.startdate
) T
)
SELECT T1.AID, T1.startdate startdate, T2.startdate - 1 enddate
FROM transitions T1
INNER JOIN transitions T2 ON T2.AID = T1.AID AND T2.Sequence = T1.Sequence + 1
ORDER BY T1.AID, T1.Sequence

Resources