Using HAVING or WHERE on SUM Fields - sql-server

Problem
We have a Users table and a Addresses table.Each user can have multiple addresses. We have a bit field called IsDefault where a user can select their default address. This field currently isnt mandatory and possibly will be in the future, so I need to do some analysis. Now I want to validate the addresses to see:
How many addresses a given user has.
How many of those addresses (if they have more than 1 address) have
the IsDefault flag set to a 1.
Basically I want to see how many of my users who have multiple addresses, have not switched on any of their addresses to be their default.
I have the following SQL query so far:
SELECT AD.User_Id,
COUNT(AD.User_Id) AS HowManyAddresses,
SUM(
CASE WHEN
AD.IsDefault IS NULL
OR
AD.IsDefault = 0
THEN
1
ELSE
0
END
) AS DefaultEmpty,
SUM(
CASE WHEN
AD.IsDefault = 1
THEN
1
ELSE
0
END
) AS DefaultAddress
FROM dbo.Addresses AS AD
JOIN dbo.Users AS U
ON U.Id = AD.User_Id
GROUP BY AD.User_ID
ORDER BY AD.User_Id
The problem I have found is I want to check the values from the DefaultAddress and DefaultEmpty SELECT SUM fields, but I get the following error when trying to reference them using WHERE or HAVING:
Invalid column name 'DefaultEmpty'.
Is it not possible to reference SUM values for selection purposes?
Technology using:
SQL Server 2008
SQL Server Management Studio 2008

Actually you need to repeat the whole SUM clause with HAVING like this -
SELECT
AD.User_Id
,COUNT(AD.User_Id) AS HowManyAddresses
,SUM(
CASE
WHEN
AD.IsDefault IS NULL OR
AD.IsDefault = 0 THEN 1
ELSE 0
END
) AS DefaultEmpty
,SUM(
CASE
WHEN
AD.IsDefault = 1 THEN 1
ELSE 0
END
) AS DefaultAddress
FROM dbo.Addresses AS AD
JOIN dbo.Users AS U
ON U.Id = AD.User_Id
GROUP BY AD.User_ID
HAVING SUM(
CASE
WHEN
AD.IsDefault IS NULL OR
AD.IsDefault = 0 THEN 1
ELSE 0
END
) = 0
ORDER BY AD.User_Id
OR
DECLARE #address TABLE(UserID INT,Address VARCHAR(100),IsDefault BIT);
INSERT INTO #address VALUES
(1,'User 1 default',1)
,(2,'User 2 non default',0)
,(3,'User 3 non default',0)
,(3,'User 3 default',1)
,(4,'User 4 default',1)
,(4,'User 4 second default',1);
SELECT
COUNT(*) OVER () AS HowManyAddresses
,ISNULL(def0.DefaultEmpty, 0) AS DefaultEmpty
,ISNULL(def1.DefaultAddress, 0) AS DefaultAddress
FROM (SELECT
AD.Address
,COUNT(AD.UserID) OVER (PARTITION BY AD.UserID) AS DefaultEmpty
FROM #address AS AD
WHERE (AD.IsDefault = 0)) def0
FULL JOIN (SELECT
AD.Address
,COUNT(AD.UserID) OVER (PARTITION BY AD.UserID) AS DefaultAddress
FROM #address AS AD
WHERE (AD.IsDefault = 1)) def1
ON def0.Address = def1.Address

You can use CTE in following if you want to use alias names in WHERE clause:
WITH cte AS (
SELECT AD.User_Id,
COUNT(AD.User_Id) AS HowManyAddresses,
SUM(
CASE WHEN
AD.IsDefault IS NULL
OR
AD.IsDefault = 0
THEN
1
ELSE
0
END
) AS DefaultEmpty,
SUM(
CASE WHEN
AD.IsDefault = 1
THEN
1
ELSE
0
END
) AS DefaultAddress
FROM dbo.Addresses AS AD
JOIN dbo.Users AS U
ON U.Id = AD.User_Id
GROUP BY AD.User_ID
ORDER BY AD.User_Id
)
SELECT *
FROM cte
WHERE /* You can use alias names here... */
GROUP BY /* You can use alias names here... */
HAVING /* You can use alias names here... */

This will count - grouped by user and default, how many addresses there are:
DECLARE #user TABLE(ID INT, Name VARCHAR(100));
INSERT INTO #user VALUES
(1,'user1') --will get a default
,(2,'user2') --no default
,(3,'user3') --both
,(4,'user4') --two defaults
,(5,'user5');--nothing
DECLARE #address TABLE(UserID INT,Address VARCHAR(100),IsDefault BIT);
INSERT INTO #address VALUES
(1,'User 1 default',1)
,(2,'User 2 non default',0)
,(3,'User 3 non default',0)
,(3,'User 3 default',1)
,(4,'User 4 default',1)
,(4,'User 4 second default',1);
EDIT: Better with PIVOT...
SELECT p.*
FROM
(
SELECT u.ID,u.Name
,CASE WHEN a.IsDefault=1 THEN 'DEFAULT' ELSE 'NORMAL' END AS PivotColumn
,COUNT(a.UserID) AS CountPerUserAndDefault
FROM #user AS u
LEFT JOIN #address AS a ON u.ID=a.UserID
GROUP BY u.ID,u.Name,a.IsDefault
) AS tbl
PIVOT
(
SUM(CountPerUserAndDefault) FOR PivotColumn IN([DEFAULT],[NORMAL])
) AS p
The result:
Name ID DEFAULT NORMAL
user1 1 1 NULL
user2 2 NULL 1
user3 3 1 1
user4 4 2 NULL
user5 5 NULL 0

Put this after GROUPBY:
HAVING SUM(CAST(AD.IsDefault AS INT)) > 0

Related

SQL - Finding Gaps in Coverage

I am running this problem on SQL server
Here is my problem.
have something like this
Dataset A
FK_ID StartDate EndDate Type
1 10/1/2018 11/30/2018 M
1 12/1/2018 2/28/2019 N
1 3/1/2019 10/31/2019 M
I have a second data source I have no control over with data something like this:
Dataset B
FK_ID SpanStart SpanEnd Type
1 10/1/2018 10/15/2018 M
1 10/1/2018 10/25/2018 M
1 2/15/2019 4/30/2019 M
1 5/1/2019 10/31/2019 M
What I am trying to accomplish is to check to make sure every date within each TYPE M record in Dataset A has at least 1 record in Dataset B.
For example record 1 in Dataset A does NOT have coverage from 10/26/2018 through 11/30/2018. I really only care about when the coverage ends, in this case I want to return 10/26/2018 because it is the first date where the span has no coverage from Dataset B.
I've written a function that does this but it is pretty slow because it is cycling through each date within each M record and counting the number of records in Dataset B. It exits the loop when it finds the first one but I would really like to make this more efficient. I am sure I am not thinking about this properly so any suggestions anyone can offer would be helpful.
This is the section of code I'm currently running
else if #SpanType = 'M'
begin
set #CurrDate = #SpanStart
set #UncovDays = 0
while #CurrDate <= #SpanEnd
Begin
if (SELECT count(*)
FROM eligiblecoverage ec join eligibilityplan ep on ec.plandescription = ep.planname
WHERE ec.masterindividualid = #IndID
and ec.planbegindate <= #CurrDate and ec.planenddate >= #CurrDate
and ec.sourcecreateddate = #MaxDate
and ep.medicaidcoverage = 1) = 0
begin
SET #Result = concat('NON Starting ',format(#currdate, 'M/d/yyyy'))
BREAK
end
set #CurrDate = #CurrDate + 1
end
end
I am not married to having a function it just could not find a way to do this in queries that wasn't very very slow.
EDIT: Dataset B will never have any TYPEs except M so that is not a consideration
EDIT 2: The code offered by DonPablo does de-overlap the data but only in cases where there is an overlap at all. It reduces dataset B to:
FK_ID SpanStart SpanEnd Type
1 10/1/2018 10/25/2018 M
instead of
FK_ID SpanStart SpanEnd Type
1 10/1/2018 10/25/2018 M
1 2/15/2019 4/30/2019 M
1 5/1/2019 10/31/2019 M
I am still futzing around with it but it's a start.
I would approach this by focusing on B. My assumption is that any absent record would follow span_end in the table. So here is the idea:
Unpivot the dates in B (adding "1" to the end dates)
Add a flag if they are present with type "M".
Check to see if any not-present records are in the span for A.
Check the first and last dates as well.
So, this looks like:
with bdates as (
select v.dte,
(case when exists (select 1
from b b2
where v.dte between b2.spanstart and b2.spanend and
b2.type = 'M'
)
then 1 else 0
end) as in_b
from b cross apply
(values (spanstart), (dateadd(day, 1, spanend)
) v(dte)
where b.type = 'M' -- all we care about
group by v.dte -- no need for duplicates
)
select a.*,
(case when not exists (select 1
from b b2
where a.startdate between b2.spanstart and b2.spanend and
b2.type = 'M'
)
then 0
when not exists (select 1
from b b2
where a.enddate between b2.spanstart and b2.spanend and
b2.type = 'M'
)
when exists (select 1
from bdates bd
where bd.dte between a.startdate and a.enddate and
bd.in_b = 0
)
then 0
when exists (select 1
from b b2
where a.startdate between b2.spanstart and b2.spanend and
b2.type = 'M'
)
then 1
else 0
end)
from a;
What is this doing? Four validity checks:
Is the starttime valid?
Is the endtime valid?
Are any intermediate dates invalid?
Is there at least one valid record?
Start by framing the problem in smaller pieces, in a sequence of actions like I did in the comment.
See George Polya "How To Solve It" 1945
Then Google is your friend -- look at==> sql de-overlap date ranges into one record (over a million results)
UPDATED--I picked Merge overlapping dates in SQL Server
and updated it for our table and column names.
Also look at theory from 1983 Allen's Interval Algebra https://www.ics.uci.edu/~alspaugh/cls/shr/allen.html
Or from 2014 https://stewashton.wordpress.com/2014/03/11/sql-for-date-ranges-gaps-and-overlaps/
This is a primer on how to setup test data for this problem.
Finally determine what counts via Ranking the various pairs of A vs B --
bypass those totally Within, then work with earliest PartialOverlaps, lastly do the Precede/Follow items.
--from Merge overlapping dates in SQL Server
with SpanStarts as
(
select distinct FK_ID, SpanStart
from Coverage_B as t1
where not exists
(select * from Coverage_B as t2
where t2.FK_ID = t1.FK_ID
and t2.SpanStart < t1.SpanStart
and t2.SpanEnd >= t1.SpanStart)
),
SpanEnds as
(
select distinct FK_ID, SpanEnd
from Coverage_B as t1
where not exists
(select * from Coverage_B as t2
where t2.FK_ID = t1.FK_ID
and t2.SpanEnd > t1.SpanEnd
and t2.SpanStart <= t1.SpanEnd)
),
DeOverlapped_B as
(
Select FK_ID, SpanStart,
(select min(SpanEnd) from SpanEnds as e
where e.FK_ID = s.FK_ID
and SpanEnd >= SpanStart) as SpanEnd
from SpanStarts as s
)
Select * from DeOverlapped_B
Now we have something to feed into the next steps, and we can use the above as a CTE
======================================
with SpanStarts as
(
select distinct FK_ID, SpanStart
from Coverage_B as t1
where not exists
(select * from Coverage_B as t2
where t2.FK_ID = t1.FK_ID
and t2.SpanStart < t1.SpanStart
and t2.SpanEnd >= t1.SpanStart)
),
SpanEnds as
(
select distinct FK_ID, SpanEnd
from Coverage_B as t1
where not exists
(select * from Coverage_B as t2
where t2.FK_ID = t1.FK_ID
and t2.SpanEnd > t1.SpanEnd
and t2.SpanStart <= t1.SpanEnd)
),
DeOverlapped_B as
(
Select FK_ID, SpanStart,
(select min(SpanEnd) from SpanEnds as e
where e.FK_ID = s.FK_ID
and SpanEnd >= SpanStart) as SpanEnd
from SpanStarts as s
),
-- find A row's coverage
ACoverage as (
Select
a.*, b.SpanEnd, b.SpanStart,
Case
When SpanStart <= StartDate And StartDate <= SpanEnd
And SpanStart <= EndDate And EndDate <= SpanEnd
Then '1within' -- starts, equals, during, finishes
When EndDate < SpanStart
Or SpanEnd < StartDate
Then '3beforeAfter' -- preceeds, meets, preceeded, met
Else '2overlap' -- one or two ends hang over spanStart/End
End as relation
From Coverage_A a
Left Join DeOverlapped_B b
On a.FK_ID = b.FK_ID
Where a.Type = 'M'
)
Select
*
,Case
When relation1 = '2' And StartDate < SpanStart Then StartDate
When relation1 = '2' Then DateAdd(d, 1, SpanEnd)
When relation1 = '3' Then StartDate
End as UnCoveredBeginning
From (
Select
*
,SUBSTRING(relation,1,1) as relation1
,ROW_NUMBER() Over (Partition by A_ID Order by relation, SpanStart) as Rownum
from ACoverage
) aRNO
Where Rownum = 1
And relation1 <> '1'

SQL Server : select when record is 1 then return line else return the 0 record

I need a little help with a query.
I have written a script that brings back an order number and the number of containers needed (code below):
SELECT
CONI.CONTNO,
CONI.ITEMNO,
CONI.[WEIGHT],
CONI.QTY,
STOK.PGROUP,
CASE WHEN CPRO.TNTCOL = 1 THEN 1
WHEN CPRO.TNTCOL = 0 THEN 0
WHEN CPRO.TNTCOL IS NULL THEN 0 END AS [TNT],
CONI.RECID,
CPRO.RECKEY
INTO
#SUB
FROM
ContItems CONI
LEFT JOIN
ContractItemProfiles CPRO ON CONI.RECID = CPRO.RECKEY
JOIN
Stock STOK ON CONI.ITEMNO = STOK.ITEMNO
WHERE
STOK.PGROUP LIKE 'FLI%'
SELECT
#SUB.CONTNO,
#SUB.TNT,
SUM(#SUB.QTY) AS [Number of flight cases]
FROM
#SUB
WHERE
#SUB.CONTNO = '123/321581'
GROUP BY
#SUB.CONTNO,
#SUB.TNT
DROP TABLE #SUB
I get this result:
Contno TNT Number of flight cases
------------------------------------------
123/321581 0 20.00
123/321581 1 1.00
I need to conditionally bring back the line that has the TNT = 1 Else if there isn't a 1 in the TNT column then bring back the record with 0
I hope this is explained enough.
That case can be replaced with
isnull(CPRO.TNTCOL, 0)
select top 1
from ( SELECT #SUB.CONTNO,
#SUB.TNT,
SUM(#SUB.QTY) AS [Number of flight cases]
FROM #SUB
WHERE #SUB.CONTNO = '123/321581'
) t
order by TNT desc

select distinct with parent-child and return boolean for at least one child with a value?

I am currently searching for orders that have at least one orderline (product) with a certain boolean set:
- the product is a subscription product
- the product is a setup product
If one of the orderlines has this value set to 1, I want to return this in the query per DISTINCT order ID.
This does not seem to work for me:
SELECT DISTINCT [ORDER].[order_id]
,[ORDERLINE].[is_subscription] AS hasSubArticles
,[ORDERLINE].[is_setup] AS hasSetupArticles
FROM [ORDER]
LEFT JOIN [ORDERLINE]
ON [ORDER].[order_id] = [ORDERLINE].[f_order_id]
WHERE [G_ORDER].[status] = 1
ORDER BY [ORDER].[order_id]
,[ORDERLINE].[is_subscription] AS hasSubArticles
,[ORDERLINE].[is_setup] AS hasSetupArticles
When I check the returned records, I receive duplicate ORDER records:
order_id hasSubArticles hasSetupArticles
----------------------------------------
17804 NULL NULL
17804 1 0
I want to return only 1 record per order ID, thus this isn't working for me.
What am I doing wrong?
Distinct does not work for your requirement. MAX, Min functions are not allowed to use with bit type. You could use Group by and SUM like this
SELECT
[ORDER].[order_id]
,CASE WHEN SUM( CASE WHEN [ORDERLINE].[is_subscription] = 1 THEN 1 ELSE 0 END) > 0 THEN 1
ELSE 0
END AS hasSubArticles
,CASE WHEN SUM( CASE WHEN [ORDERLINE].[is_setup] = 1 THEN 1 ELSE 0 END) > 0
THEN 1
ELSE 0
END hasSetupArticles
FROM [ORDER]
LEFT JOIN [ORDERLINE]
ON [ORDER].[order_id] = [ORDERLINE].[f_order_id]
WHERE [G_ORDER].[status] = 1
GROUP BY [ORDER].[order_id]

Check if record exists while inserting using User defined table type sql server

I am trying to bulk insert the records in sql server. I am using User Defined Table type to pass the collection of records from my .net application. Please take a look at the insert query below.
INSERT INTO MachineItems([Name],[Price],[Quantity],[ItemGroupID],[SubGroup] ,[IsDefault],
[IsRemovable],[MachineTypeID],[ItemType],[CreatedBy],[CreatedOn] )
SELECT mi.Name
,mi.Price
,mi.Quantity
,(SELECT ID from ItemGroups WHERE NAME=mi.ItemGroup) as ID
,mi.SubGroup,
CASE
WHEN mi.IsDefault ='Yes' THEN 1
WHEN mi.IsDefault ='No' THEN 0
WHEN mi.IsDefault IS NULL THEN 0
END ,
CASE
WHEN mi.IsRemovable ='Yes' THEN 1
WHEN mi.IsRemovable ='No' THEN 0
END ,
(SELECT ID from MachineTypes WHERE Name=mi.MachineType),
(SELECT ID from MachineItemTypes WHERE Name=mi.ItemType),
mi.CreatedBy
,mi.CreatedOn
FROM #MachineItems mi
What i want to do is put the check before inserting the records , Whether record with [MachineTypeID] and [Name] already exists in table or not. If it does not exists then insert Eles Update the record.
How can i do that with User Defined Table Type ?
You should use the MERGE command rather than a straight insert. What you are wanting to do is not really specific to User-Defined Table Types.
It would be better / more efficient if you joined the 3 subtables rather than having subqueries for columns which will execute per-row.
Example:
MERGE MachineItems AS Target
USING (SELECT mi.Name,
mi.Price,
mi.Quantity,
ig.ID, -- ItemGroupID
mi.SubGroup,
CASE
WHEN mi.IsDefault ='Yes' THEN 1
WHEN mi.IsDefault ='No' THEN 0
WHEN mi.IsDefault IS NULL THEN 0
END, -- IsDefault
CASE
WHEN mi.IsRemovable ='Yes' THEN 1
WHEN mi.IsRemovable ='No' THEN 0
END, -- IsRemovable
mt.ID, -- MachineTypeID
mit.ID, -- ItemType
mi.CreatedBy,
mi.CreatedOn
FROM #MachineItems mi
INNER JOIN ItemGroups ig
ON ig.[Name] = mi.ItemGroup
INNER JOIN MachineTypes mt
ON mt.[Name] = mi.MachineType
INNER JOIN MachineItemTypes mit
ON mit.[Name] = mi.ItemType) AS Source (
[Name],[Price],[Quantity],[ItemGroupID],[SubGroup],[IsDefault],
[IsRemovable],[MachineTypeID],[ItemType],[CreatedBy],[CreatedOn])
ON (
Target.[MachineTypeID] = Source.[MachineTypeID]
AND Target.[Name] = Source.[Name]
)
WHEN MATCHED THEN
UPDATE SET Price = Source.Price,
Quantity = Source.Quantity,
ItemGroupID = Source.ItemGroupID,
SubGroup = Source.SubGroup,
IsDefault = Source.IsDefault,
IsRemovable = Source.IsRemovable,
MachineTypeID = Source.MachineTypeID,
ItemType = Source.ItemType,
CreatedBy = Source.CreatedBy,
CreatedOn = Source.CreatedOn
WHEN NOT MATCHED BY TARGET THEN
INSERT ([Name],[Price],[Quantity],[ItemGroupID],[SubGroup] ,[IsDefault],
[IsRemovable],[MachineTypeID],[ItemType],[CreatedBy],[CreatedOn])
VALUES (Source.[Name], Source.[Price], Source.[Quantity], Source.[ItemGroupID],
Source.[SubGroup], Source.[IsDefault], Source.[IsRemovable],
Source.[MachineTypeID], Source.[ItemType], Source.[CreatedBy],
Source.[CreatedOn]);
You can Use Merge Here
Using Merge
You can Insert if Not Exists
You can Delete if Already Exists
You can Update if Already Exists
MERGE MachineItems
USING #MachineItems ON MachineItems.id = #MachineItems.id
and MachineItems.Name=#MachineItems.Name
WHEN NOT MATCHED THEN
INSERT INTO MachineItems([Name],[Price],[Quantity],[ItemGroupID],[SubGroup]
,[IsDefault],
[IsRemovable],[MachineTypeID],[ItemType],[CreatedBy],[CreatedOn] )
SELECT mi.Name
,mi.Price
,mi.Quantity
,(SELECT ID from ItemGroups WHERE NAME=mi.ItemGroup) as ID
,mi.SubGroup,
CASE
WHEN mi.IsDefault ='Yes' THEN 1
WHEN mi.IsDefault ='No' THEN 0
WHEN mi.IsDefault IS NULL THEN 0
END ,
CASE
WHEN mi.IsRemovable ='Yes' THEN 1
WHEN mi.IsRemovable ='No' THEN 0
END ,
(SELECT ID from MachineTypes WHERE Name=mi.MachineType),
(SELECT ID from MachineItemTypes WHERE Name=mi.ItemType),
mi.CreatedBy
,mi.CreatedOn
FROM #MachineItems mi

Sql query to create teams

I need a query to assign teams to a series of users. Data looks like this:
UserId Category Team
1 A null
2 A null
3 B null
4 B null
5 A null
6 B null
8 A null
9 B null
11 B null
Teams should be created by sorting by userid and the first userid becomes the team number and the consecutive A's are part of that team as are the B's that follow. The first A after the Bs starts a new team. There will always be at least one A and one B. So after the update, that data should look like this:
UserId Category Team
1 A 1
2 A 1
3 B 1
4 B 1
5 A 5
6 B 5
8 A 8
9 B 8
11 B 8
EDIT:
Need to add that the user id's will not always increment by 1. I edited the example data to show what I mean. Also, the team ID doesn't strictly have to be the id of the first user, as long as they end up grouped properly. For example, users 1 - 4 could all be on team '1', users 5 and 6 on team '2' and users 8,9 and 11 on team '3'
First you could label each row with an increasing number. Then you can use a left join to find the previous user. If the previous user has category 'B', and the current one category 'A', that means the start of a new team. The team number is then the last UserId that started a new team before the current UserId.
Using SQL Server 2008 syntax:
; with numbered as
(
select row_number() over (order by UserId) rn
, *
from Table1
)
, changes as
(
select cur.UserId
, case
when prev.Category = 'B' and cur.Category = 'A' then cur.UserId
when prev.Category is null then cur.UserId
end as Team
from numbered cur
left join
numbered prev
on cur.rn = prev.rn + 1
)
update t1
set Team = team.Team
from Table1 t1
outer apply
(
select top 1 c.Team
from changes c
where c.UserId <= t1.UserId
and c.Team is not null
order by
c.UserId desc
) as team;
Example at SQL Fiddle.
You can do this with a recursive CTE:
with userCTE as
(
select UserId
, Category
, Team = UserId
from users where UserId = 1
union all
select users.UserId
, users.Category
, Team = case when users.Category = 'A' and userCTE.Category = 'B' then users.UserId else userCTE.Team end
from userCTE
inner join users on users.UserId = userCTE.UserId + 1
)
update users
set Team = userCTE.Team
from users
inner join userCTE on users.UserId = userCTE.UserId
option (maxrecursion 0)
SQL Fiddle demo.
Edit:
You can update the CTE to get this to go:
with userOrder as
(
select *
, userRank = row_number() over (order by userId)
from users
)
, userCTE as
(
select UserId
, Category
, Team = UserId
, userRank
from userOrder where UserId = (select min(UserId) from users)
union all
select users.UserId
, users.Category
, Team = case when users.Category = 'A' and userCTE.Category = 'B' then users.UserId else userCTE.Team end
, users.userRank
from userCTE
inner join userOrder users on users.userRank = userCTE.userRank + 1
)
update users
set Team = userCTE.Team
from users
inner join userCTE on users.UserId = userCTE.UserId
option (maxrecursion 0)
SQL Fiddle demo.
Edit:
For larger datasets you'll need to add the maxrecursion query hint; I've edited the previous queries to show this. From Books Online:
Specifies the maximum number of recursions allowed for this query.
number is a nonnegative integer between 0 and 32767. When 0 is
specified, no limit is applied.
In this case I've set it to 0, i.e. not limit on recursion.
Query Hints.
I actually ended up going with the following. It finished on all 3 million+ rows in a half an hour.
declare #userid int
declare #team int
declare #category char(1)
declare #lastcategory char(1)
set #userid = 1
set #lastcategory='B'
set #team=0
while #userid is not null
begin
select #category = category from users where userid = #userid
if #category = 'A' and #lastcategory = 'B'
begin
set #team = #userid
end
update users set team = #team where userid = #userid
set #lastcategory = #category
select #userid = MIN(userid) from users where userid > #userid
End

Resources