Rawabi Bank
Rawabi Bank (fictional) · 7 tables · 28,979 rows · version 1
Rawabi Bank is a fictional retail bank with branches across Saudi Arabia and a digital branch. Customers hold current and savings accounts, credit cards and personal finance; some accounts are held jointly. Every account transaction for 2024 and 2025 is recorded with a positive amount and a direction (credit = money in, debit = money out), a channel and, where known, a merchant and category. Use it for filtering, aggregation, joins (including self-joins on categories and reversals), running balances with window functions, dormancy analysis and data cleaning.
The documentation is open to everyone. Querying this dataset in the playground is included with Formula Quest membership; lessons that use it follow their own access.
Relationships
Sample queries
SELECT a.account_number, p.product_type, a.status,
a.opening_balance
+ COALESCE(SUM(CASE WHEN t.direction = 'credit' THEN t.amount ELSE -t.amount END), 0) AS balance
FROM accounts AS a
JOIN account_products AS p ON p.product_id = a.product_id
LEFT JOIN transactions AS t ON t.account_id = a.account_id
WHERE a.status <> 'closed'
GROUP BY a.account_id, a.account_number, p.product_type, a.status, a.opening_balance
ORDER BY balance DESC
LIMIT 20;SELECT date_trunc('month', t.posted_at)::date AS month,
COALESCE(parent.category_name, 'Uncategorised') AS category_group,
SUM(t.amount) AS spent
FROM transactions AS t
LEFT JOIN transaction_categories AS c ON c.category_id = t.category_id
LEFT JOIN transaction_categories AS parent ON parent.category_id = c.parent_category_id
WHERE t.direction = 'debit'
AND t.channel IN ('pos', 'online')
AND t.reversal_of_transaction_id IS NULL
AND NOT EXISTS (SELECT 1 FROM transactions r WHERE r.reversal_of_transaction_id = t.transaction_id)
GROUP BY 1, 2
ORDER BY 1, 3 DESC;SELECT a.account_number, a.status, MAX(t.posted_at) AS last_transaction
FROM accounts AS a
LEFT JOIN transactions AS t ON t.account_id = a.account_id
WHERE a.status <> 'closed'
GROUP BY a.account_id, a.account_number, a.status
HAVING MAX(t.posted_at) < TIMESTAMP '2025-07-01' OR MAX(t.posted_at) IS NULL
ORDER BY last_transaction NULLS FIRST;SELECT a.account_number, h.holder_role, c.first_name || ' ' || c.last_name AS holder
FROM account_holders AS h
JOIN accounts AS a ON a.account_id = h.account_id
JOIN customers AS c ON c.customer_id = h.customer_id
WHERE h.account_id IN (SELECT account_id FROM account_holders WHERE holder_role = 'joint')
ORDER BY a.account_number, h.holder_role DESC;SELECT t.posted_at, t.direction, t.amount,
a.opening_balance + SUM(CASE WHEN t.direction = 'credit' THEN t.amount ELSE -t.amount END)
OVER (ORDER BY t.posted_at, t.transaction_id) AS running_balance
FROM transactions AS t
JOIN accounts AS a ON a.account_id = t.account_id
WHERE t.account_id = (SELECT account_id FROM transactions GROUP BY account_id ORDER BY count(*) DESC LIMIT 1)
ORDER BY t.posted_at, t.transaction_id;Data dictionary
branches
22 rows · primary key (branch_id)Rawabi Bank's branches, plus one digital branch for accounts opened in the mobile app.
| Column | Type | Null? | Key | Description |
|---|---|---|---|---|
| branch_id | integer | no | PK | Unique number for each branch. |
| branch_code | text | no | unique | Short branch code: city code and a sequence, e.g. 'RUH-01'. |
| branch_name | text | no | Branch name as shown to customers. | |
| branch_type | text | no | 'branch' for a physical branch, 'digital' for the online-only branch. | |
| city | text | yes | City of the branch. NULL for the digital branch. | |
| region | text | yes | Region of Saudi Arabia (Central, Western, Eastern, Northern, Southern). NULL for the digital branch. | |
| opened_on | date | no | Date the branch opened. | |
| closed_on | date | yes | Date the branch closed. NULL while it is open. |
customers
375 rows · primary key (customer_id)People registered with the bank. A customer may hold no account (still onboarding), several accounts, or be a joint holder only.
| Column | Type | Null? | Key | Description |
|---|---|---|---|---|
| customer_id | integer | no | PK | Unique number for each customer record (assigned in order of registration). |
| first_name | text | no | Given name. | |
| last_name | text | no | Family name. | |
| gender | text | no | 'F' or 'M'. | |
| date_of_birth | date | no | Date of birth. | |
| text | yes | Contact email. NULL when not provided. | ||
| city | text | no | City of the customer's address. | |
| segment | text | no | Customer segment: retail, premium or private (the wealthiest). | |
| kyc_status | text | no | Know-your-customer check: verified, pending (onboarding not finished) or expired (documents need renewal). | |
| joined_on | date | no | Date the customer registered with the bank. | |
| home_branch_id | integer | no | branches.branch_id | Branch that manages the customer relationship. |
account_products
8 rows · primary key (product_id)The account types the bank offers (or used to offer), with their fees and profit rates.
| Column | Type | Null? | Key | Description |
|---|---|---|---|---|
| product_id | integer | no | PK | Unique number for each product. |
| product_code | text | no | unique | Internal product code, e.g. 'SAV-HSD'. |
| product_name | text | no | Product name shown to customers. | |
| product_type | text | no | current, savings, credit_card or personal_finance. | |
| monthly_fee | numeric(12,2) | no | Monthly account fee in SAR (0 if free). Waived when the balance is at least min_balance. | |
| min_balance | numeric(12,2) | yes | Balance in SAR at or above which the monthly fee is waived. NULL when there is no fee. | |
| annual_fee | numeric(12,2) | no | Yearly card fee in SAR, charged on the card's anniversary month (0 if none). | |
| profit_rate | numeric(5,2) | yes | Annual profit rate in percent: paid to the customer for savings, charged for personal finance. NULL when not applicable. | |
| is_active | boolean | no | TRUE if the product is still sold to new customers. Existing holders keep discontinued products. |
accounts
708 rows · primary key (account_id)One row per account. customer_id is the primary holder; joint holders are in account_holders.
| Column | Type | Null? | Key | Description |
|---|---|---|---|---|
| account_id | integer | no | PK | Unique number for each account (assigned in order of opening). |
| account_number | text | no | unique | Synthetic account number shown to customers, e.g. 'RB-100123'. |
| customer_id | integer | no | customers.customer_id | Primary holder of the account. |
| product_id | integer | no | account_products.product_id | Which product this account is. |
| branch_id | integer | no | branches.branch_id | Branch where the account was opened (not always the customer's home branch). |
| opened_on | date | no | Date the account was opened. | |
| closed_on | date | yes | Date the account was closed. NULL while open (active or dormant). | |
| status | text | no | active, dormant (no customer activity for a long time; flagged by the bank, sometimes late) or closed. | |
| credit_limit | numeric(12,2) | yes | Credit limit in SAR for credit cards. NULL for every other product. | |
| opening_balance | numeric(14,2) | no | Balance in SAR on 2024-01-01, when the transaction history starts (0 for accounts opened later or closed earlier). Negative for cards and finance: the amount owed. |
account_holders
744 rows · primary key (account_id, customer_id)Every holder of every account (many-to-many). Each account has exactly one 'primary' holder (= accounts.customer_id); joint accounts also have a 'joint' holder.
| Column | Type | Null? | Key | Description |
|---|---|---|---|---|
| account_id | integer | no | PK accounts.account_id | The account. |
| customer_id | integer | no | PK customers.customer_id | A holder of the account. |
| holder_role | text | no | 'primary' for the main holder, 'joint' for an additional holder. | |
| added_on | date | no | Date this person became a holder of the account. |
transaction_categories
25 rows · primary key (category_id)Two-level category tree. Top-level groups have no parent; transactions are tagged with the lower-level categories only.
| Column | Type | Null? | Key | Description |
|---|---|---|---|---|
| category_id | integer | no | PK | Unique number for each category. |
| category_name | text | no | unique | Category name, e.g. 'Groceries'. |
| parent_category_id | integer | yes | transaction_categories.category_id | The group this category belongs to. NULL for a top-level group. |
transactions
27,097 rows · primary key (transaction_id)Every posting on every account from 2024-01-01 to 2025-12-31. amount is always positive; direction says whether money came in ('credit') or went out ('debit'). Transfers between two Rawabi accounts appear once on each account.
| Column | Type | Null? | Key | Description |
|---|---|---|---|---|
| transaction_id | integer | no | PK | Unique number for each transaction (increases with time). |
| account_id | integer | no | accounts.account_id | Account the transaction was posted to. |
| posted_at | timestamp | no | Date and time the transaction was posted (Saudi local time). | |
| amount | numeric(12,2) | no | Amount in SAR. Always positive: use direction for the sign. | |
| direction | text | no | 'credit' = money into the account, 'debit' = money out of it (from the customer's point of view). | |
| channel | text | no | atm, pos (card in a shop), online, branch, transfer (bank transfer, salary, internal transfer) or system (fees and profit posted by the bank). | |
| merchant_name | text | yes | Merchant for card and online purchases, exactly as received from the card network (spelling varies). NULL for ATM, transfers, salary, fees and profit. | |
| category_id | integer | yes | transaction_categories.category_id | Spending or income category. NULL when the transaction could not be categorised. |
| reversal_of_transaction_id | integer | yes | transactions.transaction_idunique | For a reversal: the transaction it cancels. NULL for ordinary transactions. |