Recursive CTE and Count Info from recursive table - sql

I have this table called tblLandAreas:
Level 1 = Bloq
Level 2 = Ships
Level 3 = Sides
Level 4 = Beds
Ship is a child of Bloq, Sides is a child of Ship and Bed is a child of Side.
And I need to show a report this way:
Only when _parentid is null
I have tried this but it is not working:

Try the following:
DECLARE #t TABLE
(
ID INT ,
ParentID INT ,
Level INT
)
INSERT INTO #t
VALUES ( 1, NULL, 1 ),
( 29, NULL, 1 ),
( 38, 29, 2 ),
( 32, 1, 2 ),
( 18, 1, 2 ),
( 41, 29, 2 ),
( 42, 41, 3 ),
( 43, 41, 3 ),
( 44, 41, 3 ),
( 45, 44, 4 )
;WITH cte AS(SELECT *, id AS baseid FROM #t WHERE ParentID IS NULL
UNION ALL
SELECT t.*, c.baseid FROM cte c JOIN #t t ON c.ID = t.ParentID)
SELECT baseid,
SUM(CASE WHEN Level = 2 THEN 1 ELSE 0 END) ships,
SUM(CASE WHEN Level = 3 THEN 1 ELSE 0 END) sides,
SUM(CASE WHEN Level = 4 THEN 1 ELSE 0 END) beds
FROM cte
WHERE ParentID IS NOT NULL
GROUP BY baseid
Output:
baseid ships sides beds
1 2 0 0
29 2 3 1
To project on your structure:
;WITH Bloque AS(SELECT LandAreaId, _ParentId, _Level, LandAreaId AS baseLandAreaId
FROM tblLandAreas WHERE _ParentId IS NULL
UNION ALL
SELECT a.LandAreaId, a._ParentId, a._Level, B.baseLandAreaId
FROM Bloque B
JOIN tblLandAreas a ON B.LandAreaId = a._ParentId)
SELECT baseLandAreaId AS LandAreaId,
SUM(CASE WHEN _Level = 2 THEN 1 ELSE 0 END) [#Ships],
SUM(CASE WHEN _Level = 3 THEN 1 ELSE 0 END) [#Sides],
SUM(CASE WHEN _Level = 4 THEN 1 ELSE 0 END) [#Beds]
FROM Bloque
WHERE _ParentId IS NOT NULL
GROUP BY baseLandAreaId

Related

Calculating date intervals from a daily-grained fact table

I have the data for student absence which I got after some transformations. The data is day by day:
WITH datasample AS (
SELECT 1 AS StudentID, 20180101 AS DateID, 0 AS AbsentToday, 0 AS AbsentYesterday UNION ALL
SELECT 1, 20180102, 1, 0 UNION ALL
SELECT 1, 20180103, 1, 1 UNION ALL
SELECT 1, 20180104, 1, 1 UNION ALL
SELECT 1, 20180105, 1, 1 UNION ALL
SELECT 1, 20180106, 0, 1 UNION ALL
SELECT 2, 20180101, 0, 0 UNION ALL
SELECT 2, 20180102, 1, 0 UNION ALL
SELECT 2, 20180103, 1, 1 UNION ALL
SELECT 2, 20180104, 0, 1 UNION ALL
SELECT 2, 20180105, 1, 0 UNION ALL
SELECT 2, 20180106, 1, 1 UNION ALL
SELECT 2, 20180107, 0, 1
)
SELECT *
FROM datasample
ORDER BY StudentID, DateID
I need to add a column (AbsencePeriodInMonth) which would calculate the student's absence period during the month.
For example, StudentID=1 was absent in one consecutive period during the month and StudentID=2 had two periods, something like this:
StudentID DateID AbsentToday AbsentYesterday AbsencePeriodInMonth
1 20180101 0 0 0
1 20180102 1 0 1
1 20180103 1 1 1
1 20180104 1 1 1
1 20180105 1 1 1
1 20180106 0 1 0
2 20180101 0 0 0
2 20180102 1 0 1
2 20180103 1 1 1
2 20180104 0 1 0
2 20180105 1 0 2
2 20180106 1 1 2
2 20180107 0 1 0
My goal is actually to calculate the consecutive absent days prior to each day in the fact table, I think I can do it if I get the AbsencePeriodInMonth column, by having this added to my query after the *:
,CASE WHEN AbsentToday = 1 THEN DENSE_RANK() OVER(PARTITION BY StudentID, AbsencePeriodInMonth ORDER BY DateID)
ELSE 0
END AS DaysAbsent
Any idea on how I can add that AbsencePeriodInMonth or maybe calculate the consecutive absent days in some other way?
You can identify each period by counting the number of 0s before hand. Then you can enumerate them using dense_rank().
select ds.*,
(case when absenttoday = 1 then dense_rank() over (partition by studentid order by grp)
else 0
end) as AbsencePeriodInMonth
from (select ds.*, sum(case when absenttoday = 0 then 1 else 0 end) over (partition by studentid order by dateid) as grp
from datasample ds
) ds
order by StudentID, DateID;
Here is a SQL Fiddle.
Using Recursive CTE and Dense_Rank
WITH datasample AS (
SELECT 1 AS StudentID, 20180101 AS DateID, 0 AS AbsentToday, 0 AS AbsentYesterday UNION ALL
SELECT 1, 20180102, 1, 0 UNION ALL
SELECT 1, 20180103, 1, 1 UNION ALL
SELECT 1, 20180104, 1, 1 UNION ALL
SELECT 1, 20180105, 1, 1 UNION ALL
SELECT 1, 20180106, 0, 1 UNION ALL
SELECT 2, 20180101, 0, 0 UNION ALL
SELECT 2, 20180102, 1, 0 UNION ALL
SELECT 2, 20180103, 1, 1 UNION ALL
SELECT 2, 20180104, 0, 1 UNION ALL
SELECT 2, 20180105, 1, 0 UNION ALL
SELECT 2, 20180106, 1, 1 UNION ALL
SELECT 2, 20180107, 0, 1
), cte as
(Select *,DateID as dd
from datasample
where AbsentToday = 1 and AbsentYesterday = 0
union all
Select d.*, c.dd
from datasample d
join cte c
on d.StudentID = c.StudentID and d.DateID = c.DateID + 1
where d.AbsentToday = 1
), cte1 as
(
Select *, DENSE_RANK() over (partition by StudentId order by dd) as de
from cte
)
Select d.*, IsNull(c.de,0) as AbsencePeriodInMonth
from cte1 c
right join datasample d
on d.StudentID = c.StudentID and c.DateID = d.DateID
order by d.StudentID, d.DateID

how to separate and sum 2 columns based on condition

I'm doing a select statement and I have a column I would like to separate into 2 columns based on their type, and then get the sum of the amounts grouped by an ID
I want all the gold and platinum types in one column, and all the silver and bronze in a 2nd column, then summed and grouped by the ID so it looks like this :
I tried doing a union like this:
SELECT
ID,
SUM(Amount) AS "Gold/Platinum",
0 AS "Bronze/Silver"
FROM
table
WHERE
Type IN ('gold', 'platinum')
GROUP BY
ID
UNION ALL
SELECT
ID,
SUM(Amount) AS "Bronze/Silver",
0 AS "Gold/Platinum"
FROM
table
WHERE
Type IN ('bronze', 'silver')
GROUP BY
ID
The gold/platinum column will be correct, but I get nothing in the bronze/silver column
Use conditional aggregation:
select id,
sum(case when Type in ('gold', 'platinum') then amount else 0 end) as gold_platinum,
sum(case when Type in ('bronze', 'silver') then amount else 0 end) as bronze_silver
from t
group by id
order by id;
You can run this in SSMS:
DECLARE #data TABLE( [ID] INT, [Type] VARCHAR(10), [Amount] INT );
INSERT INTO #data ( [ID], [Type], [Amount] ) VALUES
( 1, 'gold', 100 )
, ( 1, 'gold', 50 )
, ( 1, 'bronze', 75 )
, ( 2, 'silver', 10 )
, ( 2, 'bronze', 20 )
, ( 3, 'gold', 35 )
, ( 4, 'silver', 20 )
, ( 4, 'platinum', 30 );
SELECT
[ID]
, SUM( CASE WHEN [Type] IN ( 'gold', 'platinum' ) THEN Amount ELSE 0 END ) AS [Gold/Platinum]
, SUM( CASE WHEN [Type] IN ( 'bronze', 'silver' ) THEN Amount ELSE 0 END ) AS [Bronze/Silver]
FROM #data
GROUP BY [ID]
ORDER BY [ID];
Returns
+----+---------------+---------------+
| ID | Gold/Platinum | Bronze/Silver |
+----+---------------+---------------+
| 1 | 150 | 75 |
| 2 | 0 | 30 |
| 3 | 35 | 0 |
| 4 | 30 | 20 |
+----+---------------+---------------+

pivot multi column and mutil row

this is my origin data:
ID New LikeNew Old Warehouse
1 10 100 20 LA
1 12 130 40 CA
2 90 200 10 LA
2 103 230 30 CA
i want to get the following format:
ID LA_new LA_likeNew LA_Old CA_New CA_LikeNew CA_Old
1 10 100 20 12 130 40
2 90 200 10 103 230 30
I can only pivot on each column but can not do on all 3 columns(New, LikeNew, Old) , so how can I do that?
You can accomplish this by using conditional logic to create your fields and grouping:
select id,
max(case when warehouse = 'LA' then new else null end) LA_New,
max(case when warehouse = 'LA' then likenew else null end) LA_likeNew,
max(case when warehouse = 'LA' then old else null end) LA_Old,
max(case when warehouse = 'CA' then new else null end) CA_New,
max(case when warehouse = 'CA' then likenew else null end) CA_likeNew,
max(case when warehouse = 'CA' then old else null end) CA_Old
from yourtable
group by id
Alternatively, you can use WITH Common Table Expression
DECLARE #T AS TABLE
(
id INT ,
New INT ,
likeNew INT ,
old INT ,
Warehouse VARCHAR(50)
)
INSERT INTO #T
( id, New, likeNew, old, Warehouse )
VALUES ( 1, 10, 100, 20, 'LA' ),
( 1, 12, 130, 40, 'CA' ),
( 2, 90, 200, 10, 'LA' ),
( 2, 103, 230, 30, 'CA' );
WITH cte
AS ( SELECT *
FROM #T
WHERE Warehouse = 'LA'
),
cte1
AS ( SELECT *
FROM #T
WHERE Warehouse = 'CA'
)
SELECT a.id ,
a.new [LA_New] ,
a.likeNew [LA_LikeNew] ,
a.Old [LA_Old] ,
b.new [CA_New] ,
b.likeNew [CA_LikeNew] ,
b.Old [CA_Old]
FROM cte A
JOIN cte1 B ON a.id = b.id
Result:
id LA_New LA_LikeNew LA_Old CA_New CA_LikeNew CA_Old
----------- ----------- ----------- ----------- ----------- ----------- -----------
1 10 100 20 12 130 40
2 90 200 10 103 230 30

sql server: peak active users by date range

I needs to get count of max active users by date range.
Acrive users - it is a maximum number of not removed users.
I have UsersHistory Table:
HistoryID UserID IsRemoved OperationID ModificationDate
----------------------------------------------------------------------
1 1 0 'Add' 2012-07-24 04:27:48
2 2 0 'Add' 2012-07-26 04:18:48
3 3 0 'Add' 2012-07-27 04:29:48
4 1 0 'Update' 2012-07-28 04:47:48
5 2 0 'Update' 2012-07-29 04:01:48
6 1 1 'Remove' 2012-08-28 04:34:48
7 2 1 'Remove' 2012-08-28 04:18:48
8 3 1 'Remove' 2012-08-28 04:29:48
9 4 0 'Add' 2012-09-24 04:27:48
10 5 0 'Add' 2012-09-26 04:18:48
11 6 0 'Add' 2012-09-27 04:29:48
12 7 0 'Add' 2012-09-27 04:29:48
Expected result: Max active users by this period: 4 (HistoryID: 9, 10, 11, 12)
Update1:
HistoryID UserID IsRemoved OperationID ModificationDate
----------------------------------------------------------------------
1 1 0 'Add' 2012-07-24 04:27:48
2 2 0 'Add' 2012-07-26 04:18:48
3 3 0 'Add' 2012-07-27 04:29:48
4 1 1 'Remove' 2012-07-28 04:47:48
5 2 1 'Remove' 2012-07-28 04:47:48
6 3 1 'Remove' 2012-07-28 04:47:48
Expected result: Maximum of active(not removed) users by this period: 3
This should work for you. It will count total number of operations Add and total operations of Remove. After It subract total count of Remove from total count of Add.
DECLARE #counter1 INT,
#counter2 INT,
#result INT
SELECT #counter1 = c1
FROM(
SELECT COUNT(HistoryID) AS c1
FROM UsersHistory
WHERE OperationId = 'Add'
) x
SELECT #counter2 = c2
FROM(
SELECT COUNT(HistoryID) AS c2
FROM UsersHistory
WHERE OperationId = 'Remove'
) a
SET #result = #counter1 - #counter2
SELECT #result
EDIT
Here you go (It should count maximum as you need):
SELECT TOP 1 COUNT(*) AS maxRes
FROM (
SELECT DENSE_RANK() OVER (ORDER BY UserID ASC) AS [MaximumCount], *
FROM UsersHistory
WHERE OperationId = 'Add'
) a
WHERE
[MaximumCount] = UserID
GROUP BY MaximumCount
ORDER BY maxRes DESC
If I understand you correctly you need to make islands by OperationID and count maximum count of rows in 'Add' islands. If so then:
DECLARE #History TABLE
(
HistoryID INT ,
UserID INT ,
IsRemoved BIT ,
OperationID NVARCHAR(20)
)
INSERT INTO #History
VALUES ( 1, 1, 0, 'Add' ),
( 2, 2, 0, 'Add' ),
( 3, 3, 0, 'Add' ),
( 4, 1, 1, 'Remove' ),
( 5, 2, 1, 'Remove' ),
( 6, 3, 1, 'Remove' ),
( 7, 3, 0, 'Add' );
WITH cte1
AS ( SELECT OperationID ,
ROW_NUMBER() OVER ( ORDER BY HistoryID ) AS ID ,
1 AS Dummy ,
IIF(ISNULL(LAG(OperationID) OVER ( ORDER BY HistoryID ),
OperationID) = OperationID, 0, 1) AS ChangeMark
FROM #History
),
cte2
AS ( SELECT * ,
SUM(Dummy) OVER ( ORDER BY ID )
+ SUM(ChangeMark) OVER ( ORDER BY ID ) AS InervalID
FROM cte1
),
cte3
AS ( SELECT StartSeqNo = MIN(InervalID) ,
EndSeqNo = MAX(InervalID)
FROM ( SELECT InervalID ,
rn = InervalID
- ROW_NUMBER() OVER ( ORDER BY InervalID )
FROM cte2
) a
GROUP BY rn
),
cte4
AS ( SELECT COUNT(*) AS C
FROM cte2 c2
JOIN cte3 c3 ON c2.InervalID BETWEEN c3.StartSeqNo AND c3.EndSeqNo
WHERE c2.OperationID = 'Add'
GROUP BY c3.EndSeqNo
)
SELECT MAX(c) AS UserCount
FROM cte4

SQL: get latest (un)subscribeaction from table

I have the following table:
ID (int)
EMAIL (varchar(50))
CAMPAIGNID (int)
isSubscribe (bit)
isActionByUser (bit)
This table stores all subscribe and unsubscribe actions on campaigns for a user. These actions can be done by the user itself (isActionByUser = true) or by the administration desk (isActionByUser = false).
I need to get the last action to determine if a user is subscribed or unsubscribed. But keeping in mind that when a user did an unsubscribe action from a campaign, it will have priority on other subscribe actions by the administration desk.
I have found a nice solution to get the lastest record grouped by EMAIL and CAMPAIGNID. But I can't figure out how I do incorporate the requirement that an isActionByUser = true, has absolute priority over records with isActionByUser = false.
Also: when the administration desk does an unsubscribe action, it will have priority over a record with (isSubscribe = true and isActionByUser).
Example data:
ID EMAIL CAMPAIGNID ISSUBSCRIBE ISACTIONBYUSER
-----------------------------------------------------------
1 a#aa.com 1 1 0
2 b#bb.com 1 1 0
3 c#cc.com 1 1 0
4 a#aa.com 1 0 1
5 a#aa.com 1 1 0
6 c#cc.com 1 1 1
7 c#cc.com 1 0 0
The expected result would be:
ID EMAIL CAMPAIGNID ISSUBSCRIBE ISACTIONBYUSER
-----------------------------------------------------------
2 b#bb.com 1 1 0
4 a#aa.com 1 0 1
7 c#cc.com 1 0 0
With the following query
select cs1.*
from
[TABLE] cs1
left join
[TABLE] cs2
on
cs1.EM_EMAIL = cs2.EM_EMAIL
and
cs1.EM_CAMPAIGNID = cs2.EM_CAMPAIGNID
and
cs1.id < cs2.id
where cs2.id is null
I' m having the following result:
ID EMAIL CAMPAIGNID ISSUBSCRIBE ISACTIONBYUSER
-----------------------------------------------------------
2 b#bb.com 1 1 0
5 a#aa.com 1 1 0
7 c#cc.com 1 0 0
Another approach:
SELECT *
FROM [TABLE] cs
WHERE id in
(
SELECT top 1 id
FROM [TABLE] ss
WHERE
cs.EMAIL = ss.EMAIL
and
cs.CAMPAIGNID = ss.CAMPAIGNID
and ISSUBSCRIBE = (
select top 1 min(convert(int, ISSUBSCRIBE))
FROM [TABLE] sss
WHERE
cs.EMAIL = sss.EMAIL
and
cs.CAMPAIGNID = sss.CAMPAIGNID
)
and ISACTIONBYUSER= (
select top 1 max(convert(int, ISACTIONBYUSER))
FROM [TABLE] ssss
WHERE
cs.EMAIL = ssss.EMAIL
and
cs.CAMPAIGNID = ssss.CAMPAIGNID
)
)
This will produce the following result:
ID EMAIL CAMPAIGNID ISSUBSCRIBE ISACTIONBYUSER
-----------------------------------------------------------
2 b#bb.com 1 1 0
4 a#aa.com 1 0 1
6 c#cc.com 1 1 1
Which is also not correct. And I'm afraid performance will be a big rpoblem with this approach.
So any ideas how I can solve this?
Ok, try the following query:
SELECT DISTINCT B.*
FROM YourTable A
OUTER APPLY (SELECT TOP 1 *
FROM YourTable
WHERE Email = A.Email AND CampaignId = A.CampaignId
ORDER BY CASE WHEN ISSUBSCRIBE = 0 THEN 1 ELSE 2 END,
CASE WHEN ISACTIONBYUSER = 1 THEN 1 ELSE 2 END,
ID DESC) B
Try this: [Updated to handle unsubscribe and subscribed users]
declare #test table (id int, email varchar(100), CAMPAIGNID int, ISSUBSCRIBE bit, ISACTIONBYUSER bit)
INSERT INTO #test
SELECT 1,'a#aa.com',1,1,0 UNION
SELECT 2,'b#bb.com',1,1,0 UNION
SELECT 3,'c#cc.com',1,1,0 UNION
SELECT 4,'a#aa.com',1,0,1 UNION
SELECT 5,'a#aa.com',1,1,0 UNION
SELECT 6,'c#cc.com',1,1,1 UNION
SELECT 7,'c#cc.com',1,0,0 UNION
select 8, 'd#dd.com', 1, 1, 1 UNION
select 9, 'd#dd.com', 1, 0, 1 UNION
select 10, 'd#dd.com', 1, 1, 1
;WITh CTE AS
(
select s.*,
ROW_NUMBER() OVER (PARTITION BY email,campaignid
ORDER BY
case
when ISSUBSCRIBE = 0 AND ISACTIONBYUSER = 0 THEN 1
when ISSUBSCRIBE = 0 AND ISACTIONBYUSER = 1 THEN 1
when ISSUBSCRIBE = 1 AND ISACTIONBYUSER = 1 THEN 1 ELSE 2 END, ID DESC) Rn1
from #test s
)
SELECT * FROM CTE WHERE Rn1 = 1
order by id
This is some standard SQL that might get you there, though it's not the prettiest ever:
Updated:
select s.*
from Subscriptions s
join (
-- Apply the user unsubscribe logic to get the proper ID
select case when b.ID is not null and a.ISACTIONBYUSER = 0 then b.ID else a.ID end as ID
from (
-- Latest overall
select ID, EMAIL, CAMPAIGNID,
(select ISACTIONBYUSER from Subscriptions where ID = z.ID) as ISACTIONBYUSER
from (
select max(ID) as ID, EMAIL, CAMPAIGNID
from Subscriptions a
group by EMAIL, CAMPAIGNID
) as z
) as a
left join (
-- Latest user unsubscribe
select max(ID) as ID, EMAIL, CAMPAIGNID, 1 as ISACTIONBYUSER
from Subscriptions
where ISSUBSCRIBE = 0
and ISACTIONBYUSER = 1
group by EMAIL, CAMPAIGNID
) as b on a.EMAIL = b.EMAIL
and a.CAMPAIGNID = b.CAMPAIGNID
) as i on s.ID = i.ID
I've updated this to account for this case:
insert into Subscriptions select 8, 'd#dd.com', 1, 1, 1
insert into Subscriptions select 9, 'd#dd.com', 1, 0, 1
insert into Subscriptions select 10, 'd#dd.com', 1, 1, 1