TSQL - How to avoid UNION ALL - sql

Sample Data:
DECLARE #Parent TABLE
(
[Id] INT
, [Misc_Val] VARCHAR(5)
) ;
DECLARE #Children TABLE
(
[Id] INT
, [P_ID] INT
) ;
INSERT INTO #Parent
VALUES
( 1, 'One' )
, ( 2, 'Two' )
, ( 3, 'Three' )
, ( 5, 'Four' ) ;
INSERT INTO #Children
VALUES
( 10, 1 )
, ( 11, 1 )
, ( 21, 2 )
, ( 23, 2 )
, ( 30, 3 )
, ( 40, 4 ) ;
Goal:
To efficiently output three fields ( [Id] and [IsChild], [Misc_Val] ). Output all records from #Parent table with [IsChild] = 0 and output all MATCHING records from #Child table (#Parent.Id = #Children.P_Id) with [IsChild] = 1.
Expected Output
Id IsChild Misc_Val
1 0 One
2 0 Two
3 0 Three
5 0 Four
10 1 One
11 1 One
21 1 Two
23 1 Two
30 1 Three
My try:
SELECT [P].[Id]
, 0 AS [IsChild]
, [P].[Misc_Val]
FROM #Parent AS [P]
UNION ALL
SELECT [C].[Id]
, 1
, [P].[Misc_Val]
FROM #Parent AS [P]
JOIN #Children AS [C]
ON [C].[P_ID] = [P].[Id] ;
Is there a better way to do this than using UNION ALL? #Parent and #Children tables are quite big and so am trying to avoid querying the #Parent table twice.
UPDATE: The below answer made me realized something I missed out when creating the post with mocked data. We do need some additional data from #Parent table regardless in the final output.

You can use CROSS APPLY to add the child table to the parent table.
This may or may not be faster, it can depend on indexing and so forth. You need to check the query plan.
SELECT v.Id
, v.IsChild
, P.Misc_Val
FROM #Parent AS P
CROSS APPLY (
SELECT
P.Id,
0 AS IsChild
UNION ALL
SELECT
C.Id,
1
FROM #Children AS C
WHERE C.P_ID = P.Id
) v;
Note that the first SELECT in the apply has no FROM and therefore does not do any table access.

Related

Assign a unique ID to groups of rows that connect over 2 columns in SQL [duplicate]

This question already has answers here:
Group All Related Records in Many to Many Relationship, SQL graph connected components
(6 answers)
Closed 1 year ago.
My situation is the following:
I've a table with 2 ID-Columns and I want to assign a unique ID to rows that link by either one.
Here is an example of 6 rows in my table. All of these rows need to get the same unique ID.
Row
ID1
ID2
1
A
1
2
A
2
3
B
2
4
B
3
5
C
3
6
C
4
Rows 1 and 2 need to get the same unique id because they have the same ID1.
Row 3 needs to get that as well because its ID2 matches the ID2 of row 2.
Row 4 needs to get that as well because its ID1 matches the ID1 of row 3.
Row 5 needs to get that as well because its ID2 matches the ID2 of row 4.
Row 6 needs to get that as well because its ID1 matches the ID1 of row 5.
Basically the two columns form a chain and I want to assign an ID to that chain.
Is there some reasonably efficient way to do this in SQL?
Okay. This is really clumsy solution (and I should probably hand in my SQL Server badge for even suggesting it), but I think it'll get you over the line. I just hope you're not running this on a very large data set.
First up, I created some dummy temp tables to mimic your data (plus a few extra rows):
DROP TABLE IF EXISTS #Tbl1 ;
CREATE TABLE #Tbl1
(
[id] TINYINT NOT NULL
, [ID1] CHAR(1) NOT NULL
, [ID2] TINYINT NOT NULL
) ;
INSERT
INTO #Tbl1 ( [id], [ID1], [ID2] )
VALUES ( 1, 'A', 1 ), ( 2, 'A', 2 ), ( 3, 'B', 2 )
, ( 4, 'B', 3 ), ( 5, 'C', 3 ), ( 6, 'C', 4 )
, ( 7, 'D', 5 ), ( 8, 'D', 5 ), ( 9, 'E', 6 ) ;
Then, using a CTE and the LAG function, I identified which rows would see an increment of a unique identifier, and dumped all this in temp table:
DROP TABLE IF EXISTS #Tbl2 ;
WITH cte_Lags AS
(
SELECT [id]
, [ID1]
, LAG ( [ID1], 1, '' )
OVER ( ORDER BY [ID1] ASC, [ID2] ASC ) AS [ID1_lag]
, [ID2]
, LAG ( [ID2], 1, 0 )
OVER ( ORDER BY [ID1] ASC, [ID2] ASC ) AS [ID2_lag]
FROM #Tbl1
)
SELECT [id] AS [row]
, [ID1]
, [ID2]
, CASE
WHEN [ID1] = [ID1_lag]
OR [ID2] = [ID2_lag]
THEN 0
ELSE 1
END AS [incr_id]
INTO #Tbl2
FROM cte_Lags ;
I then add a column for your unique ID to the temp table:
ALTER TABLE #Tbl2 ADD [unique_id] TINYINT NULL ;
Now this is where it gets real messy!
I create a iterative loop that cycles through each row of the temp table and updates the unique_id column with the appropriate number, incremented only if the row is flagged to be incremented:
DECLARE #RowNum AS TINYINT ;
DECLARE #i AS TINYINT = 0 ;
WHILE ( ( SELECT COUNT(*) FROM #Tbl2 WHERE [unique_id] IS NULL ) > 0 )
BEGIN
SELECT TOP(1) #RowNum = [row]
FROM #Tbl2
WHERE [unique_id] IS NULL
ORDER BY [ID1] ASC, [ID2] ASC, [row] ASC ;
IF ( ( SELECT [incr_id] FROM #Tbl2 WHERE [row] = #RowNum ) = 1 )
SET #i += 1 ;
UPDATE #Tbl2
SET [unique_id] = #i
WHERE [row] = #RowNum ;
END
SELECT [row]
, [ID1]
, [ID2]
, [unique_id]
FROM #Tbl2
ORDER BY [ID1] ASC, [ID2] ASC ;
Now this all assumes that data doesn't repeat further down the table -- e.g. ('A', 1) is not going to reappear at row 50. If it does, this'll all need a little rethink.
I really hope someone cleverer than I can do this for you in a simple recursive CTE or a funky grouping function. But until then, this'll keep the boss happy.
A recursive CTE is useful for something like this.
Traverse through the records, then group the results.
WITH RCTE AS (
SELECT
[Row] AS BaseRow
, ID1 AS BaseID1
, ID2 AS BaseID2
, 1 AS Lvl
, [Row], ID1, ID2
FROM YourTable
UNION ALL
SELECT
c.BaseRow
, c.BaseID1
, c.BaseID2
, c.Lvl + 1
, t.[Row], t.ID1, t.ID2
FROM RCTE c
JOIN YourTable t
ON t.[Row] < c.[Row]
AND (t.ID2 = c.ID2 OR (t.ID2 < c.ID2 AND t.ID1 = c.ID1))
)
SELECT
BaseRow AS [Row]
, BaseID1 AS ID1
, MIN(ID2) AS ID2
FROM RCTE
GROUP BY BaseRow, BaseID1
ORDER BY BaseRow, BaseID1;
db<>fiddle here

Common Table Expression to traverse down hierarchy

The Structure
I have 2 tables that link to each other. One is a set of values and a nullable foreign key that points to the Id of the other table, which contains 2 foreign keys back to the other table.
HierarchicalTable
Id LeftId RightId SomeValue
1 1 2 some value
2 3 4 top level in tree
3 5 6 incorrect hierarchy 1
4 7 8 incorrect result top level
IntermediateTable
Id SomeValue HierarchicalTableId
1 some value NULL
2 value NULL
3 NULL 1
4 value NULL
5 incorrect result 1 NULL
6 incorrect result 3 NULL
7 incorrect result 3 NULL
8 NULL 3
Each table points down the hierarchy. Here is this structure graphed out for the Hierarchical Table records 1 & 2 and their IntermediateTable values:
(H : HierarchicalTable, I : IntermediateTable)
H-2
/ \
I-3 I-4
/
H-1
/ \
I-1 I-2
The Problem
I need to be able to send in an Id for a given HierarchicalTable and get all the HierarchicalTable records below it. So, for the structure above, if I pass 1 into a query, I should just get H-1 (and from that, I can load the related IntermediateTable values). If I pass 2, I should get H-2 and H-1 (and, again, use those to load the relevant IntermediateTable values).
The Attempts
I've tried using a CTE, but there are a few main things that are different from the examples I've seen:
In my structure, the objects point down to their children, instead of up to their parent
I have the Id of the top object, not the Id of the bottom object.
My hierarchy is split across 2 tables. This shouldn't be a big issue once I understand the algorithm to find the results I need, but this could be causing additional confusion for me.
If I run this query:
declare #TargetId bigint = 2
;
with test as (
select h.*
from dbo.hierarchicaltable h
inner join dbo.intermediatetable i
on (h.leftid = i.id or h.rightid = i.id)
union all
select h.*
from dbo.hierarchicaltable h
where h.id = #TargetId
)
select distinct *
from test
I get all 4 records in the HierarchicalTable, instead of just records 1 & 2. I'm not sure if what I want is possible to do with a CTE.
Try this:
I'm build entire tree with both tables, then filter (only hierarchicaltable records).
DECLARE #HierarchicalTable TABLE(
Id INT,
LeftId INT,
RightId INT,
SomeValue VARCHAR(MAX)
)
INSERT INTO #HierarchicalTable
VALUES
(1, 1, 2, 'some value '),
(2, 3, 4, 'top level in tree '),
(3, 5, 6, 'incorrect hierarchy 1 '),
(4, 7, 8, 'incorrect result top level')
DECLARE #IntermediateTable TABLE(
Id INT,
SomeValue VARCHAR(MAX),
HierarchicalTableId INT
)
INSERT INTO #IntermediateTable
VALUES
(1, 'some value' ,NULL ),
(2, 'value ' ,NULL ),
(3, NULL ,1 ),
(4, 'value ' ,NULL ),
(5, 'incorrect result 1' ,NULL ),
(6, 'incorrect result 3' ,NULL ),
(7, 'incorrect result 3' ,NULL ),
(8, NULL ,3 )
DECLARE #TargetId INT = 2;
WITH CTE AS (
SELECT Id AS ResultId, LeftId, RightId, NULL AS HierarchicalTableId
FROM #HierarchicalTable
WHERE Id = #TargetId
UNION ALL
SELECT C.Id AS ResultId, C.LeftId, C.RightId, NULL AS HierarchicalTableId
FROM #HierarchicalTable C
INNER JOIN CTE P ON P.HierarchicalTableId = C.Id
UNION ALL
SELECT NULL AS ResultId, NULL AS LeftId, NULL AS RightId, C.HierarchicalTableId
FROM #IntermediateTable C
INNER JOIN CTE P ON P.LeftId = C.Id OR P.RightId = C.Id
)
SELECT *
FROM CTE
WHERE ResultId IS NOT NULL

SQL Server - from two rows, one column to one row, two columns?

if object_id( 'tempdb.dbo.#ctp', 'u' ) is not null
drop table #ctp ;
create table #ctp( id int, mastername varchar( 16 ) ) ;
insert into #ctp values( 1, 'Big Boy' ) ;
if object_id( 'tempdb.dbo.#client', 'u' ) is not null
drop table #client ;
create table #client( id int, name varchar(16 ), type int ) ;
insert into #client values( 1, 'ABC', 5 ) ;
insert into #client values( 2, 'XYZ', 6 ) ;
if object_id( 'tempdb.dbo.#ctpclient', 'u' ) is not null
drop table #ctpclient ;
create table #ctpclient( id int, ctpfk int, clientfk int ) ;
insert into #ctpclient values( 1, 1, 1 ) ;
insert into #ctpclient values( 2, 1, 2 ) ;
select tp.mastername
, c.name
, c.type
, cc.ctpfk
, cc.clientfk
from #ctp tp
join #ctpclient cc
on tp.id = cc.ctpfk
join #client c
on c.id = cc.clientfk
;
current output
mastername|name|type
Big Boy|ABC|5
Big Boy|XYZ|6
Instead of two rows of output, I would like the output to be as follows:
mastername|nameone|nametwo
Big Boy | ABC | XYZ
What is the optimal way to do this given that I have a many to many table such as #ctpclient?
Assuming you always have 2 rows you can use a crosstab (aka conditional aggregation). It would look something like this.
with SortedValues as
(
select tp.mastername
, c.name
, ROW_NUMBER() over (partition by mastername order by clientfk) as RowNum
from #ctp tp
join #ctpclient cc on tp.id = cc.ctpfk
join #client c on c.id = cc.clientfk
)
select mastername
, MAX(case when RowNum = 1 then name end) as NameOne
, MAX(case when RowNum = 2 then name end) as NameTwo
from SortedValues
group by mastername
If you have a varying numbers you can still accomplish but it is bit more complex.

Recursive select in SQL

I have an issue I just can't get my head around. I know what I want, just simply can't get it out on the screen.
What I have is a table looking like this:
Id, PK UniqueIdentifier, NotNull
Name, nvarchar(255), NotNull
ParentId, UniqueIdentifier, Null
ParentId have a FK to Id.
What I want to accomplish is to get a flat list of all the id's below the Id I pass in.
example:
1 TestName1 NULL
2 TestName2 1
3 TestName3 2
4 TestName4 NULL
5 TestName5 1
The tree would look like this:
-1
-> -2
-> -3
-> -5
-4
If I now ask for 4, I would only get 4 back, but if I ask for 1 I would get 1, 2, 3 and 5.
If I ask for 2, I would get 2 and 3 and so on.
Is there anyone who can point me in the right direction. My brain is fried so I appreciate all help I can get.
declare #T table(
Id int primary key,
Name nvarchar(255) not null,
ParentId int)
insert into #T values
(1, 'TestName1', NULL),
(2, 'TestName2', 1),
(3, 'TestName3', 2),
(4, 'TestName4', NULL),
(5, 'TestName5', 1)
declare #Id int = 1
;with cte as
(
select T.*
from #T as T
where T.Id = #Id
union all
select T.*
from #T as T
inner join cte as C
on T.ParentId = C.Id
)
select *
from cte
Result
Id Name ParentId
----------- -------------------- -----------
1 TestName1 NULL
2 TestName2 1
5 TestName5 1
3 TestName3 2
Here's a working example:
declare #t table (id int, name nvarchar(255), ParentID int)
insert #t values
(1, 'TestName1', NULL),
(2, 'TestName2', 1 ),
(3, 'TestName3', 2 ),
(4, 'TestName4', NULL),
(5, 'TestName5', 1 );
; with rec as
(
select t.name
, t.id as baseid
, t.id
, t.parentid
from #t t
union all
select t.name
, r.baseid
, t.id
, t.parentid
from rec r
join #t t
on t.ParentID = r.id
)
select *
from rec
where baseid = 1
You can filter on baseid, which contains the start of the tree you're querying for.
Try this:
WITH RecQry AS
(
SELECT *
FROM MyTable
UNION ALL
SELECT a.*
FROM MyTable a INNER JOIN RecQry b
ON a.ParentID = b.Id
)
SELECT *
FROM RecQry
Here is a good article about Hierarchy ID models. It goes right from the start of the data right through to the query designs.
Also, you could use a Recursive Query using a Common Table Expression.
I'm guessing that the easiest way to accomplish what you're looking for would be to write a recursive query using a Common Table Expression:
MSDN - Recursive Queries Using Common Table Expressions

t-sql recursive query

Based on an existing table I used CTE recursive query to come up with following data. But failing to apply it a level further.
Data is as below
id name parentid
--------------------------
1 project 0
2 structure 1
3 path_1 2
4 path_2 2
5 path_3 2
6 path_4 3
7 path_5 4
8 path_6 5
I want to recursively form full paths from the above data. Means the recursion will give the following output.
FullPaths
-------------
Project
Project\Structure
Project\Structure\Path_1
Project\Structure\Path_2
Project\Structure\Path_3
Project\Structure\Path_1\path_4
Project\Structure\Path_2\path_5
Project\Structure\Path_3\path_6
Thanks
Here's an example CTE to do that:
declare #t table (id int, name varchar(max), parentid int)
insert into #t select 1, 'project' , 0
union all select 2, 'structure' , 1
union all select 3, 'path_1' , 2
union all select 4, 'path_2' , 2
union all select 5, 'path_3' , 2
union all select 6, 'path_4' , 3
union all select 7, 'path_5' , 4
union all select 8, 'path_6' , 5
; with CteAlias as (
select id, name, parentid
from #t t
where t.parentid = 0
union all
select t.id, parent.name + '\' + t.name, t.parentid
from #t t
inner join CteAlias parent on t.parentid = parent.id
)
select *
from CteAlias
Try something like this:
WITH Recursive AS
(
SELECT
ID,
CAST(PathName AS VARCHAR(500)) AS 'FullPaths',
1 AS 'Level'
FROM
dbo.YourTable
WHERE
ParentID = 0
UNION ALL
SELECT
tbl.ID,
CAST(r.FullPaths + '\' + tbl.PathName AS VARCHAR(500)) AS 'FullPaths',
r.Level + 1 AS 'Level'
FROM
dbo.YourTable tbl
INNER JOIN
Recursive r ON tbl.ParentID = r.ID
)
SELECT * FROM Recursive
ORDER BY Level, ID
Output:
ID FullPaths Level
1 project 1
2 project\structure 2
3 project\structure\path_1 3
4 project\structure\path_2 3
5 project\structure\path_3 3
6 project\structure\path_1\path_4 4
7 project\structure\path_2\path_5 4
8 project\structure\path_3\path_6 4
try this:
DECLARE #YourTable table (id int, nameof varchar(25), parentid int)
INSERT #YourTable VALUES (1,'project',0)
INSERT #YourTable VALUES (2,'structure',1)
INSERT #YourTable VALUES (3,'path_1',2)
INSERT #YourTable VALUES (4,'path_2',2)
INSERT #YourTable VALUES (5,'path_3',2)
INSERT #YourTable VALUES (6,'path_4',3)
INSERT #YourTable VALUES (7,'path_5',4)
INSERT #YourTable VALUES (8,'path_6',5)
;WITH Rec AS
(
SELECT
CONVERT(varchar(max),nameof) as nameof,id
FROM #YourTable
WHERE parentid=0
UNION ALL
SELECT
CONVERT(varchar(max),r.nameof+'\'+y.nameof), y.id
FROM #yourTable y
INNER jOIN Rec r ON y.parentid=r.id
)
select * from rec
output:
nameof
-----------------------------------------------
project
project\structure
project\structure\path_1
project\structure\path_2
project\structure\path_3
project\structure\path_3\path_6
project\structure\path_2\path_5
project\structure\path_1\path_4
(8 row(s) affected)
Something like
;WITH MyCTE AS
(
SELECT
name AS FullPaths, id
FROM
MyTable
WHERE
parentid = 0 /*Normally it'd be IS NULL with an FK linking the 2 columns*/
UNION ALL
SELECT
C.FullPaths + '\' + M.name, M.id
FROM
MyCTE C
JOIN
MyTable M ON M.parentid = C.id
)
SELECT FullPaths FROM MyCTE
You'll have to change the name of #test table I was using.
WITH cte(id, name, parentid) AS
(
SELECT id, convert(varchar(128), name), parentid
FROM #test
WHERE parentid = 0
UNION ALL
SELECT t.id, convert(varchar(128), c.name +'\'+t.name), t.parentid
FROM #test t
INNER JOIN cte c
ON c.id = t.parentid
)
SELECT name as FullPaths
FROM cte
order by id