Calculate year over year increase SQL Server 2008R2 - sql

Below is my table structure. I need to calculate rent for length of lease for each properties:
Let's look at PropertyID = 12077:
Area = 1280
StartDate = 2023-02-01
EndDate = 2027-10-31
BaseRent = 21.53
RentIncreasePercent = .04 (4 percent)
IncreaseRepeatMonths = 12 months (NOTE: First 12 months there won't be any increase)
Since this property lease starts and ends between year 2023 and 2028, I'd like to know (in separate row per year) amount of rent to be collected each year. This would take percent increase every 12 months (compound rent increase) into consideration.
Example:
21.53 * 1280 would give rent for first 12 months. However, lease started in February so year 2023 total rent amount would be = ((21.23 * 1280)/12) * 11
For year 2024, first month rent would be = (21.23 * 1280)/12 because rent only increases every 12 months. For next 11 months of 2024, rent would be ((12.23 * 1.04 * 1280)/12)* 11.
For year 2025, first month rent would be (12.23 * 1.04 *1280)/12). However, next 11 months of 2025 would be ((12.72 * 1.04 * 1280)/12)*11. 12.72 comes from compound increase.
How would I go about coming up with a view to do this? Most confusing part to me is not knowing how to accommodate for lease start date when it is not starting on January.
declare #table table
(
PropertyID int,
area int,
StartDate date,
EndDate date,
BaseRent decimal(12,2),
RentIncreaseBasis varchar(30),
RentIncreasePercent decimal(5,2),
IncreaseRepeatMonths int
)
insert #table values (12076, 5627, '2024-01-01', '2028-12-31', '16.52', '% Increase', 0.03, 12)
insert #table values (12077, 1280, '2023-02-01', '2027-10-31', '21.53', '% Increase', 0.04, 12)
insert #table values (12078, 1000, '2017-03-01', '2025-11-30', '23.52', '% Increase', 0.01, 12)
insert #table values (12079, 2000, '2020-02-01', '2024-09-30', '15.57', '% Increase', 0.05, 12)
insert #table values (12080, 3000, '2018-05-01', '2020-08-31', '18.58', '% Increase', 0.04, 12)
insert #table values (12081, 4000, '2019-08-01', '2020-12-31', '22.56', '% Increase', 0.03, 12)
insert #table values (12082, 5000, '2017-02-01', '2028-03-31', '19.53', '% Increase', 0.02, 12)
select * from #table

I recommend to use a calendar table which containts all the months from your table.
I hope my example will work in SQL 2008.
-- here is your code
-- the calendar table
DECLARE #MonthCalendar table(
[Month] date PRIMARY KEY
)
DECLARE #MinDate date,#MaxDate date
-- get min and max date
SELECT
#MinDate=MIN(StartDate),
#MaxDate=MAX(EndDate)
FROM #table
-- fill the calendar table
;WITH monthCTE AS(
SELECT CAST(#MinDate AS date) [Month]
UNION ALL
SELECT DATEADD(MONTH,1,[Month])
FROM monthCTE
WHERE [Month]<#MaxDate
)
INSERT #MonthCalendar([Month])
SELECT [Month]
FROM monthCTE
OPTION(MAXRECURSION 0);
-- final query
SELECT
*,
(BaseRent*Area*(1+RentIncreasePercent*IncreaseCount))/12 MonthRentAmount,
(1+RentIncreasePercent*IncreaseCount) TotalPercent
FROM
(
SELECT *,(ROW_NUMBER()OVER(PARTITION BY t.PropertyID ORDER BY m.[Month])-1)/12 IncreaseCount
FROM #table t
JOIN #MonthCalendar m ON m.[Month] BETWEEN t.StartDate AND t.EndDate
--WHERE t.PropertyID=12077
) q
-- query for total amounts by PropertyIDs and Years
SELECT
PropertyID,
YEAR(StartDate) [Year],
SUM((BaseRent*Area*(1+RentIncreasePercent*IncreaseCount))/12) YearRentAmount
FROM
(
SELECT *,(ROW_NUMBER()OVER(PARTITION BY t.PropertyID ORDER BY m.[Month])-1)/12 IncreaseCount
FROM #table t
JOIN #MonthCalendar m ON m.[Month] BETWEEN t.StartDate AND t.EndDate
--WHERE t.PropertyID=12077
) q
GROUP BY PropertyID,YEAR(StartDate)
ORDER BY PropertyID,[Year]

Related

SQL Server = Creating a stored procedure

CREATE TABLE dbo.HighScores
(
[User] varchar(255),
Score int,
DateAdded datetime
)
INSERT INTO dbo.HighScores
VALUES ('Bob', 2500, '2 Jan 2013 13:13'),
('Jon', 1500, '2 Jan 2013 13:15'),
('Amy', 3500, '2 Jan 2013 13:18'),
('Joe', 1750, '2 Jan 2013 13:23'),
('Don', 500, '2 Jan 2013 13:33'),
('Ann', 800, '2 Jan 2013 14:03'),
('Mav', 1200, '2 Jan 2013 15:13'),
('Ken', 2600, '2 Jan 2013 15:32'),
('Ace', 2500, '2 Jan 2013 16:45'),
('Tom', 2700, '2 Jan 2013 16:59'),
('Leo', 300, '2 Jan 2013 17:33'),
('Jay', 1000, '2 Jan 2013 18:03'),
('Roy', 1200, '2 Jan 2013 18:13'),
('Vic', 2100, '2 Jan 2013 19:32'),
('Ted', 1800, '2 Jan 2013 20:45'),
('Pat', 1400, '2 Jan 2013 20:59')
create a stored procedure called pr_GetHighScoreList
We now need to add highscores to the table using pr_PutHighScoreList
• You can only appear on the high score list once, and only your highest score must be stored.
• We would also need to record the movement in the highscore table, when players move up in position.
Example:
pr_PutHighScoreList 'Ann', 2750
Will increase Ann's score from 800 to 2750 and we need to record that she moved from position 14 to 2 in a separate table.
• Create the proc, and any other objects that may be required to store this.
This is what i did so far:
ALTER PROCEDURE [dbo].[pr_PutHighScoreList]
(
#name as nvarchar(20),
#score as int
)
AS
BEGIN
IF EXISTS
(SELECT * from [HighScores]
WHERE [User]=#name
AND [Score]>#score)
UPDATE [HighScores]
SET [User] = #name
,[Score] = #score
,[DateAdded] = getdate()
WHERE [User]=#name
ELSE
(SELECT * from [HighScores]
WHERE [User]!= #name)
INSERT INTO [HighScores]
([User]
,[Score]
,[DateAdded])
VALUES
(#name
,#score
,getdate())
END
Perhaps this will get you started
;with cte as (
Select *
,RN = row_number() over (order by score desc,dateAdded desc)
From HighScores
)
Select * From cte where RN<=5
Union
Select A.*
From cte A
Join (Select R1=RN-1,R2=RN+1 from cte where [User]='Ann' ) B on A.RN between B.R1 and B.R2
Order by RN
Results
I would use lag() and lead() for Ann and her neighbors. This is what a query looks like:
select *
from (select hs.*,
row_number() over (order by score desc, [User]) as seqnum,
lag([User]) over (order by score desc, [User]) as prev_user,
lead([User]) over (order by score desc, [User]) as next_user
from highscores hs
) hs
where seqnum <= 5 or
'Ann' in ([User], prev_user, next_user)
order by seqnum;
You can adapt it for a stored procedure.
Here is a db<>fiddle.

Get the list of year values based on the gap and year value in the table

Scenario: I have a table with Year and Gap columns. What I need the output as, starting from the given year value it incremented up to the value in the gap column.
i.e., If the YearVal is 2001, and Gap is 3, I need the output as
Result
--------
2001
2002
2003
What I have tried:
DECLARE #ResultYears TABLE (Gap INT, YearVal INT);
INSERT INTO #ResultYears (Gap, YearVal) VALUES (3, 2001);
;WITH FinalResult AS (
SELECT YearVal AS [YR] FROM #ResultYears
UNION ALL
SELECT [YR] + 1 FROM FinalResult
WHERE [YR] + 1 <= (SELECT YearVal + (Gap -1) FROM #ResultYears)
)
SELECT * FROM FinalResult;
db<>fiddle demo with one entry in the table.
Using the query above, I can achieve the expected result. But if the table have more than one entry, the query is not working.
i.e., If I have the entries in the table as below:
DECLARE #ResultYears TABLE (Gap INT, YearVal INT);
INSERT INTO #ResultYears (Gap, YearVal) VALUES
(3, 2001), (4, 2008), (1, 2014), (2, 2018);
How can I modify the query to achieve my expected result?
db<>fiddle demo with more than one entry in the table.
Is this what you're after?
DECLARE #ResultYears TABLE (Gap INT, YearVal INT);
INSERT INTO #ResultYears (Gap, YearVal) VALUES
(3, 2001), (4, 2008), (1, 2014), (2, 2018);
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS I
FROM N N1, N N2), --100 is more than enough
Years AS(
SELECT RY.YearVal + T.I AS [Year],
RY.Gap,
RY.YearVal
FROM #ResultYears RY
JOIN Tally T ON RY.Gap > T.I)
SELECT *
FROM Years Y
ORDER BY Y.YearVal;
Personally I prefer a tally table over a rCTE; they are far quicker, especially with large datasets, or where the rCTE would have to do a high volume of recursion.
Demo on db<>fiddle
Initially Create one user defined table type function which return the Gap years
CREATE FUNCTION [dbo].[ufn_GetYears]
(
#i_Gap INT,#Year INT
)
RETURNS #Temp TABLE
(
Years INT
)
AS
BEGIN
;WITH CTE
AS
(
SELECT 1 AS Seq,DATEFROMPARTS ( #Year,01,01) AS Years
UNION ALL
SELECT seq +1,DATEADD(YEAR,1,Years)
FROM Cte
WHERE Seq < #i_Gap
)
INSERT INTO #Temp
SELECT DATEPART(YEAR,Years )
FROM CTE
RETURN
END
Sample Data
DECLARE #ResultYears TABLE
(Gap INT,
YearVal INT
);
INSERT INTO #ResultYears (Gap, YearVal) VALUES
(3, 2001), (4, 2008), (1, 2014), (2, 2018);
Sql Query to get the expected result using CROSS APPLY
SELECT R.Gap,dt.Years
FROM #ResultYears R
CROSS APPLY [dbo].[ufn_GetYears](R.Gap,R.YearVal) AS dt
Result
Gap Years
---------
3 2001
3 2002
3 2003
4 2008
4 2009
4 2010
4 2011
1 2014
2 2018
2 2019
If for a reason, you prefer recursive CTE (which is definetly slower)
DECLARE #ResultYears TABLE (Gap INT, YearVal INT);
INSERT INTO #ResultYears (Gap, YearVal) VALUES (3, 2001), (4, 2008), (1, 2014), (2, 2018);
;WITH FinalResult AS (
SELECT YearVal, Gap, YearVal [YR] FROM #ResultYears
UNION ALL
SELECT YearVal, Gap, [YR] + 1
FROM FinalResult
WHERE [YR] + 1 <= YearVal + (Gap -1)
)
SELECT * FROM FinalResult
ORDER BY [YR];
You need to keep original row parameters in the recursive part. this way recursion runs as desired.

Need query for somewhat complicated aggregation

I haven't found a way to solve this (in an elegant way), so I'd like to ask for your help with calculating the average week-hours for specific month.
declare #person table (pers_id int, [from] date, [to] date, hrs decimal(4, 2));
insert into #person values (72, '2017-09-01', '2017-11-13', 20);
insert into #person values (72, '2017-11-14', null, 35);
declare #months table (ym date);
insert into #months values ('2017-09-01');
insert into #months values ('2017-10-01');
insert into #months values ('2017-11-01');
insert into #months values ('2017-12-01');
/* so I need a query whouch would output average week-hours: */
2017-09-01 = 20
2017-10-01 = 20
2017-11-01 = 28.5
= (13/30)*20 + (17/30)*35 ; (assumed each month has 30 days)
2017-12-01 = 35
Anybody willing to help me out?
(am on Azure SQL)
This assumes at most one change in hours per month. If you have more, then you'd need more complex calculations around the proportions
select
start,
round(sum(
case
when [from]>[start] then (datediff(d,[from],finish)+1) * hrs/days
when [to]<[finish] then (datediff(d,start,[to])+1) * hrs/days
else hrs end
),2)
from
(select ym as start, EOMONTH(ym) as finish,
30
--datepart(d, EOMONTH(ym))
as days from #months) months
inner join ( select pers_id, [from], isnull([to], EOMONTH(getdate()))as[to], hrs from #person) p
on months.finish>=p.[from] and months.start<=p.[to]
group by start

SQL Server: select newest rows who's sum matches a value

Here is a table...
ID QTY DATE CURRENT_STOCK
----------------------------------
1 1 Jan 30
2 1 Feb 30
3 2 Mar 30
4 6 Apr 30
5 8 May 30
6 21 Jun 30
I need to return the newest rows whose summed qty equal or exceed the current stock level, excluding any additional rows once this total has been reached, so I am expecting to see just these rows...
ID QTY DATE CURRENT_STOCK
----------------------------------
4 6 Apr 30
5 8 May 30
6 21 Jun 30
I am assuming I need a CTE (Common Table Expression) and have looked at this question but cannot see how to translate that to my requirement.
Help!?
Declare #YourTable table (ID int,QTY int,DATE varchar(25), CURRENT_STOCK int)
Insert Into #YourTable values
(1 ,1 ,'Jan' ,30),
(2 ,1 ,'Feb' ,30),
(3 ,2 ,'Mar' ,30),
(4 ,6 ,'Apr' ,30),
(5 ,8 ,'May' ,30),
(6 ,21 ,'Jun' ,30)
Select A.*
From #YourTable A
Where ID>= (
Select LastID=max(ID)
From #YourTable A
Cross Apply (Select RT = sum(Qty) from #YourTable where ID>=A.ID) B
Where B.RT>=CURRENT_STOCK
)
Returns
ID QTY DATE CURRENT_STOCK
4 6 Apr 30
5 8 May 30
6 21 Jun 30
One way to do it with your provided data set
if object_id('tempdb..#Test') is not null drop table #Test
create table #Test (ID int, QTY int, Date_Month nvarchar(5), CURRENT_STOCK int)
insert into #Test (ID, QTY, Date_Month, CURRENT_STOCK)
values
(1, 1, 'Jan', 30),
(2, 1, 'Feb', 30),
(3, 2, 'Mar', 30),
(4, 6, 'Apr', 30),
(5, 8, 'May', 30),
(6, 21, 'Jun', 30)
if object_id('tempdb..#Finish') is not null drop table #Finish
create table #Finish (ID int, QTY int, Date_Month nvarchar(5), CURRENT_STOCK int)
declare #rows int = (select MAX(ID) from #Test)
declare #stock int = (select MAX(CURRENT_STOCK) from #Test)
declare #i int = 1
declare #Sum int = 0
while #rows > #i
BEGIN
select #Sum = #Sum + QTY from #Test where ID = #rows
IF (#SUM >= #stock)
BEGIN
set #i = #rows + 1 -- to exit loop
END
insert into #Finish (ID, QTY, Date_Month, CURRENT_STOCK)
select ID, QTY, Date_Month, CURRENT_STOCK from #Test where ID = #rows
set #rows = #rows - 1
END
select * from #Finish
Setup Test Data
-- Setup test data
CREATE TABLE #Stock
([ID] int, [QTY] int, [DATE] varchar(3), [CURRENT_STOCK] int)
;
INSERT INTO #Stock
([ID], [QTY], [DATE], [CURRENT_STOCK])
VALUES
(1, 1, 'Jan', 30),
(2, 1, 'Feb', 30),
(3, 2, 'Mar', 30),
(4, 6, 'Apr', 30),
(5, 8, 'May', 30),
(6, 21, 'Jun', 30)
;
Solution for SQL Server 2012+
If you have a more recent version of SQL server which supports full window function syntax, you can do it look this:
-- Calculate a running total of qty by Id descending
;WITH stock AS (
SELECT *
-- This calculates the SUM over a 'window' of rows based on the first
-- row in the result set through the current row, as specified by the
-- ORDER BY clause
,SUM(qty) OVER(ORDER BY Id DESC
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW) AS TotalQty
FROM #Stock
),
-- Identify first row in mininum set that matches or exceeds CURRENT_STOCK
first_in_set AS (
SELECT TOP 1 *
FROM stock
WHERE TotalQty >= CURRENT_STOCK
)
-- Fetch matching set
SELECT *
FROM #stock
WHERE Id >= (SELECT Id FROM first_in_set)
Solution for SQL Server 2008
For SQL Server 2008, which only has basic support for window functions, you can calculate the running total using CROSS APPLY:
-- Calculate a running total of qty by Id descending
;WITH stock AS (
SELECT *
-- This window function causes the results of this query
-- to be sorted in descending order by Id
,ROW_NUMBER() OVER(ORDER BY Id DESC) AS sort_order
FROM #Stock s1
-- CROSS APPLY 'applies' the query (or UDF) to every row in a result set
-- This CROSS APPLY query produces a 'running total'
CROSS APPLY (
SELECT SUM(Qty) AS TotalQty
FROM #Stock s2
WHERE s2.Id >= s1.id
) total_calc
WHERE TotalQty >= s1.CURRENT_STOCK
),
-- Identify first row in mininum set that matches or exceeds CURRENT_STOCK
first_in_set AS (
SELECT TOP 1 Id
FROM stock
WHERE sort_order = 1
)
-- Fetch matching set
SELECT *
FROM #stock
WHERE Id >= (SELECT Id
FROM first_in_set)

SQL count exposure of life time by age

(Using SQL Server 2008)
I need some help visualizing a solution. Let's say I have the following simple table for members of a pension scheme:
[Date of Birth] [Date Joined] [Date Left]
1970/06/1 2003/01/01 2007/03/01
I need to calculate the number of lives in each age group from 2000 to 2009.
NOTE: "Age" is defined as "age last birthday" (or "ALB") on 1 January of each of those yeasrs. e.g. if you are exactly 41.35 or 41.77 etc. years old on 1/1/2009 then you would be ALB 41.
So if the record above were the only entry in the database, then the output would be something like:
[Year] [Age ] [Number of Lives]
2003 32 1
2004 33 1
2005 34 1
2006 35 1
2007 36 1
(For 2000, 2001, 2002, 2008 and 2009 there are no lives on file since the sole member only joined on 1/1/2003 and left on 1/3/2007)
I hope I am making myself clear enough.
Anyone have any suggestions?
Thanks, Karl
[EDIT]
Adding another layer to the problem:
What if I had:
[Date of Birth] [Date Joined] [Date Left] [Gender] [Pension Value]
1970/06/1 2003/01/01 2007/03/01 'M' 100,000
and I want the output to be:
[Year] [Age ] [Gender] sum([Pension Value]) [Number of Lives]
2003 32 M 100,000 1
2004 33 M 100,000 1
2005 34 M 100,000 1
2006 35 M 100,000 1
2007 36 M 100,000 1
Any ideas?
WITH years AS
(
SELECT 1900 AS y
UNION ALL
SELECT y + 1
FROM years
WHERE y < YEAR(GETDATE())
),
agg AS
(
SELECT YEAR(Dob) AS Yob, YEAR(DJoined) AS YJoined, YEAR(DLeft) AS YLeft
FROM mytable
)
SELECT y, y - Yob, COUNT(*)
FROM agg
JOIN years
ON y BETWEEN YJoined AND YLeft
GROUP BY
y, y - Yob
OPTION (MAXRECURSION 0)
People born on same year always have the same age in your model
That's why if they go at all, they always go into one group and you just need to generate one row per year for the period they stay in the program.
You can try something like this
DECLARE #Table TABLE(
[Date of Birth] DATETIME,
[Date Joined] DATETIME,
[Date Left] DATETIME
)
INSERT INTO #Table ([Date of Birth],[Date Joined],[Date Left]) SELECT '01 Jun 1970', '01 Jan 2003', '01 Mar 2007'
INSERT INTO #Table ([Date of Birth],[Date Joined],[Date Left]) SELECT '01 Jun 1979', '01 Jan 2002', '01 Mar 2008'
DECLARE #StartYear INT,
#EndYear INT
SELECT #StartYear = 2000,
#EndYear = 2009
;WITH sel AS(
SELECT #StartYear YearVal
UNION ALL
SELECT YearVal + 1
FROM sel
WHERE YearVal < #EndYear
)
SELECT YearVal AS [Year],
COUNT(Age) [Number of Lives]
FROM (
SELECT YearVal,
YearVal - DATEPART(yy, [Date of Birth]) - 1 Age
FROM sel LEFT JOIN
#Table ON DATEPART(yy, [Date Joined]) <= sel.YearVal
AND DATEPART(yy, [Date Left]) >= sel.YearVal
) Sub
GROUP BY YearVal
Try the following sample query
SET NOCOUNT ON
Declare #PersonTable as Table
(
PersonId Integer,
DateofBirth DateTime,
DateJoined DateTime,
DateLeft DateTime
)
INSERT INTO #PersonTable Values
(1, '1970/06/10', '2003/01/01', '2007/03/01'),
(1, '1970/07/11', '2003/01/01', '2007/03/01'),
(1, '1970/03/12', '2003/01/01', '2007/03/01'),
(1, '1973/07/13', '2003/01/01', '2007/03/01'),
(1, '1972/06/14', '2003/01/01', '2007/03/01')
Declare #YearTable as Table
(
YearId Integer,
StartOfYear DateTime
)
insert into #YearTable Values
(1, '1/1/2000'),
(1, '1/1/2001'),
(1, '1/1/2002'),
(1, '1/1/2003'),
(1, '1/1/2004'),
(1, '1/1/2005'),
(1, '1/1/2006'),
(1, '1/1/2007'),
(1, '1/1/2008'),
(1, '1/1/2009')
;WITH AgeTable AS
(
select StartOfYear, DATEDIFF (YYYY, DateOfBirth, StartOfYear) Age
from #PersonTable
Cross join #YearTable
)
SELECT StartOfYear, Age, COUNT (1) NumIndividuals
FROM AgeTable
GROUP BY StartOfYear, Age
ORDER BY StartOfYear, Age
First some preparation to have something to test with:
CREATE TABLE People (
ID int PRIMARY KEY
,[Name] varchar(50)
,DateOfBirth datetime
,DateJoined datetime
,DateLeft datetime
)
go
-- some data to test with
INSERT INTO dbo.People
VALUES
(1, 'Bob', '1961-04-02', '1999-01-01', '2007-05-07')
,(2, 'Sadra', '1960-07-11', '1999-01-01', '2008-05-07')
,(3, 'Joe', '1961-09-25', '1999-01-01', '2009-02-11')
go
-- helper table to hold years
CREATE TABLE dimYear (
CalendarYear int PRIMARY KEY
)
go
-- fill-in years for report
DECLARE
#yr int
,#StartYear int
,#EndYear int
SET #StartYear = 2000
SET #EndYear = 2009
SET #yr = #StartYear
WHILE #yr <= #EndYear
BEGIN
INSERT INTO dimYear (CalendarYear) values(#yr)
SET #yr =#yr+1
END
-- show test data and year tables
select * from dbo.People
select * from dbo.dimYear
go
Then a function to return person's age for each year, if the person is still an active member.
-- returns [CalendarYear], [Age] for a member, if still active member in that year
CREATE FUNCTION dbo.MemberAge(#DateOfBirth datetime, #DateLeft datetime)
RETURNS TABLE
AS
RETURN (
SELECT
CalendarYear,
CASE
WHEN DATEDIFF(dd, cast(CalendarYear AS varchar(4)) + '-01-01',#DateLeft) > 0
THEN DATEDIFF(yy, #DateOfBirth, cast(CalendarYear AS varchar(4)) + '-01-01')
ELSE -1
END AS Age
FROM dimYear
);
go
And the final query:
SELECT
a.CalendarYear AS "Year"
,a.Age AS "Age"
,count(*) AS "Number Of Lives"
FROM
dbo.People AS p
CROSS APPLY dbo.MemberAge(p.DateOfBirth, p.DateLeft) AS a
WHERE a.Age > 0
GROUP BY a.CalendarYear, a.Age
Deal with this in pieces (some random thoughts) - create views to test you dev steps if you can:
ALB - do a query that, for a given year, gives you your memeber's ALB
Member in year - another bit of query that tell you whether a member was a member in a given year
Put those two together and you should be able to create a query that says whether a person was a member in a given year and what their ALB was for that year.
Hmm, tricky - following this chain of thought what you'd then want to do is generate a table that has all the years the person was a member and their ALB in that year (and a unique id)
From 4. select year, alb, count(id) group by year, alb
I'm not sure I'm going in the right direction from about 3 though it should work.
You may find a (temporary) table of years helpful - joining things to a table of dates makes all kinds of things possible.
Not really an answer, but certainly some direction...