Recursive CTE to update parent records through multiple levels - sql-server

Will a CTE use data that is updated as part of the CTE in the next recursion? I am trying to attempt this CTE because the performance of a similar UPDATE logic inside of a WHILE loop is not performing well and I was hoping that using the CTE would be more set based and perform better.
I am having trouble with a recursive CTE updating a table until the parent rows are all marked properly.
SQL Fiddle
The SQL fiddle shows the table and basic CTE. Before even adding in any of the AND/OR or level logic, I cannot seem to get the CTE to climb the hierarchy and mark the parents as "met".
Here is the example table:
| LogicID | ParentLogic | Depth | Type | Description | Met |
|--------- |------------- |------- |------ |------------------------------------- |----- |
| 1 | NULL | NULL | NULL | Conditions All Met | 0 |
| 2 | 1 | 1 | AND | The sky or ocean is blue | 0 |
| 3 | 2 | 2 | OR | The sky is blue | 0 |
| 4 | 2 | 2 | OR | The ocean is blue | 1 |
| 5 | 1 | 1 | AND | The grass is green or road is black | 0 |
| 6 | 5 | 2 | OR | The grass is green | 1 |
| 7 | 5 | 2 | OR | The road is black | 0 |
| 8 | 1 | 1 | AND | Birds, bugs or the 4 below | 0 |
| 9 | 8 | 2 | OR | There are birds | 0 |
| 10 | 8 | 2 | OR | There are bugs | 0 |
| 11 | 8 | 2 | OR | All 4 below | 0 |
| 12 | 11 | 3 | AND | There are dogs | 1 |
| 13 | 11 | 3 | AND | There are cats | 1 |
| 14 | 11 | 3 | AND | There are people | 1 |
| 15 | 11 | 3 | AND | There are chairs | 1 |
DROP TABLE MyLogic
CREATE TABLE MyLogic
(
LogicID int
,ParentLogic int
,Depth int
,Type varchar(4)
,Description varchar(35)
,Met int
);
INSERT INTO MyLogic
( LogicID, ParentLogic, Depth, Type, Description, Met )
VALUES
( 1, NULL, NULL, NULL, 'Conditions All Met', 0 ),
( 2, 1, 1, 'AND', 'The sky or ocean is blue', 0 ),
( 3, 2, 2, 'OR', 'The sky is blue', 0 ),
( 4, 2, 2, 'OR', 'The ocean is blue', 1 ),
( 5, 1, 1, 'AND', 'The grass is green or road is black', 0 ),
( 6, 5, 2, 'OR', 'The grass is green', 1 ),
( 7, 5, 2, 'OR', 'The road is black', 0 ),
( 8, 1, 1, 'AND', 'Birds, bugs or the 4 below', 0 ),
( 9, 8, 2, 'OR', 'There are birds', 0 ),
( 10, 8, 2, 'OR', 'There are bugs', 0 ),
( 11, 8, 2, 'OR', 'All 4 below', 0 ),
( 12, 11, 3, 'AND', 'There are dogs', 1 ),
( 13, 11, 3, 'AND', 'There are cats', 1 ),
( 14, 11, 3, 'AND', 'There are people', 1 ),
( 15, 11, 3, 'AND', 'There are chairs', 1 )
This is just a sample of a much more complicated set of logic. Basically the idea is that I need each set of children to "rollup" to the parent using the logic in the table. The depth is variable but is likely to be 7 deep.
So, LogicID 12,13,14,15 are ANDed together and would then mark 11 as Met. Then 9,10,11 would be evaluated and if any (OR) then mark 8 as Met. And so on, until the top level parent LogicID 1 is either Met or not Met.
Can this be done with the CTE and if so can someone help me get it going?
EDIT::
Thanks for the help - as requested here is the update statement.
DECLARE #maxdepth AS int = ( SELECT MAX (Depth) FROM MyLogic)
DECLARE #counter AS int = 0
WHILE ( #counter < #maxdepth )
BEGIN
UPDATE
UP
SET
UP.Met =
--SELECT *,
CASE
WHEN ORIG.Type = 'AND' AND ORIG.Met = 0 AND COUNTS.CountMet = 2 THEN 0
WHEN ORIG.Type = 'AND' AND ORIG.Met = 0 AND COUNTS.CountMet = 1 THEN 0
WHEN ORIG.Type = 'AND' AND ORIG.Met = 1 AND COUNTS.CountMet = 2 THEN 0
WHEN ORIG.Type = 'AND' AND ORIG.Met = 1 AND COUNTS.CountMet = 1 THEN 1
WHEN ORIG.Type = 'OR' AND ORIG.Met = 1 AND COUNTS.CountMet = 1 THEN 1
WHEN ORIG.Type = 'OR' AND ORIG.Met = 0 AND COUNTS.CountMet = 2 THEN 1
WHEN ORIG.Type = 'OR' AND ORIG.Met = 1 AND COUNTS.CountMet = 2 THEN 1
WHEN ORIG.Type = 'OR' AND ORIG.Met = 0 AND COUNTS.CountMet = 1 THEN 0
END
FROM
MyLogic UP
INNER JOIN dbo.MyLogic ORIG
ON UP.LogicID = ORIG.ParentLogic
INNER JOIN ( SELECT
DIST.ParentLogic
,COUNT(DISTINCT DIST.Met) AS CountMet
FROM
MyLogic DIST
GROUP BY
DIST.ParentLogic
) COUNTS
ON ORIG.ParentLogic = COUNTS.ParentLogic
SET #counter = #counter + 1
END

I do not believe you will be able to accomplish your goal in this scenario with CTE. Every approach I tried with CTE hinges on being able to use GROUP BY in the recursive part of the CTE which is not allowed.
Here is an alternative that should speed up your UPDATE. It's still a bit iterative, but it seems to be a good bit faster in my quick testing owing mostly to more set-based approach and fewer joins needed. It should scale more efficiently as well than the current process.
DECLARE #D AS INT = ( SELECT MAX (Depth) FROM MyLogic)
WHILE #D > 0 BEGIN
UPDATE P SET
Met = C.Met
FROM MyLogic P
INNER JOIN (
SELECT
ParentLogic,
CASE
WHEN SUM(CASE WHEN Type = 'OR' THEN Met ELSE 0 END) > 0
OR SUM(CASE WHEN Type = 'AND' THEN Met ELSE 0 END) = COUNT(*)
THEN 1 ELSE 0 END AS Met
FROM MyLogic
WHERE Depth = #D
GROUP BY
ParentLogic
) C -- Only update parents with children
ON P.LogicID = C.ParentLogic
WHERE COALESCE(P.Depth, 0) = #D - 1 -- Get next level up
SET #D = #D - 1
END
SELECT * FROM MyLogic

Related

Sum two columns in SQL and optimise SQL query

I have the following query which returns two columns, I want to sum both columns and create the third column through summing the two.
Is there any way I can recreate the below query by removing the subquery? Any way I can achieve the same through joins?
SELECT
IIF(c2.isdeleted = 1 OR c2.approved = 0, 0, 1) AS Contentcount,
(SELECT COUNT(c1.content)
FROM comments c1
WHERE c1.parentcommentid = c2.id
AND c1.isdeleted = 0
AND c1.approved = 1) ChildContentcount --Anyway to remove the subquery
FROM
comments c2
WHERE
c2.discussionid = '257943'
AND c2.parentcommentid IS NULL
ORDER BY
c2.pinned DESC,
c2.createddate
Sample data:
+----------+--------------+
| content | childcontent |
+----------+--------------+
| 1 | 8 |
| 0 | 0 |
| 1 | 3 |
+----------+--------------+
Expected output:
+----------+----------------+---------+
| content | childcontent | sumdata |
+----------+----------------+---------+
| 1 | 8 | 9 |
| 0 | 0 | 0 |
| 1 | 3 | 4 |
| 1 | 8 | 9 |
+----------+----------------+---------+
You can use CROSS APPLY or OUTER APPLY instead of a correlated subquery.
Then you can re-use the values.
select c.pinned, c.createddate
, c.discussionid
, ca1.content
, ca2.childcontent
, (ca1.content + ca2.childcontent) AS sumdata
FROM comments c
CROSS APPLY
(
SELECT CASE
WHEN c.isdeleted = 1 OR c.approved = 0 THEN 0
ELSE 1
END AS content
) ca1
CROSS APPLY
(
SELECT COUNT(c2.content) AS childcontent
FROM comments c2
WHERE c2.parentcommentid = c.id
AND c2.isdeleted = 0
AND c2.approved = 1
) ca2
WHERE c.discussionid = '257943'
AND c.parentcommentid IS NULL
ORDER BY
c.pinned DESC,
c.createddate;
Subquery and sum the columns :
select tbl.* , Contentcount+ChildContentcount third_sum from
(
select IIF(c2.isdeleted = 1 OR c2.approved = 0, 0, 1) AS Contentcount,
(SELECT COUNT(c1.content)
FROM comments c1
WHERE c1.parentcommentid = c2.id
AND c1.isdeleted = 0
AND c1.approved = 1) ChildContentcount
FROM
comments c2
WHERE
c2.discussionid = '257943'
AND c2.parentcommentid IS NULL ) tbl
If you supply sql fiddle, we can try to create it alternative ways

How to update column of a table from another table in sequence when table don't relate

I am not sure how to update column's value from another table in SQL Server when table don't relate to each other by unique key.
A
+--------+--------+--------+
| Id | col2_A | col3_A |
+--------+--------+--------+
| 1 | 3 | 5 |
| 2 | 3 | 3 |
| 3 | 3 | 2 |
| 4 | 3 | 1 |
| 5 | 3 | 8 |
+--------+--------+--------+
B
+--------+--------+
| Id | Col1_B |
+--------+--------+
| 11 | 6 |
| 12 | 7 |
| 13 | 8 |
| 14 | 9 |
| 15 | 10 |
+--------+--------+
Required Result:
+--------+--------+--------+
| Id | col2_A | col3_A |
+--------+--------+--------+
| 1 | 1 | 6 |
| 2 | 1 | 7 |
| 3 | 1 | 8 |
| 4 | 1 | 9 |
| 5 | 1 | 10 |
+--------+--------+--------+
Pseudo code
Replace/update Col3_A with col1_B in sequence.
My code: doesn't work as my code updates Col3_A with random values
Update A
Set col2_A = '1', col3_A = (select col1_B from B)
Do I need to use cursor?Please help
Try this:
DECLARE #t1 TABLE ( id INT, col1 INT, col2 INT );
DECLARE #t2 TABLE ( id INT, col1 INT );
INSERT INTO #t1
VALUES ( 1, 3, 5 ),
( 2, 3, 3 ),
( 3, 3, 2 ),
( 4, 3, 1 ),
( 5, 3, 8 );
INSERT INTO #t2
VALUES ( 11, 6 ),
( 12, 7 ),
( 13, 8 ),
( 14, 9 ),
( 15, 10 );
UPDATE l
SET l.col1 = '1', l.col2 = r.col1
FROM ( SELECT ROW_NUMBER() OVER ( ORDER BY id ) row ,
*
FROM #t1
) AS l
INNER JOIN ( SELECT ROW_NUMBER() OVER ( ORDER BY id ) row ,
*
FROM #t2
) AS r ON r.row = l.row
WHERE r.row = l.row;
SELECT *
FROM #t1;
Result:
Is this what you want?
update A, B set col2_A = 1, A.col3_A = B.Col1_B where A.Id = B.Id;
Edit 1
OK, then add pseudo id, and drop it after update.
alter table A add column id1 int;
set #n = 0;
update A set id1 = #n := #n+1;
update A, (select #n := #n + 1 id, col1_B from B, (SELECT #n := 0) m) b set A.col3_A = b.col1_B where A.id1 = b.id;
alter table A drop column id1;

In SQL getting the Max() of a Count() for a specific Group by

My script
SELECT ans.Questions_Id,ans.Answer_Numeric,ans.Option_Id, opt.Description, count(ans.Option_Id) as [Count]
FROM Answers ans
LEFT OUTER JOIN Questions que
ON ans.Questions_Id = que.Id
LEFT OUTER JOIN Options opt
ON ans.Option_Id = opt.Id
WHERE que.Survey_Id = 1
and ans.Questions_Id = 1
GROUP By ans.Questions_Id,ans.Answer_Numeric,ans.Option_Id, opt.Description
ORDER BY 2, 5 desc
I am trying to get the top number responses (Description) for each Answer_Numeric. The result at the moment looks like this:
| Questions_Id | Answer_Numeric | Option_Id | Description | Count
-----------------------------------------------------------------------
| 1 | 1 | 27 | Technology | 183
| 1 | 1 | 24 | Personal Items | 1
| 1 | 2 | 28 | Wallet / Purse | 174
| 1 | 2 | 24 | Personal Items | 3
| 1 | 2 | 26 | Spiritual | 1
| 1 | 3 | 24 | Personal Items | 53
| 1 | 3 | 25 | Food / Fluids | 5
| 1 | 3 | 26 | Spiritual | 5
| 1 | 3 | 27 | Technology | 1
| 1 | 3 | 28 | Wallet / Purse | 1
As from the example data from above I need it to look like this:
| Questions_Id | Answer_Numeric | Option_Id | Description | Count
-----------------------------------------------------------------------
| 1 | 1 | 27 | Technology | 183
| 1 | 2 | 28 | Wallet / Purse | 174
| 1 | 3 | 24 | Personal Items | 53
I am pretty sure that I need to have a max or something in my Having clause but everything I have tried has not worked. Would really appreciate any help on this.
You can use ROW_NUMBER:
SELECT Questions_Id, Answer_Numeric, Option_Id, Description, [Count]
FROM (
SELECT ans.Questions_Id,ans.Answer_Numeric,ans.Option_Id,
opt.Description, count(ans.Option_Id) as [Count],
ROW_NUMBER() OVER (PARTITION BY ans.Questions_Id, ans.Answer_Numeric
ORDER BY count(ans.Option_Id) DESC) AS rn
FROM Answers ans
LEFT OUTER JOIN Questions que
ON ans.Questions_Id = que.Id
LEFT OUTER JOIN Options opt
ON ans.Option_Id = opt.Id
WHERE que.Survey_Id = 1
and ans.Questions_Id = 1
GROUP By ans.Questions_Id,
ans.Answer_Numeric,
ans.Option_Id,
opt.Description) AS t
WHERE t.rn = 1
ORDER BY 2, 5 desc
Alternatively you can use RANK so as to handle ties, i.e. more than one rows per Questions_Id, Answer_Numeric partition sharing the same maximum Count number.
Use row_number():
SELECT *
FROM (SELECT ans.Questions_Id, ans.Answer_Numeric, ans.Option_Id, opt.Description,
count(*) as cnt,
row_number() over (partition by ans.Questions_Id, ans.Answer_Numeric
order by count(*) desc) as seqnum
FROM Answers ans LEFT OUTER JOIN
Questions que
ON ans.Questions_Id = que.Id LEFT OUTER JOIN
Options opt
ON ans.Option_Id = opt.Id
WHERE que.Survey_Id = 1 and ans.Questions_Id = 1
GROUP By ans.Questions_Id, ans.Answer_Numeric, ans.Option_Id, opt.Description
) t
WHERE seqnum = 1
ORDER BY 2, 5 desc;
we can get the same result set in different ways and I have taken sample data set you just merge your joins in this code
declare #Table1 TABLE
(Id int, Answer int, OptionId int, Description varchar(14), Count int)
;
INSERT INTO #Table1
(Id, Answer, OptionId, Description, Count)
VALUES
(1, 1, 27, 'Technology', 183),
(1, 1, 24, 'Personal Items', 1),
(1, 2, 28, 'Wallet / Purse', 174),
(1, 2, 24, 'Personal Items', 3),
(1, 2, 26, 'Spiritual', 1),
(1, 3, 24, 'Personal Items', 53),
(1, 3, 25, 'Food / Fluids', 5),
(1, 3, 26, 'Spiritual', 5),
(1, 3, 27, 'Technology', 1),
(1, 3, 28, 'Wallet / Purse', 1)
;
SELECT tt.Id, tt.Answer, tt.OptionId, tt.Description, tt.Count
FROM #Table1 tt
INNER JOIN
(SELECT OptionId, MAX(Count)OVER(PARTITION BY OptionId ORDER BY OptionId)AS RN
FROM #Table1
GROUP BY OptionId,count) groupedtt
ON
tt.Count = groupedtt.RN
WHERE tt.Count <> 5
GROUP BY tt.Id, tt.Answer, tt.OptionId, tt.Description, tt.Count
OR
select distinct Count, Description , Id , Answer from #Table1 e where 1 =
(select count(distinct Count ) from #Table1 where
Count >= e.Count and (Description = e.Description))

update order column in a table but maintain the order

i have a table
id | title | F_ID | order
----------------------------
1 | test 1| 1 | 44
2 | test 3| 1 | 3
3 | test 4| 1 | 1
4 | test 5| 2 | 1
i want to update order column to +10 for all rows that have F_ID 1 but keep the order
the result need to be
id | title | F_ID | order
----------------------------
1 | test 1| 1 | 30
2 | test 3| 1 | 20
3 | test 4| 1 | 10
4 | test 5| 2 | 1
i can insert all rows that i want to update to temp table
and then loop the rows and update every row in the real table by [id].
maybe there is a better option?
I think this should work:
SQL Fiddle
MS SQL Server 2008 Schema Setup:
create table test (id int, title varchar(49), F_ID int, [order] int)
insert test values
(1 , 'test 1', 1, 44),
(2 , 'test 3', 1, 3),
(3 , 'test 4', 1, 1),
(4 , 'test 5', 2, 1)
Query 1:
update test
set [order] = new_order
from test t
inner join (
select
id,
new_order = ROW_NUMBER() over (partition by f_id order by [order]) * 10
from test t
where f_id = 1
) t2
on t.id = t2.id
Results:
Query 2:
select * from test
Results:
| ID | TITLE | F_ID | ORDER |
|----|--------|------|-------|
| 1 | test 1 | 1 | 30 |
| 2 | test 3 | 1 | 20 |
| 3 | test 4 | 1 | 10 |
| 4 | test 5 | 2 | 1 |
Well there might be a better solution then this but you can try this by using recursive CTE.
;WITH updCTE
AS
(
SELECT 30 AS YourOrder, 1 AS id
UNION ALL
SELECT YourOrder - 10 AS YourOrder, id + 1 AS id
FROM updCTE
WHERE YourOrder > 1
)
UPDATE YourTable
SET [order] = YourOrder
FROM updCTE
JOIN YourTable ON updCTE.id = YourTable.id
WHERE YourTable.F_ID = 1
ORDER BY YourTable.id

Sorting table in sql server

My table data like
id LedgerName
1 "105 AAA"
2 "102 sss"
3 "GGGG"
4 "107 BBB"
5 "BBBB"
6 "101 TTT"
i want sorting the Ledger like
6 "101 TTT"
2 "102 sss"
1 "105 AAA"
4 "107 BBB"
5 "BBBB"
3 "GGGG"
Normal Order by is not working.
i used split function to split for number based sorting in order by ..
how to fix this issue
With the data you have provided a regular order by on LedgerName is doing what you want.
Below is version that deals with data that is a bit more complicated.
SQL Fiddle
MS SQL Server 2008 Schema Setup:
create table YourTable
(
id int,
LedgerName varchar(20)
)
insert into YourTable values
(1, '105 AAA' ),
(2, '1020 sss' ),
(3, ' ' ),
(4, null ),
(5, '0' ),
(6, '999 sss' ),
(7, '9999 sss' ),
(8, 'GGGG' ),
(9, '107 BBB' ),
(10, 'BBBB' ),
(11, '101 TTT' )
Query 1:
select id,
LedgerName
from YourTable
order by case when patindex('%[^0-9]%', isnull(LedgerName, '')+' ') = 1 then 1 else 0 end,
cast(left(LedgerName, patindex('%[^0-9]%', LedgerName+' ')-1) as int),
LedgerName
Results:
| ID | LEDGERNAME |
-------------------
| 5 | 0 |
| 11 | 101 TTT |
| 1 | 105 AAA |
| 9 | 107 BBB |
| 6 | 999 sss |
| 2 | 1020 sss |
| 7 | 9999 sss |
| 4 | (null) |
| 3 | |
| 10 | BBBB |
| 8 | GGGG |

Resources