Here is a short continuation of my previous post on a “budget app” database structure. Based on my desired features from the last post, I’ve got some views for displaying net worth and one for displaying the categories with the budgets.
/*Ths is simply the current net worth. Not so useful*/
CREATE OR ALTER VIEW dbo.NetWorthDisplay
AS
SELECT IIF( GROUPING( AD.AccountTypeGroup ) = 1, 'Net Worth', AD.AccountTypeGroup ) AS AccountTypeGroup,
SUM( AD.CurrentBalance ) AS CurrentBalance
FROM DBO.AccountsDisplay AS AD
GROUP BY ROLLUP ( AD.AccountTypeGroup )
GO
/*This one is much more useful, showing the net worth as it changes by month.
I need to create the Months table first.*/
CREATE TABLE dbo.Months (
MonthStartDate DATE NOT NULL,
MonthEndDate DATE NOT NULL,
DaysInMonth INT NOT NULL,
CONSTRAINT PK_Months PRIMARY KEY CLUSTERED ( MonthStartDate )
)
INSERT INTO dbo.Months( MonthStartDate, MonthEndDate, DaysInMonth )
SELECT M.MonthStartDate,
EOMONTH( M.MonthStartDate ) AS MonthEndDate,
DATEDIFF( DAY, M.MonthStartDate, EOMONTH( M.MonthStartDate ) ) + 1 AS DaysInMonth
FROM (
/*I decided to start from Jan 1st 2021 as that is when my personal Bluecoin records start*/
SELECT DATEADD( MONTH, ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ), '2021-01-01' ) AS MonthStartDate
FROM SYS.objects AS O
) AS M
GO
CREATE OR ALTER VIEW dbo.NetWorthDisplayByMonth
AS
SELECT IIF( GROUPING( TY.AccountTypeGroup ) = 1, 'Net Worth', TY.AccountTypeGroup ) AS AccountTypeGroup,
M.MonthStartDate,
ISNULL( SUM( A.StartingBalance + ISNULL( T.TotalAmount, 0 ) ), 0 ) AS CurrentBalance
FROM (
SELECT MonthStartDate
FROM dbo.Months
WHERE DATEDIFF( MONTH, MonthStartDate, GETDATE() ) BETWEEN 0 AND 12
) AS M
CROSS JOIN dbo.AccountTypes AS TY
LEFT JOIN (
SELECT *
FROM dbo.Accounts
WHERE DeletedFlag = 0
) AS A
ON A.AccountTypeID = TY.AccountTypeID
AND A.OpeningDate <= M.MonthStartDate
OUTER APPLY (
SELECT SUM( Amount ) AS TotalAmount
FROM dbo.Transactions
WHERE DeletedFlag = 0
AND AccountID = A.AccountID
AND TransactionDate <= M.MonthStartDate
) AS T
GROUP BY ROLLUP ( TY.AccountTypeGroup ),
M.MonthStartDate
GO
I wanted to be able to handle budgets like the app, which means you can select a time window (to start with I’ll handle Weekly, Fortnightly, Monthly, Quarterly, and Annually) and an amount. Then, the view to display the budget per category will calculate how to scale the budget amount based on the timewindow.
/*Create our budgets table first*/
DROP TABLE IF EXISTS dbo.Budgets
CREATE TABLE dbo.Budgets (
BudgetID INT IDENTITY(1,1) NOT NULL,
TimeWindowType VARCHAR(20) NOT NULL,
BudgetAmount DECIMAL(10,2) NOT NULL,
LastModifiedDatetimeUTC DATETIME2(0) NOT NULL,
DeletedFlag BIT NOT NULL,
CONSTRAINT PK_Budgets PRIMARY KEY CLUSTERED ( BudgetID )
)
/*Then we need to alter the Categories table to link to a budget*/
DROP TABLE IF EXISTS dbo.Categories
CREATE TABLE dbo.Categories (
CategoryID INT IDENTITY(1,1) NOT NULL,
CategoryName VARCHAR(200) NOT NULL,
CategoryGroup VARCHAR(200) NOT NULL,
BudgetID INT NULL, /*In the app, you can set a budget at a category level or a category group level. Not sure yet how I'd handle it*/
LastModifiedDatetimeUTC DATETIME2(0) NOT NULL,
DeletedFlag BIT NOT NULL,
CONSTRAINT PK_CategoryID PRIMARY KEY CLUSTERED ( CategoryID )
)
/*Now let's create a generic table valued function to show all categories and compare the spending with the budget between any two dates*/
CREATE OR ALTER FUNCTION dbo.CategoriesAndBudgetDisplay (
@ComparisonStartDate DATE,
@ComparisonEndDate DATE
)
RETURNS TABLE
AS
RETURN (
SELECT C.CategoryName,
C.CategoryGroup,
T.Currency,
T.TotalTransactionAmount,
B.BudgetID,
B.TimeWindowType,
B.BudgetAmount
FROM dbo.Categories AS C
LEFT JOIN (
SELECT Currency,
SUM( Amount ) AS TotalTransactionAmount,
CategoryID
FROM dbo.Transactions
WHERE DeletedFlag = 0
AND TransactionDate BETWEEN @ComparisonStartDate AND @ComparisonEndDate
GROUP BY Currency,
CategoryID
) AS T
ON T.CategoryID = C.CategoryID
LEFT JOIN (
SELECT BudgetID,
TimeWindowType,
/*For now, I will keep it simply an assume 365 days a year*/
CASE TimeWindowType WHEN 'Annually' THEN -BudgetAmount * ( 1 + DATEDIFF( DAY, @ComparisonStartDate, @ComparisonEndDate ) ) / 365
WHEN 'Quarterly' THEN -BudgetAmount * ( 1 + DATEDIFF( DAY, @ComparisonStartDate, @ComparisonEndDate ) ) / 365 * 4
WHEN 'Monthly' THEN -BudgetAmount * ( 1 + DATEDIFF( DAY, @ComparisonStartDate, @ComparisonEndDate ) ) / 365 * 12
WHEN 'Fortnightly' THEN -BudgetAmount * ( 1 + DATEDIFF( DAY, @ComparisonStartDate, @ComparisonEndDate ) ) / 365 * 26
WHEN 'Weekly' THEN -BudgetAmount * ( 1 + DATEDIFF( DAY, @ComparisonStartDate, @ComparisonEndDate ) ) / 365 * 52
ELSE NULL
END AS BudgetAmount
FROM dbo.Budgets
WHERE DeletedFlag = 0
) AS B
ON B.BudgetID = C.BudgetID
WHERE C.DeletedFlag = 0
AND C.CategoryGroup NOT IN ( '(New Account)', '(Transfer)' )
)
GO
I’ve also built one stored procedure that’s for updating, deleting, or creating new accounts. I only have basic error handling - capture it to an error table and re-throw. If there was an application on top of this, it would need to do something with that error.
/*Create a simple error table to save any errors for now. A real app would handle it better*/
DROP TABLE IF EXISTS dbo.Errors
CREATE TABLE dbo.Errors (
ErrorMessage NVARCHAR(4000) NOT NULL,
ErrorLine INT NULL,
ErrorNumber INT NOT NULL,
ErrorProcedure NVARCHAR(128) NULL,
ErrorSeverity INT NOT NULL,
ErrorState INT NOT NULL,
ErrorDatetimeUTC DATETIME2(0) NOT NULL,
INDEX IDX_Errors_DT CLUSTERED ( ErrorDatetimeUTC )
)
GO
/*I am using default values for most of the parameters since if you are deleting an account, you only need the account ID*/
CREATE OR ALTER PROC dbo.CreateUpdateDeleteAccount
@AccountID INT,
@AccountName VARCHAR(200) = '',
@AccountTypeID INT = 1,
@OpeningDate DATE = '2021-01-01',
@StartingBalance DECIMAL(10,2) = 0,
@DefaultCurrency VARCHAR(3) = 'AUD',
@Notes NVARCHAR(2000) = '',
@Delete BIT = 0
AS
BEGIN
SET XACT_ABORT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRY
/*At this early stage, I'm not sure yet how I would handle errors, so I will save them in an error table*/
IF @Delete = 1
UPDATE A
SET A.DeletedFlag = 1,
A.LastModifiedDatetimeUTC = GETUTCDATE()
FROM dbo.Accounts AS A
WHERE A.AccountID = @AccountID
ELSE
BEGIN
/*I wanted a robust way to handle upserts - update the row if it exists, insert a new row if it doesn't. I've used a solution from
https://michaeljswart.com/2017/07/sql-server-upsert-patterns-and-antipatterns/ for this.
I'm going to assume that inserts are more common, but apparently this pattern does quite well for both*/
BEGIN TRAN
UPDATE A
SET A.AccountName = @AccountName,
A.AccountTypeID = @AccountTypeID,
A.OpeningDate = @OpeningDate,
A.StartingBalance = @StartingBalance,
A.DefaultCurrency = @DefaultCurrency,
A.Notes = @Notes,
A.LastModifiedDatetimeUTC = GETUTCDATE()
FROM dbo.Accounts AS A
WHERE A.AccountID = @AccountID
/*So, basically, try to update an existing record, and if that affects 0 rows, insert a new one*/
INSERT dbo.Accounts( AccountName, AccountTypeID, OpeningDate, StartingBalance, DefaultCurrency, Notes, LastModifiedDatetimeUTC, DeletedFlag )
SELECT @AccountName,
@AccountTypeID,
@OpeningDate,
@StartingBalance,
@DefaultCurrency,
@Notes,
GETUTCDATE(),
0 AS DeletedFlag
WHERE @@ROWCOUNT = 0
COMMIT TRAN
END
END TRY
BEGIN CATCH
INSERT INTO dbo.Errors( ErrorMessage, ErrorLine, ErrorNumber, ErrorProcedure, ErrorSeverity, ErrorState, ErrorDatetimeUTC )
VALUES( ERROR_MESSAGE(), ERROR_LINE(), ERROR_NUMBER(), ERROR_PROCEDURE(), ERROR_SEVERITY(), ERROR_STATE(), GETUTCDATE() )
;THROW
END CATCH
END
Bluecoin features I’m missing from my database:
- Budgets
By Category- By Category group
Display net worth- Project net worth
Display running balance of accountsDisplay current balance of account- Handling non-default currencies in the running totals, budgets, etc
- Stored procedures for:
Creating, deleting, or updating accounts- Creating, deleting, or updating categories
- Creating, deleting, or updating transactions
- Creating, deleting, or updating budgets
- Implementing reminders