I have these financial transactions (name of client, date of transactions and debit and credit amount). There can be multiple transactions for the same client on the same date.
Name Date Debit Credit
A 2021-01-01 00:00:00.0000000 10 0
A 2021-01-01 00:00:00.0000000 9 0
A 2021-02-01 00:00:00.0000000 11 0
A 2021-03-01 00:00:00.0000000 0 50
A 2021-04-01 00:00:00.0000000 30 0
B 2021-01-01 00:00:00.0000000 10 0
B 2022-02-01 00:00:00.0000000 0 12
B 2022-03-01 00:00:00.0000000 0 50
B 2024-04-01 00:00:00.0000000 3 0
I would like the balances for each client for each month. (It's fine if the there are missing years / months for when there were no transactions but ideally the result should be gapless.)
Below results with gaps (e.g. gap between B 2021/1 and B 2022/2)
Name Year Month balance
A 2021 1 -19
A 2021 2 -30
A 2021 3 20
A 2021 4 -10
B 2021 1 -10
B 2022 2 2
B 2022 3 52
B 2024 4 49
Here results without gaps (... elided year/month with no transactions)
Name Year Month balance
A 2021 1 -19 -- start from the very first transaction
A 2021 2 -30
A 2021 3 20
A 2021 4 -10
A 2021 5 -10 -- no transaction for 2021/5 so repeat the previous balance
... -- show all year/months to 2024-03
A 2024 4 -10 -- all the way to the current month
B 2021 1 -10
B 2021 2 -10 -- no transaction
B 2021 3 -10 -- no transaction
... -- show all year/months to 2021-12
B 2022 1 -10 -- no transaction
B 2022 2 2
B 2022 3 52
B 2022 4 52 -- no transaction
B 2022 5 52 -- no transaction
... -- show all year/months to 2024-03
B 2024 3 49 -- no transaction
B 2024 4 49
I managed to do this in 2 steps.
- First group per client, year and month.
- Then for each group sum all the preceding transactions to get the balance.
Here the SQL
DECLARE @MyTransactions TABLE (
[Name] nvarchar(10),
[Date] datetime2,
[Debit] decimal,
[Credit] decimal
)
INSERT INTO @MyTransactions ([Name],[Date],[Debit],[Credit]) VALUES ('A','2021-01-01',10,0)
INSERT INTO @MyTransactions ([Name],[Date],[Debit],[Credit]) VALUES ('A','2021-02-01',11,0)
INSERT INTO @MyTransactions ([Name],[Date],[Debit],[Credit]) VALUES ('A','2021-03-01',0,50)
INSERT INTO @MyTransactions ([Name],[Date],[Debit],[Credit]) VALUES ('A','2021-04-01',30,0)
INSERT INTO @MyTransactions ([Name],[Date],[Debit],[Credit]) VALUES ('B','2021-01-01',10,0)
INSERT INTO @MyTransactions ([Name],[Date],[Debit],[Credit]) VALUES ('B','2022-02-01',0,12)
INSERT INTO @MyTransactions ([Name],[Date],[Debit],[Credit]) VALUES ('B','2022-03-01',0,50)
INSERT INTO @MyTransactions ([Name],[Date],[Debit],[Credit]) VALUES ('B','2024-04-01',3,0)
DECLARE @MyGroups TABLE (
[Name] nvarchar(10),
[Year] int,
[Month] int
)
-- 1. first group per client, year and month
INSERT INTO @MyGroups ([Name],[Year],[Month])
SELECT [Name], DATEPART(YEAR, [Date]), DATEPART(MONTH, [Date])
FROM @MyTransactions
GROUP BY [Name],DATEPART(YEAR, [Date]), DATEPART(MONTH, [Date])
-- 2. now for each group sum all the preceding transactions to get the balance
SELECT
G.[Name], G.[Year], G.[Month], SUM(ISNULL(T.Credit,0) - ISNULL(T.Debit,0))
FROM @MyGroups G
JOIN @MyTransactions T ON
T.[Name] = G.[Name]
AND
(
(DATEPART(YEAR, T.[Date]) = G.[Year] AND DATEPART(MONTH, T.[Date]) <= G.[Month])
OR
(DATEPART(YEAR, T.[Date]) < G.[Year])
)
GROUP BY G.[Name], G.[Year], G.[Month]
Questions
- Is there a way to do this more elegantly, e.g. in one step without a temporary grouping table?
- How to ensure that there are no gaps between the dates?
Answers
You can achieve this without using temporary tables by using window functions and generating a series of dates for each client to ensure there are no gaps in the dates. Here's how you can do it:
WITH AllDates AS (
SELECT DISTINCT
[Name],
DATEADD(MONTH, m.MonthOffset, DATEFROMPARTS(Y.YearValue, m.MonthValue, 1)) AS TransactionDate
FROM
(VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10), (11), (12)) AS m(MonthOffset)
CROSS JOIN
(VALUES (2021), (2022), (2023), (2024)) AS Y(YearValue)
),
ClientTransactions AS (
SELECT
t.[Name],
DATEADD(MONTH, DATEDIFF(MONTH, 0, t.[Date]), 0) AS TransactionMonth,
SUM(ISNULL(t.Credit, 0) - ISNULL(t.Debit, 0)) AS Balance
FROM
@MyTransactions t
GROUP BY
t.[Name],
DATEADD(MONTH, DATEDIFF(MONTH, 0, t.[Date]), 0)
),
FilledDates AS (
SELECT
ad.[Name],
ad.TransactionDate AS TransactionMonth,
COALESCE(ct.Balance, LAG(ct.Balance) OVER (PARTITION BY ad.[Name] ORDER BY ad.TransactionDate)) AS Balance
FROM
AllDates ad
LEFT JOIN
ClientTransactions ct ON ad.[Name] = ct.[Name] AND ad.TransactionDate = ct.TransactionMonth
)
SELECT
[Name],
YEAR(TransactionMonth) AS [Year],
MONTH(TransactionMonth) AS [Month],
Balance
FROM
FilledDates
ORDER BY
[Name],
[Year],
[Month];
In this query:
-
AllDates
CTE generates all possible combinations of client names and months spanning from 2021 to 2024. -
ClientTransactions
CTE calculates the balance for each client for each month based on the transactions. -
FilledDates
CTE left joins theAllDates
with theClientTransactions
to ensure all combinations are included, and fills in the missing balances by using the last known balance (using theLAG
function). -
Finally, the main query selects the client name, year, month, and balance from the
FilledDates
CTE, ensuring the results are ordered by client name, year, and month.
This approach ensures there are no gaps in the dates and provides the desired results in one step without the need for temporary grouping tables.