SQL Sever parent, child, child relationships within table - sql

I have the following table of items with contain a list of name with map back to a parent field within the table -
id nameVal parentId
1 A NULL
2 B NULL
3 C NULL
4 D NULL
5 E NULL
6 A1 1
7 A2 6
8 A3 1
9 A4 7
10 B1 2
11 B2 2
it can be more that one step away from the parent record - A, A1, A4 are all related etc. as below...
A1 => A
A2 => A1 => A
A3 => A
A4 => A2 => A1 => A
So what I'm trying to do is pull through all records where a relationship exists, ie, A4 would bring back all the A's as there is a link to the original A records.
Is this possible?

You can do this with a recursive CTE. The key is to get the parent. Here is one method:
with cte as (
select id, nameval, id as orig
from t
where parentid is null
union all
select t.nameval, cte.orig
from cte join
t
on t.parentid = cte.id
)
select cte.*
from cte
where cte.orig = (select cte2.orig from cte cte2 where ct2.nameval = 'A4');

Perhaps a little more than necessary, but consider the following:
You can set the top node (null will default to the entire hierachy)
You also can set a filter. This can be empty for no filter, a single ID, or a delimited string of IDs.
Declare #T table (id int,nameVal varchar(50),parentId int)
Insert into #T values
(1 ,'A', NULL),
(2 ,'B', NULL),
(3 ,'C', NULL),
(4 ,'D', NULL),
(5 ,'E', NULL),
(6 ,'A1', 1),
(7 ,'A2', 6),
(8 ,'A3', 1),
(9 ,'A4', 7),
(10 ,'B1', 2),
(11 ,'B2', 2)
Declare #Top int = null --<< Sets top of Hier Try 6
Declare #Nest varchar(25) = '|-----' --<< Optional: Added for readability
Declare #Filter varchar(25) = '7' --<< Empty for All or try '7,10'
;with cteP as (
Select Seq = cast(10000+Row_Number() over (Order by nameVal) as varchar(500))
,ID
,parentId
,Lvl=1
,nameVal
From #T
Where IsNull(#Top,-1) = case when #Top is null then isnull(parentId,-1) else ID end
Union All
Select Seq = cast(concat(p.Seq,'.',10000+Row_Number() over (Order by r.nameVal)) as varchar(500))
,r.ID
,r.parentId
,p.Lvl+1
,r.nameVal
From #T r
Join cteP p on r.parentId = p.ID)
,cteR1 as (Select *,R1=Row_Number() over (Order By Seq) From cteP)
,cteR2 as (Select A.Seq,A.ID,R2=Max(B.R1) From cteR1 A Join cteR1 B on (B.Seq like A.Seq+'%') Group By A.Seq,A.ID )
,cte as (
Select A.R1
,B.R2
,A.ID
,A.parentId
,A.Lvl
,nameVal = Replicate(#Nest,A.Lvl-1) + A.nameVal
From cteR1 A
Join cteR2 B on A.ID=B.ID
)
Select Distinct A.*
From cte A
Join (
Select A.R1,A.R2
From cte A
Join (Select R1 from cte Where IIF(#Filter='',1,0)+CharIndex(concat(',',ID,','),concat(',',#Filter+','))>0) B
on B.R1 between A.R1 and A.R2
) B on A.R1 between B.R1 and B.R2
Order By A.R1
Returns
Now, if you set #Filter = '7,10', you'll get
If you set #Filter = '', you'll get

You can use a recursive query to fetch all of the related rows. Whether the data will be in the form you want, I don't know, as the question seems a bit unclear about that. But for example
Fetch a record and all descendants:
with r as (
select *
from my_table t
where id = 7
union all
select t1.*
from my_table t1
inner join r
on t1.parent_id = r.id
)
select * from r;
Fetch a record and all ancestors:
with r as (
select *
from my_table t
where id = 7
union all
select t1.*
from my_table t1
inner join r
on t1.id = r.parent_id
)
select * from r;
Now maybe you want both children and ancestors. This can get a little trickier; recursion works best in a straight line so there can be no infinite loops. One way is to union together the two above queries. If your real query has complex logic that you don't want to write twice, then you could use this to get a list of ID's and then run the real query over a select ... where id in (my_list) type query.
Another consideration is whether a record can have multiple children. If we have
A
A1 => A
A10 => A1
A11 => A1
A2 => A
A20 => A2
A21 => A2
you could say these are all related (through A; some are "cousins"). So if you search from A1 and union the first two example queries, you'd get A, A1, A10, A11... but would you also want the other children of A? If so you could take a slightly different approach:
First, find the eldest ancestor:
with r as (
select *
from my_table t
where id = 7
union all
select t1.*
from my_table t1
inner join r
on t1.id = r.parent_id
)
select id from r where parent_id is null;
Then run the original "all descendants" query against that ID. If you want to get it all into a single statement, the following *should * work I think (but I'm not where I can test it):
with ancestors as (
select *
from my_table t
where id = 7
union all
select t1.*
from my_table t1
inner join ancestors
on t1.id = ancestors.parent_id
) , related as (
select *
from ancestors
where parent_id is null
union all
select t1.*
from my_table t1
inner join related
on t1.parent_id = related.id
)
select * from related;

Related

How to get the total count of records after union of CTE tables in SQL?

I am applying CTE on 3 to 4 tables and combining the results using UNION.I am not storing the combined result anywhere. So now I am facing the challenge to get total number records resulted after union of these four tables.
Also I have to select limited number of rows based on certain flag set like if export to excel is set then select 25000 records else select 10000 records.
Please help me on this.
Code sample looks like below:
WITH Item_Characteristics_CTE AS
(
SELECT
sequence, item_id
FROM
Item_Characteristics_Log
),
Item_Required_Quantity_Log_CTE AS
(
SELECT
sequence, item_id
FROM
Item_Required_Quantity_Log
)
SELECT
c1.item_id
FROM
Item_Characteristics_CTE c1
INNER JOIN
Item_Characteristics_CTE c2 ON c1.sequence = c2.sequence
UNION
SELECT
c1.item_id AS item_id
FROM
Item_Required_Quantity_Log_CTE c1
INNER JOIN
Item_Required_Quantity_Log_CTE c2 ON c1.sequence = c2.sequence
WHERE
C2.RN = C1.RN
I am not sure what flag you mean in your above question. But to get the count, you could use one additional CTE like this:
;WITH Item_Characteristics_CTE
AS (
SELECT sequence
,item_id
FROM Item_Characteristics_Log
)
,Item_Required_Quantity_Log_CTE
AS (
SEELECT sequence
,item_id FROM Item_Required_Quantity_Log
)
,Item_Id_Count
AS (
SELECT c1.item_id
FROM Item_Characteristics_CTE c1
INNER JOIN Item_Characteristics_CTE c2 ON c1.sequence = c2.sequence
UNION
SELECT c1.item_id AS item_id
FROM Item_Required_Quantity_Log_CTE c1
INNER JOIN Item_Required_Quantity_Log_CTE c2 ON c1.sequence = c2.sequence
WHERE C2.RN = C1.RN
)
SELECT item_id
,count(item_id)
FROM Item_Id_Count
You could wrap the query after the CTE as a subquery and count those rows.
;WITH Item_Characteristics_CTE AS (
-- YOUR EXISTING CTE QUERIES
), Item_Required_Quantity_Log_CTE (
-- YOUR EXISTING CTE QUERIES
)
SELECT COUNT(*)
FROM (
-- YOUR EXISTING SELECT + HOWEVER YOU FILTER BY THE FLAG
) T
If you need the output of the CTE queries and the counts, then you could output the CTE into a temp table, then have 1 query to select from the temp table and another to count the rows.
;WITH Item_Characteristics_CTE AS (
-- YOUR EXISTING CTE QUERIES
), Item_Required_Quantity_Log_CTE (
-- YOUR EXISTING CTE QUERIES
)
SELECT c1.item_id INTO #T FROM ... -- (EXISTING QUERIES)
SELECT * FROM #T
SELECT COUNT(*) FROM #T
UPDATE BASED ON FEEDBACK FOR ##ROWCOUNT AND OPTIONAL MAX ROWS
The query below takes the flag and decided how many max rows to get, then provides that max row count to an outer query of your CTE results.
DECLARE #IsExcel BIT = 1 -- Your flag whether excel or not
DECLARE #Rows INT = CASE WHEN #IsExcel = 1 THEN 25000 ELSE 10000 END
WITH Item_Characteristics_CTE AS (
SELECT
sequence,
item_id
FROM Item_Characteristics_Log
), Item_Required_Quantity_Log_CTE AS (
SELECT
sequence,
item_id
FROM Item_Required_Quantity_Log
)
SELECT TOP (#Rows) *
FROM (
SELECT
c1.item_id
FROM Item_Characteristics_CTE c1
INNER JOIN Item_Characteristics_CTE c2
ON c1.sequence = c2.sequence
UNION
SELECT
c1.item_id AS item_id
FROM Item_Required_Quantity_Log_CTE c1
INNER JOIN Item_Required_Quantity_Log_CTE c2
ON c1.sequence = c2.sequence
WHERE C2.RN = C1.RN
)
DECLARE #TotalRows INT = ##ROWCOUNT -- OR "SELECT ##ROWCOUNT"

Count distinct records in one column with multiple values in another column

I'm pretty sure this is an easy question, but I'm having trouble wording it.
I need to count the total number of values in one column based on distinct criteria in another column.
Example:
A CD
B ABC
C AD
D A
Would yield:
A 3
B 1
C 2
D 2
First, you shouldn't be storing lists of things in a string.
But, sometimes one is stuck with this format. In your example, you seem to have a table with all possible values. If so you can use a join:
select e.col1, count(e2.col2)
from example e left join
example e2
on charindex(e.col1, e2.col2) > 0
group by e.col1;
Note: this counts rows containing the value rather. If multiple values appear in a single row, the query is a bit more complicated.
Here is how you can do it:
DECLARE #t TABLE ( c1 CHAR(1), c2 VARCHAR(5) )
INSERT INTO #t
VALUES ( 'A', 'CD' ),
( 'B', 'ABC' ),
( 'C', 'AD' ),
( 'D', 'A' )
SELECT t.c1 ,
SUM(count) AS count
FROM #t t
CROSS APPLY ( SELECT LEN(c2) - LEN(REPLACE(c2, t.c1, '')) AS count
FROM #t
WHERE c2 LIKE '%' + t.c1 + '%'
) ca
GROUP BY t.c1
Assuming table is called yourtable and fields are like soo.
fielda fieldb
A CD
B ABC
C AD
D A
Code
SELECT a.fielda, (SELECT COUNT(b.fieldb)
FROM yourtable b
WHERE b.fieldb LIKE '%a.fielda%' AND b.fielda = a.fielda) AS counter
FROM yourtable a
You can use a correlated subquery with LIKE
Sample Data
with cte(a,b) as
(
select 'A','CD'
union all select 'B','ABC'
union all select'C','AD'
union all select'D','A'
)
Query
select a,(select count(*) from cte c2 where b like '%' + c1.a +'%')
from cte c1
group by a
Output
A 3
B 1
C 2
D 2
Use a correlated sub-query when counting. Use LIKE to find rows to find rows to count.
select t1.col1, (select count(*) from tablename t2
where t2.col2 like '%' || t1.col1 ||'%')
from tablename t1
|| is ANSI SQL concatenation. Some products use concat(), or + instead.
Looks like you need a self join there but the trick would be to use a pattern match on the join rather than an equi-join...
create table x1(c1 char(1) primary key, c2 varchar(5) not null);
select x1.c1, count(*)
from x1 x1
join x1 x2 on x2.c2 like '%' || x1.c1 || '%'
group by x1.c1
order by 1;

SQL Server - Providing priority to where clause condtions

Please consider the following SQL.
declare #t1 table(site int, id int, name varchar(2))
declare #t2 table(site int, id int, mark int)
insert into #t1
select 1,1,'A'
union select 1,2,'B'
union select 1,3,'C'
union select 2,2,'D'
union select 2,3,'C'
insert into #t2
select 1,1,10
union select 1,2,20
union select 0,3,30
union select 1,3,40
union select 2,3,40
union select 2,3,40
select distinct a.site, a.id,a.name,b.mark
from #t1 a
inner join #t2 b
on (a.site =b.site or b.site = 0) and a.id = b.id
where a.site=1
It produces the following result
site id name mark
----------------------------
1 1 A 10
1 2 B 20
1 3 C 30
1 3 C 40
It's correct.
But I want a person's data exactly once. The SQL should first check whether there is an entry for a person in #t2 for a specific site. If entry is found, then use it. If not, the mark of that person will be the person's mark who has the same name in site 0.
In this case, I want the result as follows.
site id name mark
----------------------------
1 1 A 10
1 2 B 20
1 3 C 40
But if (1,3,40) isn't in #t2, The result should be as follows.
site id name mark
----------------------------
1 1 A 10
1 2 B 20
1 3 C 30
How can I do this?
I can do it using Common Table Expression.
So please provide me a faster way.
I'll run it on about 100 millions rows.
You can roll all of the conditions into the on clause:
declare #target_site as Int = 1
select distinct a.site, a.id, a.name, b.mark
from #t1 as a inner join
#t2 as b on a.site = #target_site and a.id = b.id and
( a.site = b.site or ( b.site = 0 and not exists ( select 42 from #t2 where site = #target_site and id = a.id ) ) )
Outer Join to the t2 table twice, and Use a subquery to ensure that only records that have a match or are zeroes are included.
Select distinct a.site, a.id, a.name,
coalesce(sm.mark, zs.mark) mark
from #t1 a
Left Join #t2 sm -- for site match
on sm.id = a.id
And sm.site = a.site
Left Join #t2 zs -- for zero site
on zs.id = a.id
And zs.site = 0
Where Exists (Select * From #t2
Where id = a.id
And Site In (a.Site, 0))
And a.site=1

SQL - Is it possible to join a table to a resultset created by several select/union-alls?

I'm trying to find a workaround (hack) to some limitations preventing me from using a temporary table or a table variable in my SQL query.
I have a real table (technically it's a derived table that results from an UNPIVOT of a poorly designed table) which lacks several necessary fields. I need to hardcode these fields into the result until we can cleanup the database issue.
Given a table like:
tblEntity
ID | Name
1 | One
2 | Two
I need to join several fields such as:
ID | Order
1 | 2
2 | 1
The join would result in:
ID | Name | Order
1 | One | 2
2 | Two | 1
My question is: can I join tblEntity to a resultset created like:
SELECT 1, 2
UNION ALL
SELECT 2, 1
Is it possible to join? If so, what is the syntax?
select te.*, t.Ord from tblEntity te
inner join (
SELECT 1 as Id, 2 as Ord
UNION ALL
SELECT 2, 1
) t on te.ID = t.Id
Making a few assumptions, this would do it:
SELECT en.ID, en.Name, xx.OrderBy
from tblEntity en
inner join (select 1 Id, 2 OrderBy
union all
select 2,1) xx
on xx.Id = en.ID
In SQL-Server 2008, it's also possible to use Table Value Constructors:
CREATE TABLE #tblEntity
( ID INT
, Name CHAR(10)
) ;
INSERT INTO #tblEntity
(ID, Name)
VALUES
( 1, 'One' ) ,
( 2, 'Two' ) ;
SELECT
t.ID, t.Name, o.Ordr AS "Order"
FROM
#tblEntity AS t
JOIN
( VALUES
(1,2)
, (2,1)
) AS o(ID, Ordr)
ON o.ID = t.ID ;
You can test the above in: data.stackexchange.com
Many ways of doing this e.g.
WITH T1 (Id, "Order")
AS
(
SELECT 1, 2
UNION ALL
SELECT 2, 1
)
SELECT e.*, T1."Order"
FROM tblEntity e
JOIN T1
ON e.Id = T1.Id;
e.g. 2
SELECT e.*, T1."Order"
FROM tblEntity e
JOIN (
VALUES (1, 2),
(2, 1)
) AS T1 (Id, "Order")
ON e.Id = T1.Id;
e.g. 3
WITH T1
AS
(
SELECT *
FROM (
VALUES (1, 2),
(2, 1)
) AS T (Id, "Order")
)
SELECT e.*, T1."Order"
FROM tblEntity e
JOIN T1
ON e.Id = T1.Id;
...and so on.

Problem combining result of two different queries into one

I have two tables (TableA and TableB).
create table TableA
(A int null)
create table TableB
(B int null)
insert into TableA
(A) values (1)
insert into TableB
(B) values (2)
I cant join them together but still I would like to show the result from them as one row.
Now I can make select like this:
select
(select A from tableA) as A
, B from TableB
Result:
A B
1 2
But if I now delete from tableB:
delete tableB
Now when I run the same query as before:
select
(select A from tableA) as A
, B from TableB
I see this:
A B
But I was expecting seeing value from tableA
like this:
Expected Result:
A B
1
Why is this happening and how can I still see the value from TableA although selectB is returning 0 rows?
I am using MS SQL Server 2005.
Use a LEFT JOIN (although it's more of a cross join in your case).
If your db supports it:
SELECT a.a, b.b
FROM a
CROSS JOIN b
If not, do something like:
SELECT a.a, b.b
FROM a
LEFT JOIN b ON ( 1=1 )
However, once you have more rows in a or b, this will return the cartesian product:
1 1
1 2
2 1
2 2
This will actually give you what you're looking for, but if you only have one row per table:
select
(select A from tableA) as A
, (select B from TableB) as B
give this a try:
DECLARE #TableA table (A int null)
DECLARE #TableB table (B int null)
insert into #TableA (A) values (1)
insert into #TableB (B) values (2)
--this assumes that you don't have a Numbers table, and generates one on the fly with up to 500 rows, you can increase or decrease as necessary, or just join in your Numbers table instead
;WITH Digits AS
(
SELECT 0 AS nbr
UNION SELECT 1 UNION SELECT 2 UNION SELECT 3
UNION SELECT 4 UNION SELECT 5 UNION SELECT 6
UNION SELECT 7 UNION SELECT 8 UNION SELECT 9
)
, AllNumbers AS
(
SELECT u3.nbr * 100 + u2.nbr * 10 + u1.nbr + 1 AS Number
FROM Digits u1, Digits u2, Digits u3
WHERE u3.nbr * 100 + u2.nbr * 10 + u1.nbr + 1 <= 500
)
, AllRowsA AS
(
SELECT
A, ROW_NUMBER() OVER (ORDER BY A) AS RowNumber
FROM #TableA
)
, AllRowsB AS
(
SELECT
B, ROW_NUMBER() OVER (ORDER BY B) AS RowNumber
FROM #TableB
)
SELECT
a.A,b.B
FROM AllNumbers n
LEFT OUTER JOIN AllRowsA a on n.Number=a.RowNumber
LEFT OUTER JOIN AllRowsB b on n.Number=b.RowNumber
WHERE a.A IS NOT NULL OR b.B IS NOT NULL
OUTPUT:
A B
----------- -----------
1 2
(1 row(s) affected)
if you DELETE #TableB, the output is:
A B
----------- -----------
1 NULL
(1 row(s) affected)
try this:
select a, (select b from b) from a
union
select b, (select a from a) from b
should retrieve you all the existing data.
you can filter it more by surrounding it with another select