Sum over N days excluding Weekends and Holidays - sql

I have below table
AccountID
Date
Amount
123
07/02/2021
2000
123
07/09/2021
9000
123
07/15/2021
500
123
07/20/2021
500
123
07/28/2021
500
I am trying to create a test script to test data for just one month(July). I want to sum the amount over 5 days where 5 days does not count weekends and holidays. Since it is month of July the holiday falls on July 5th 2021(07/05/2021).
The output should look something like below
AccountID
Date
Amount
123
07/02/2021
11000
123
07/09/2021
9500
123
07/15/2021
1000
123
07/20/2021
500
123
07/28/2021
500
Below is the table create and data insert statements for reference :-
create table TRANSACTIONS (
AccountID int,
Date date,
Amount int
)
insert into TRANSACTIONS values (123, '07/02/2021', 2000)
insert into TRANSACTIONS values (123, '07/09/2021', 9000)
insert into TRANSACTIONS values (123, '07/15/2021', 500)
insert into TRANSACTIONS values (123, '07/20/2021', 500)
insert into TRANSACTIONS values (123, '07/28/2021', 500)
I was able to create script that could sum over 5 days with skipping weekends(Saturday and Sunday). I am not able to think how can I skip the holiday on July 5th, 2021. I am fine with hardcoding it since this is just for testing purposes. The code 'DATEPART(WEEKDAY, h2.Date) not in (1, 7)' skips Weekend and 'DATEADD(d, 6, h1.Date)' here I am adding 6 and not 5 even the sum should be for over 5 days because after reading some articles I figured that in skipping weekends the last day is not inclusive so used 6 instead of 5. This code adds perfectly over 5 days skipping weekends
SELECT AccountId, Date,
(
SELECT SUM(Amount)
FROM TRANSACTIONS h2
WHERE
h1.AccountID = h2.AccountID and
DATEPART(WEEKDAY, h2.Date) not in (1, 7) and
h2.Date between h1.Date AND DATEADD(d, 6, h1.Date)
) as SumAmount
FROM TRANSACTIONS h1

The only sane way to tackle this is to have a calendar table to represent holidays. The easiest approach is to store every date for the date range you're likely to need (eg 1970-2030) with the type of the date, perhaps and enum of WORKDAY, WEEKEND, HOLIDAY or whatever works, eg
CREATE TABLE CALENDAR (
Date DATE,
Day_type varchar(16)
);
-- insert rows for dates you care about
Depending on where you live, you may need to include a region column too (typically the country and/or state).
With such a table, you join to it:
SELECT
AccountId,
DATEADD(DAY, (DATEDIFF(DAY, 0, t.Date)/7)*7 + 7, 0) as Date,
SUM(Amount)
FROM TRANSACTIONS t
JOIN CALENDAR c on t.Date = c.Date
AND c.day_type = 'WORKDAY'
WHERE t.Date BETWEEN <your date range>
GROUP BY AccountId, DATEADD(DAY, (DATEDIFF(DAY, 0, t.Date)/7)*7 + 7, 0)

Related

Determine if a class falls on a holiday, given the start and end dates of the class and the dates of all holidays

I have 2 tables in a database, the first has the following kind of information in it
SECTION_NUMBER
SECTION_ID
MEETING_ID
DAY_TYPE
MEETING_NUMBER
DATE_TIME_BEGIN
DATE_TIME_END
390
166316
102451
1
1
2023-01-23 9:30:00
2023-05-17 10:50:00
390
166316
102451
3
1
2023-01-23 9:30:00
2023-05-17 10:50:00
655
166314
102452
3
1
2023-01-23 12:00:00
2023-05-20 12:00:00
655
166314
102452
7
1
2023-01-23 12:00:00
2023-05-20 12:00:00
283
166315
102453
7
1
2023-01-23 12:00:00
2023-05-20 12:00:00
Of note, a section will have 1 entry for each day the section meets, 1 being monday, 2 tuesday, etc.
In the example screenshot, section 390 meets monday and wednesday, and the first meeting day is 1/23/23 with the end being 5/17/23.
I have a second table with holidays in it
description
DATE_VALUE
DayOfWeek
Day
Winter Break
2023-01-02 0:00:00
1
M
MLK Day
2023-01-16 0:00:00
1
M
Lincoln's Day
2023-02-17 0:00:00
5
F
Non-Teaching Day
2023-02-18 0:00:00
6
S
Washington's Day
2023-02-20 0:00:00
1
M
I have a third table which, for a section number, shows its meeting days. However, it does not take into account holidays.
section_number
NbrOfDays
360
33
655
16
I tried solving the issue in python but then found out that SSRS only supports python scripts in the 2017 version and at my work we are using 2016.
What kind of SQL Server 2016 queries exist that would somehow allow me to iterate through the section rows, and check if a holiday falls between the start and end date, and falls on the day the section meets, and decrement the meeting days by 1 for each holiday which meets that criteria?
Using the data as an example, there are 2 Monday holidays between the section 390 begin and end date, so the NbrOfDays from the third table needs to be updated to 31 from 33 since it has a meeting day type of 1 = Monday.
I don't think the numbers you state quite add up based on your sample data. e.g. in your holiday table, there is only one date that falls between 2023-01-23 and 2023-05-17, not two as you stated.
I'm not sure how you get 16 as the total for section 655 either unless you are not counting weekends?
anyway... Don't worry about the length of this answer the actual answer bit is only a few lines lines of code.
Anyway, I think that you can just create a view that will work this out for you. There is no need for the third table, the view will replace the third table.
Apologies if I'm adding comments that assume you are not familiar with basic querying, I'm only assuming this from the approach you wanted to take.
Set up data to replicate your sample
I first created your sample data with the following using temp tables called #meets and #hols.
CREATE TABLE #meets(SECTION_NUMBER int, SECTION_ID int, MEETING_ID int, DAY_TYPE int, MEETING_NUMBER int, DATE_TIME_BEGIN DATETIME, DATE_TIME_END datetime)
INSERT INTO #meets VALUES
(390, 166316, 102451, 1, 1, '2023-01-23 9:30:00', '2023-05-17 10:50:00'),
(390, 166316, 102451, 3, 1, '2023-01-23 9:30:00', '2023-05-17 10:50:00'),
(655, 166314, 102452, 3, 1, '2023-01-23 12:00:00', '2023-05-20 12:00:00'),
(655, 166314, 102452, 7, 1, '2023-01-23 12:00:00', '2023-05-20 12:00:00'),
(283, 166315, 102453, 7, 1, '2023-01-23 12:00:00', '2023-05-20 12:00:00')
CREATE TABLE #hols([description] varchar(30), DATE_VALUE date, DayOfWeek int, Day char(1))
INSERT INTO #hols VALUES
('Winter Break' , '2023-01-02', 1, 'M'),
('MLK Day' , '2023-01-16', 1, 'M'),
('Lincoln''s Day' , '2023-02-17', 5, 'F'),
('Non-Teaching Day' , '2023-02-18', 6, 'S'),
('Washington''s Day' , '2023-02-20', 1, 'M')
Add a date/calendar table
Then I created a date table. You may already have one so use that if you do but if not, create one in your database as they are incredibly useful for things like this.
This post shows how to create one CreatingADateTable
I've included the code here in case the link is dead.
-- prevent set or regional settings from interfering with
-- interpretation of dates / literals
SET DATEFIRST 1 -- 1 = Monday, 7 = Sunday
DECLARE #StartDate date = '20100101'; -- << change this if required
DECLARE #CutoffDate date = DATEADD(DAY, -1, DATEADD(YEAR, 30, #StartDate));
;WITH seq(n) AS
(
SELECT 0 UNION ALL SELECT n + 1 FROM seq
WHERE n < DATEDIFF(DAY, #StartDate, #CutoffDate)
),
d(d) AS
(
SELECT DATEADD(DAY, n, #StartDate) FROM seq
),
src AS
(
SELECT
TheDate = CONVERT(date, d),
TheDay = DATEPART(DAY, d),
TheDayName = DATENAME(WEEKDAY, d),
TheWeek = DATEPART(WEEK, d),
TheISOWeek = DATEPART(ISO_WEEK, d),
TheDayOfWeek = DATEPART(WEEKDAY, d),
TheMonth = DATEPART(MONTH, d),
TheMonthName = DATENAME(MONTH, d),
TheQuarter = DATEPART(Quarter, d),
TheYear = DATEPART(YEAR, d),
TheFirstOfMonth = DATEFROMPARTS(YEAR(d), MONTH(d), 1),
TheLastOfYear = DATEFROMPARTS(YEAR(d), 12, 31),
TheDayOfYear = DATEPART(DAYOFYEAR, d)
FROM d
)
SELECT *
INTO myDateTable -- << CHANGE TABLE NAME HERE IF YOU NEED TO
FROM src
ORDER BY TheDate
OPTION (MAXRECURSION 0);
Now the answer!
The following will give you each section_number, the day of the week for the meeting and the number of days
SELECT
SECTION_NUMBER
, TheDayName
, NbrOfDays = COUNT(*)
FROM #meets m
JOIN myDateTable d on d.TheDate BETWEEN CAST(m.DATE_TIME_BEGIN as date) AND CAST(m.DATE_TIME_END as date) and m.DAY_TYPE = d.TheDayOfWeek
LEFT JOIN #hols h on d.TheDate = h.DATE_VALUE
WHERE h.DATE_VALUE IS NULL
and d.TheDate >=CAST(GETDATE() as Date) -- optionaly if you want ignore past meetings
GROUP BY SECTION_NUMBER, DayOfWeek, TheDayName
ORDER BY SECTION_NUMBER, DayOfWeek
All this does is join every date in the myDateTable table to the #meets table where the dates fall between the start and end dates in the #meets table, it also joins on the day_type so only matching days are returned. It then left joins to the #hols table and then we only include dates where no match was found in the #hols table. Then we simply group the results and count how many records are in each group.
gives us this
If you just want results to look like your example, we can just remove the DayOfWeek grouping like this.
SELECT
SECTION_NUMBER
, NbrOfDays = COUNT(*)
FROM #meets m
JOIN myDateTable d on d.TheDate BETWEEN CAST(m.DATE_TIME_BEGIN as date) AND CAST(m.DATE_TIME_END as date) and m.DAY_TYPE = d.TheDayOfWeek
LEFT JOIN #hols h on d.TheDate = h.DATE_VALUE
WHERE h.DATE_VALUE IS NULL
and d.TheDate >=CAST(GETDATE() as Date) -- optionaly if you want ignore past meetings
GROUP BY SECTION_NUMBER
ORDER BY SECTION_NUMBER
which gives us this...
I've left a line in there to filter out past meetings but you can comment that out if you don't need it.
If you want to turn these queries into permanent views then you can do that with something like
CREATE VIEW MeetingCountBySectionAndDay AS
[copy query from above here]
Then you can just query the view like a table with something like
SELECT * FROM MeetingCountBySectionAndDay
If holidays are added/removed or meetings are added/edited, the view will automatically reflect the changes without you needing to do any work.

Get the ranges from the table on the basis of value

I have some specific transaction count value and I have to check if that count lies within what range. The ranges are specified in the below table. For example, if the count value is >= 1 AND less than 250001 from the range_start column then the count lies with range_id 1.
The tricky part is that if the transaction count on the first day of the month is greater than 1 and less than 31 and lies in range_id 3 then I have to divide the count into 3 bands for example if I have 30 transactions on the first day of the month then I will calculate the fees for the 10 transactions on the basis of range_id 1, and for the other 10 the fees would be calculated on the basis of range_id 2 and the remaining 10 transactions would be calculated on the range_id 3. Now on the second day of the month fees calculation would start from band 3 and it will keep moving to the next bands as the transaction volume would increase.
More Exaplanation:
Fees calculation is like this:
Total Transactions on first day of the month are:30
Auth Fees for First 10 transaction would be 10 * 0.1698 (range_id 1) = 1.698
Auth Fees the other 10 transactions would be 10 * 0.1536 (range_id 2)= 1.536
Auth Fees the other 10 transactions would be 10 * 0.1403 (range_id 3)= 1.403
Total Transactions on the second day of the month are : 20
Auth Fees for first 10 transactions would be 10 * 1.403 (range_id 4) = 14.03
Auth Fees for second 10 transactions would be 10 * 0.1036 (range_id 5) = 1.036
Total Transactions on the third day of the month are : 5
Auth Fees for 5 transactions would be 5 * 1.036 (range_id 6) = 5.18
I am saving the daily transaction count in a table, for example for the first day it would be 30,
for the second day it would be 50 and for the third day it would be 55 and on the first day of the month it would be reset to 0.
The fees calculation is daily and the transaction table will only have data for one day and at the end of the calculation, the transaction table is dropped, So I am keeping the total count for the previous business day in the table(gstl_daily_volume) as well in order to calculate the range_id next day.
I have achieved the band calculation with the below queries but the problem with this is that it can only calculate the fees accurately for one day and for the second day it starts again from the range_id 1 as it is not considering the volume of the previous day from gstl_daily_volume table. Please help me understand how can I continue on the second day from where I left off on first day of the month while considering the volume of the previous day.
declare #rangeTable TABLE
(
rangeId INT,
rangeStart INT,
rangeEnd INT NULL,
authFees decimal(8,4),
settlementFees decimal(8,4),
declinedFees decimal(8,4)
)
insert into #rangeTable
values
(1, 1, 11, 0.1698, 0.1359, 0.3284),
(2, 11, 21, 0.1536, 0.1536, 0.3280),
(3, 21, 31, 0.1403, 0.1330, 0.3278),
(4, 31, 41, 0.1203, 0.1320, 0.3276),
(5, 41, 51, 0.1036, 0.1310, 0.3274),
(6, 51, NULL, 0.0873, 0.1300, 0.3272)
declare #transactionsTable TABLE
(
transactionId INT IDENTITY(1,1),
transactionDate DateTime,
transactionAmount decimal(8,2)
)
insert into #transactionsTable
values
(N'2020-12-01', 500),
(N'2020-12-01', 501)
--- calculate per date total transaction fees
select
Datee = CAST(C.transactionDate AS DATE),
TotalSettlememtFee = SUM(C.settlementFees)
From
(
select
*
-- each transaction counts as 1
--,AuthFee = 1 * B.authFees
--,SettlememtFee = 1 * settlementFees
from
(
-- set a row number or transaction number per transaction which resets every month
select
rowNumber = ROW_NUMBER() OVER(PARTITION BY DATEPART(MONTH, transactionDate), DATEPART(YEAR, transactionDate) ORDER BY transactionDate),
*
from #transactionsTable tt
) A
-- cross apply to calculate which range for each transaction
CROSS APPLY
(
select
*
From #rangeTable rtt
where A.rowNumber >= rtt.rangeStart
AND A.rowNumber < rtt.rangeEnd
) B
) C
-- group by date to get the per date fees
group by CAST(C.transactionDate AS DATE)
Daily Volume table where The total count would be saved for each day:
GO
/****** Object: Table [dbo].[gstl_daily_volume] Script Date: 1/4/2021 5:50:08 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[gstl_daily_volume](
[business_date] [datetime] NULL,
[record_type] [varchar](50) NULL,
[total_count] [varchar](50) NULL,
[band_id] [bigint] NULL
) ON [PRIMARY]
GO
Please help, thanks in advance.
As the transactions table is a staging table, i.e. it only has data for current day - and gets empty end of the day.
I think if you add the previous days transaction count in your calculated row number this will work as expected.
DECLARE #previousDayTransactionCount INT;
-- TODO: set this to MAX from gstl_daily_volume for current month
--- calculate per date total transaction fees
select
Datee = CAST(C.transactionDate AS DATE),
TotalSettlememtFee = SUM(C.settlementFees)
From
(
select
*
-- each transaction counts as 1
--,AuthFee = 1 * B.authFees
--,SettlememtFee = 1 * settlementFees
from
(
-- set a row number or transaction number per transaction which resets every month
-- Then Add it here to current calculated rowNumber
select
rowNumber = #previousDayTransactionCount + (ROW_NUMBER() OVER(PARTITION BY DATEPART(MONTH, transactionDate), DATEPART(YEAR, transactionDate) ORDER BY transactionDate)),
*
from #transactionsTable tt
) A
-- cross apply to calculate which range for each transaction
CROSS APPLY
(
select
*
From #rangeTable rtt
where A.rowNumber >= rtt.rangeStart
AND A.rowNumber < rtt.rangeEnd
) B
) C
-- group by date to get the per date fees
group by CAST(C.transactionDate AS DATE)

Removing duplicates from 'over partition by'

Im using over partition by clause to calculate peoples monthly figures.
A short example of my results:
Date Person Team Daily Figure Month To Date
24/09/17 James 2 50 200
24/09/17 James 2 50 200
25/09/17 James 2 50 200
25/09/17 James 2 50 200
I am calculating the monthly figure by partitioning the daily figure over the person and the month e.g.
CASE
WHEN
MONTH([DATE]) = MONTH(getdate())
THEN SUM(Figure)
OVER (PARTITION BY [Name],
MONTH(DATE]))
ELSE 0
END AS 'Month To Date'
The main issue im having is i only want to display todays daily figure but with the whole month value. I group the figure for each person and limit to today but to be able to group by person i need to SUM the month to date figure which obviously leaves me with
Date Person Team Daily Figure Month To Date
25/09/17 James 2 100 800
The daily figure is correct but its obviously summing the duplicated rows which gives me an incorrect figure.
The ideal result for today would be:
Date Person Team Daily Figure Month To Date
25/09/17 James 2 100 200
with no duplicated rows.
Has anyone got any advice on this? To basically remove duplicated partitioned rows so i just get the correct month to date figure grouped
UPDATE:
Each row is just an individual figure for each person. Nothing is grouped so each person could have atleast 20 separate rows for figures on each day
Something like this?
declare #t table (Date date, Person varchar(100), Team int, [Daily Figure] int);
insert into #t values
('20170924', 'James', 2, 50),
('20170924', 'James', 2, 50),
('20170925', 'James', 2, 50),
('20170925', 'James', 2, 50),
('20170801', 'James', 2, 80),
('20170802', 'James', 2, 80);
select Date, Person, Team, sum([Daily Figure]) as [Daily Figure],
sum(sum([Daily Figure])) over(partition by Person, Team, month(date)) as [month to date figure]
from #t
group by Date, Person, Team;

How to validate if there was 12 sequential payments

As example :
I have this scenario where we receive payments, a singular payment per family, and register those payments with it's amount in the DB.
The thing is that a family can move their loan from bank1 to bank2, only if they have 12 or more sequential payments.
As example if they have registered a payment for
oct, nov, dec, jan, feb, mar, apr, may, jun, jul, ago, and sept.
and feb didn't received any payment, the count will start over at march.
Coworkers are suggesting that the best approach is, in every payment registration count the total payments and register the total sequential payments in an int column called sequential.
as:
Payment Family Bank Date Sequential
---------------------------------------------------------
1200 2 1 10-22-2009 1
1200 2 1 11-22-2009 2
.
.
.
1200 2 1 08-22-2010 11
1200 2 1 09-22-2010 12
What I think, there must be an approach where the sequential column is needless, where if I want to validate if the last order by Date DESC 12 rows are sequential with only 1 month in difference.
any ideas?
Edited:
There will be million of rows in this table.
Also prefer to have only the dates in the tables and work with them at application level
Analytics!
Data:
create table payments
(amount number,
family number,
bank number,
payment_date date
);
insert into payments values (1200, 2, 1, date '2010-01-01');
insert into payments values (1200, 2, 1, date '2010-02-02');
insert into payments values (1200, 2, 1, date '2010-03-03');
insert into payments values (1200, 2, 1, date '2010-04-04');
insert into payments values (1200, 2, 1, date '2010-05-05');
insert into payments values (1200, 2, 1, date '2010-06-07');
insert into payments values (1200, 2, 1, date '2010-07-07');
--skip august
--insert into payments values (1200, 2, 1, date '2010-08-08');
insert into payments values (1200, 2, 1, date '2010-09-09');
insert into payments values (1200, 2, 1, date '2010-10-10');
insert into payments values (1200, 2, 1, date '2010-11-11');
--double pay november
insert into payments values (1200, 2, 1, date '2010-11-30');
insert into payments values (1200, 2, 1, date '2010-12-12');
Query:
select *
from (select family, bank,
trunc(payment_date, 'mon') as payment_month,
lead ( trunc(payment_date, 'mon'))
over ( partition by family
order by payment_date)
as next_payment_month
from payments
order by payment_date desc
)
-- eliminate multiple payments in month
where payment_month <> next_payment_month
-- find a gap
and add_months(payment_month, 1) <> (next_payment_month)
-- stop at the first gap
and rownum = 1
Results:
FAMILY BANK PAYMENT_M NEXT_PAYM
---------- ---------- --------- ---------
2 1 01-JUL-10 01-SEP-10
You can use the value in NEXT_PAYMENT_MONTH to perform whatever comparison you want at the application level.
SELECT trunc(MONTHS_BETWEEN(SYSDATE, DATE '2010-01-01')) FROM DUAL
gives you a number of months - that was what I meanty by using the value at the application level.
So this:
select trunc(
months_between(sysdate,
(select next_payment_date
from (select family, bank,
trunc(payment_date, 'mon') as payment_month,
lead ( trunc(payment_date, 'mon'))
over ( partition by family
order by payment_date)
as next_payment_month
from payments
where family = :family
order by payment_date desc
)
where payment_month <> next_payment_month
and add_months(payment_month, 1) <> (next_payment_month)
and rownum = 1
)
)
from dual
Gives you a number of months with successive payments since the last missed month.
To validate whether a single family have 12 sequential payments over the past twelve months, regardless of bank, use:
select sum(payment) total_paid,
count(*) total_payments,
count(distinct trunc(pay_date,'mon')) paid_months
from payment_table
where family = :family and pay_date between :start_date and :end_date;
total_payments indicates the number of payments made in the period, while paid_months indicates the number of separate months in which payments were made.
If you want to check whether they have already switched bank in the selected period, add a group by bank clause to the above query.
To list all families with 12 distinct months of payments within the period, use:
select family,
sum(payment) total_paid,
count(*) total_payments,
count(distinct trunc(pay_date,'mon')) paid_months
from payment_table
where pay_date between :start_date and :end_date
group by family
having count(distinct trunc(pay_date,'mon')) = 12;
If you want to restrict the results to families that have not already switched bank in the selected period, add a and count(distinct bank) = 1 condition to the having clause of the above query.
I suggest ensuring that the payment table has an index on family and pay_date.
I think a simple query will help, check this:
SELECT COUNT(*)
FROM payments p
WHERE p.Family = 2 AND p.Date between '01-01-2009' and '12-01-2009'
this way, you'll get the number of payments between any date with your current table structure.
How about this:
SELECT PT.Payment
, PT.Family
, PT.Bank
, PT.Date
, (SELECT COUNT(*) FROM PaymentTable T
WHERE DATEDIFF (d, T.Date, PT.Date) < 31) as IsSequential
FROM PaymentsTable PT
The above query will tell you for each payment if it's sequential (i.e. if there was a payment made the month before it)
Then you could run a query to determine if there are 12 sequential payments made for a specific month, or for a specific family.
Let's say you want to display all families that have at least 12 sequential payments:
SELECT ST.Family
, COUNT(ST.IsSequential) as NumberOfSequentialPayments
FROM
(SELECT PT.Payment
, PT.Family
, PT.Bank
, PT.Date
, (SELECT COUNT(*) FROM PaymentTable T
WHERE DATEDIFF (d, T.Date, PT.Date) < 31) as IsSequential
FROM PaymentsTable PT
) AS ST
WHERE NumberOfSequentialPayments >= 12
GROUP BY ST.Family
It is possible to do it as other have pointed out.
However, this is not a case when you have relational data, but you do things sequentially, which is a bad thing.
This is a case when a business rule is sequential in nature; in such cases having a sequential helper field might
simplify your queries
improve performance (if you talk about 100M records this sudenlly becomes almost highest rated factor and various denormalization ideas spring to mind)
make sense for other business rules (allow more functionality and flexibility)
Re last point: I think the most complete solution would require re-examining the business rules - you would probably discover that users would talk about 'missed payments', which suggest other tables, such as 'payment plan/schedule' and tied with other processes this might be really the right place to have either missed payment column or sequential value... This structure would also support flexibility in grace periods, prepaying, etc...

SQL how to make one query out of multiple ones

I have a table that holds monthly data of billing records. so say Customer 1234 was billed in Jan/Feb and Customer 2345 was billing Jan/Feb/Mar. How can I group these to show me a concurrent monthly billing cycle. But also need to have non-concurrent billed months, so Customer 3456 was billed Feb/Apl/Jun/Aug
SELECT custName, month, billed, count(*) as Tally
FROM db_name
WHERE
GROUP BY
Results needed:
Customer 1234 was billed for 2 months Concurrent
Customer 2345 was billed for 3 months Concurrent
Customer 3456 was billed for 4 months Non-Concurrent
Any suggestions?
If the month is stored as a datetime field, you can use DATEDIFF to calculate the number of months between the first and the last bill. If the number of elapsed months equals the total number of bills, the bills are consecutive.
select
'Customer ' + custname + ' was billed for ' +
cast(count(*) as varchar) + ' months ' +
case
when datediff(month,min(billdate),max(billdate))+1 = count(*)
then 'Concurrent'
else 'Non-Concurrent'
end
from #billing
where billed = 1
group by custname
If you store the billing month as an integer, you can just subtract instead of using DATEDIFF. Replace the WHEN row with:
when max(billdate)-min(billdate)+1 = count(*)
But in that case I wonder how you distinguish between years.
If the months were all in a sequence, and we are limiting our search to a particular year then Min(month) + Count(times billed) - 1 should = Max(month).
declare #billing table(Custname varchar(10), month int, billed bit)
insert into #billing values (1234, 1, 1)
insert into #billing values (1234, 2, 1)
insert into #billing values (2345, 3, 1)
insert into #billing values (2345, 4, 1)
insert into #billing values (2345, 5, 1)
insert into #billing values (3456, 1, 1)
insert into #billing values (3456, 3, 1)
insert into #billing values (3456, 9, 1)
insert into #billing values (3456, 10, 1)
Select CustName, Count(1) as MonthsBilled,
Case
when Min(Month) + Count(1) - 1 = Max(Month)
then 1
else 0
end Concurrent
From #billing
where Billed = 1
Group by CustName
Cust Months Concurrent
1234 2 1
2345 3 1
3456 4 0
The suggestions here work based on an assumption that you will never bill a customer twice or more in the same month. If that isn't a safe assumption, you need a different approach. Let us know if that's the case.
how about:
SELECT custName, month, count(*) as tally
from billing
where billed = 1
group by custName, month
You left out some important information (like how Month is stored) and what database you're using, but here's a logical approach that you can start with:
CREATE VIEW CustomerBilledInMonth (CustName, Month, AmountBilled, ContinuousFlag) AS
SELECT CustName, Month, SUM(AmountBilled), 'Noncontinuous'
FROM BillingTable BT1
WHERE NOT EXISTS
(SELECT * FROM BillingTable BT2 WHERE BT2.CustName = BT1.CustName AND BT2.Month = BT1.Month - 1)
GROUP BY CustName, Month
UNION
SELECT CustName, Month, SUM(AmountBilled), 'Continuous'
FROM BillingTable BT1
WHERE EXISTS
(SELECT * FROM BillingTable BT2 WHERE BT2.CustName = BT1.CustName AND BT2.Month = BT1.Month - 1)
GROUP BY CustName, Month
Assuming that Month here is a consecutive integer field incremented by one from the first possible month in the system, this gives you with each customer's billing for each month summed up, and an additional flag containing 'Continuous' for those months that followed a month in which the customer was also billed and 'Noncontinuous' for those months that followed a month in which the customer was not billed.
Then:
SELECT CustName, LISTOF(Month), SUM(AmountBilled), MAX(ContinuousFlag)
FROM CustomerBilledInMonth GROUP BY CustName
will give you more or less what you want (where LISTOF is some kind of COALESCE type function dependent on the exact database you're using).