Count number of years since last deduction - sql

I have a table similar to below where the same account has its fiscal years (FY) and deductions for each year broken out in multiple rows. Accounts can range from 1 - 20+ years. How do I group to one unique row that shows the current year and how many years its been since the account had a deduction?
from this:
to this:
Started to utilize the CTE approach as I have in the past, but as before it started to get ugly and I know there has to be a simpler approach...

Assuming the current year is the most recent year, you would use aggregation:
select account, max(fy),
sum(case when fy = max_fy then deductions end) as this_year_deduction,
max(fy) - max(case when deduction < 0 then fy end) as years_since_deduction
from (select t.*, max(fy) over (partition by account) as max_fy
from t
) t
group by account;
Note: I assume the third column is the most recent deduction. The query uses a window function to extract that.

Haven't used the methods below but I think it is close to what is needed. Corrections welcome. (Code not tested)
with nonZeroes as
(
select * from YourTable where deductions <> 0
)
select Account,
FY,
FY - LAST_VALUE(FY) OVER (PARTITION BY Account
ORDER BY Year Desc
RANGE BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING) AS years_since_deductions
from nonZeroes

Related

How to conditional SQL select

My table consists of user_id, revenue, publish_month columns.
Right now I use group_by user_id and sum(revenue) to get revenue for all individual users.
Is there a single SQL query I can use to query for user revenue across a time period conditionally? If for a specific user, there is a row for this month, I want to query for this month, last month and the month before. If there is not yet a row for this month, I want to query for last month and the two months before.
Any advice with which approach to take would be helpful. If I should be using cases, if-elses with exists or if this is do-able with a single SQL query?
UPDATE---since I did a bad job of describing the question, I've come to include some example data and expected results
Where current month is not present for user 33
Where current month is present
Assuming publish_month is a DATE datatype, this should get the most recent three months of data per user...
SELECT
user_id, SUM(revenue) as s_revenue
FROM
(
SELECT
user_id, revenue, publish_month,
MAX(publish_month) OVER (PARTITION BY user_id) AS user_latest_publish_month
FROM
yourtableyoudidnotname
)
summarised
WHERE
publish_month >= DATEADD(month, -2, user_latest_publish_month)
GROUP BY
user_id
If you want to limit that to the most recent 3 months out of the last 4 calendar months, just add AND publish_month >= DATEADD(month, -3, DATE_TRUNC(month, GETDATE()))
The ambiguity here is why it is important to include a Minimal Reproducible Example
With input data and require results, we could test our code against your requirements
If you're using strings for the publish_month, you shouldn't be, and should fix that with utmost urgency.
You can use a windowing function to "number" the months. In this way the most recent one will have a value of 1, the prior 2, and the one before 3. Then you can only select the items with a number of 3 or less.
Here is how:
SELECT user_id, revienue, publish_month,
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY publish_month DESC) as RN
FROM yourtableyoudidnotname
now you just select the items with RN less than 3 and do your sum
SELECT user_id, SUM(revenue) as s_revenue
FROM (
SELECT user_id, revenue, publish_month,
ROW_NUMBER() OVER(PARTITION BY user_id ORDER BY publish_month DESC) as RN
FROM yourtableyoudidnotname
) X
WHERE RN <= 3
GROUP BY user_id
You could also do this without a sub query if you use the windowing function for SUM and a range, but I think this is easier to understand.
From the comment -- there could be an issue if you have months from more than one year. To solve this make the biggest number in the order by always the most recent. so instead of
ORDER BY publish_month DESC
you would have
ORDER BY (100*publish_year)+publish_month DESC
This means more recent years will always have a higher number so january of 2023 will be 202301 while december of 2022 will be 202212. Since january is a bigger number it will get a row number of 1 and december will get a row number of 2.

group by category to show difference by month in SQL

I have a view that brings the total number of members in each class for each month however I want to show the growth by subtracting the total_count of the previous month with the current month in a separate column. For example, I would like to view the data as mentioned in the attached image. Here on that table, you can see the total count for a membership class "doctor" in the month of December is 5 however in January it's 4 so when I look at this report on February I would like to have a separate column called "Growth" which will subtract the January total member class from December total member class and show the growth. When we move to March it will do the same by subtracting the total member class of February with January and show the difference in the growth column. Any help on how can I achieve it through a query would be greatly appreciated. I'm using MS SQL.
As I mentioned, if you were on a supported version of SQL Server (2008 has been completely unsupported for 18~ months) this would be very simple:
SELECT JoinedDate,
MembershipClass,
TotalCount,
TotalCount - LAG(TotalCount) OVER (PARTITION BY MembershipClass ORDER BY JoinedDate ASC) AS Growth
FROM dbo.YourTable;
Instead, you'll need to use ROW_NUMBER, and a self join, which will be far less performant:
WITH RNs AS(
SELECT JoinedDate,
MembershipClass,
TotalCount,
ROW_NUMBER() OVER (PARTITION BY MembershipClass ORDER BY JoinedDate ASC) AS RN
FROM dbo.YourTable)
SELECT RN1.JoinedDate,
RN1.MembershipClass,
RN1.TotalCount,
RN1.TotalCount - RN2.TotalCount AS Growth
FROM RNs RN1
LEFT JOIN RNs RN2 ON RN1.MembershipClass = RN2.MembershipClass
AND RN1.RN = RN2.RN + 1;

SQLlite getting min and max and average using nested queries

I have these tables
record: sid (string), cid (string), quarter (string), year (integer), grade(integer)
student: sid (string)
For every student who has taken at least one class, meaning a student is entered in the record table at least once, i need to get their GPA in the most recent quarter they were enrolled in. I need to display sid, quarter, year, and grade (gpa).
There are 3 quarters in a given calendar year, and it may be helpful to observe the order of the occurrence of quarters is in reverse alphabetical order ('W' > 'S' > 'F'). These stands for winter, spring, fall respectively. Fall being the latest quarter of the year.
this is what i came up with:
select sid, quarter, year, avg(grade) as gpa
from (select sid, min(quarter) as quarter, year, avg(grade) as grade
from (select *, max(year) as maxy
from record
group by sid)
group by sid)
group by sid;
this gives me the average grade for all quarters/years enrolled, and doesn't give me the latest quarter either.
I can only use functions such as NOT EXIST / EXIST, NOT IN/IN , group by, order by. I cannot use rank().
I was told that I should use NOT EXIST to get the latest quarter since the most recent quarter means for a specific quarter, there is no succeeding quarter.
any help would be greatly appreciated. thank you!
You want solution using not exists? Here you go.
Select t.*
From record t
Where not exists
(Select 1 from record tt
Where tt.year > t.year
And tt.quarter < t.quarter
And tt.sid = t.sid)
Above query will give you all the data of student for latest quarter, then you can use the aggregate function according to your requirement.
Use row_number():
select r.*
from (select r.*,
row_number() over (partition by sid
order by case quarter when 'W' then 1 when 'S' then 2 when 'F' then 3 else 4 end desc
) as seqnum
from records r
) r
where seqnum = 1;
Although this can be simplified a little bit by relying on the "alphabetical" ordering of quarter, I don't recommend that approach. Relying on alphabetic ordering for time periods is counterintuitive and will make the code harder to understand.
If the quarter were stored as a number then it would be appropriate for use in order by.

Summarizing consecutive values in T-SQL

I have a question regarding T-SQL:
I have a database of my insurance clients, who have a contractual obligation to pay the company insurance fee ever month. An example of the dataset is presented below:
I have the date and client_id and the overdue_flag. The later is a binary one: 0 if given client has no overdue payment and 1 if he/she has. The question: I would like to create a summary of overdue months (see image 2).
If its the first month the client is overdue then it should be 1, if second then 2 and so on. However, if the client comes clean (makes good on overdue payments) it should go back to 0, and if the same client is overdue again, the count of overdue months should restart the count from 1. In other words: I only would like to sum the consecutive overdue months.
Thanks in advance for the help!
Use a cumulative sum to define the groups by the number of non-overdue months before each row. Then row_number() for the overdue periods:
select t.*,
(case when overdue_flag = 1
then row_number() over (partition by client_id, grp, overdue_flag order by date)
end) as months_overdue
from (select t.*,
sum(1 - overdue_flag) over (partition by client_id order by date) as grp
from t
) t

Is there a way to count how many strings in a specific column are seen for the 1st time?

**Is there a way to count how many strings in a specific column are seen for
Since the value in the column 2 gets repeated sometimes due to the fact that some clients make several transactions in different times (the client can make a transaction in the 1st month then later in the next year).
Is there a way for me to count how many IDs are completely new per month through a group by (never seen before)?
Please let me know if you need more context.
Thanks!
A simple way is two levels of aggregation. The inner level gets the first date for each customer. The outer summarizes by year and month:
select year(min_date), month(min_date), count(*) as num_firsts
from (select customerid, min(date) as min_date
from t
group by customerid
) c
group by year(min_date), month(min_date)
order by year(min_date), month(min_date);
Note that date/time functions depends on the database you are using, so the syntax for getting the year/month from the date may differ in your database.
You can do the following which will assign a rank to each of the transactions which are unique for that particular customer_id (rank 1 therefore will mean that it is the first order for that customer_id)
The above is included in an inline view and the inline view is then queried to give you the month and the count of the customer id for that month ONLY if their rank = 1.
I have tested on Oracle and works as expected.
SELECT DISTINCT
EXTRACT(MONTH FROM date_of_transaction) AS month,
COUNT(customer_id)
FROM
(
SELECT
date_of_transaction,
customer_id,
RANK() OVER(PARTITION BY customer_id
ORDER BY
date_of_transaction ASC
) AS rank
FROM
table_1
)
WHERE
rank = 1
GROUP BY
EXTRACT(MONTH FROM date_of_transaction)
ORDER BY
EXTRACT(MONTH FROM date_of_transaction) ASC;
Firstly you should generate associate every ID with year and month which are completely new then count, while grouping by year and month:
SELECT count(*) as new_customers, extract(year from t1.date) as year,
extract(month from t1.date) as month FROM table t1
WHERE not exists (SELECT 1 FROM table t2 WHERE t1.id==t2.id AND t2.date<t1.date)
GROUP BY year, month;
Your results will contain, new customer count, year and month