SQL Quest

Lessons, hints, explained solutions, the in-browser PostgreSQL editor and practice answer checks in your browser are available. Graded assessments, recorded progress from answer checks and new certificates aren't available yet.

Rawabi Bank

Rawabi Bank (fictional) · 7 tables · 28,979 rows · version 1

Open in the playground

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

branches22PKbranch_idbranch_codebranch_namebranch_typecityregionopened_onclosed_oncustomers375PKcustomer_idFKhome_branch_idfirst_namelast_namegenderdate_of_birthemailcitysegment+ 2 more columnsaccount_products8PKproduct_idproduct_codeproduct_nameproduct_typemonthly_feemin_balanceannual_feeprofit_rateis_activeaccounts708PKaccount_idFKcustomer_idFKproduct_idFKbranch_idaccount_numberopened_onclosed_onstatuscredit_limit+ 1 more columnsaccount_holders744PKaccount_idPKcustomer_idholder_roleadded_ontransaction_categories25PKcategory_idFKparent_category_idcategory_nametransactions27,097PKtransaction_idFKaccount_idFKcategory_idFKreversal_of_transaction…posted_atamountdirectionchannelmerchant_name
Arrows point from a foreign key to the table it references. PK = primary key, FK = foreign key. Numbers are row counts.

Sample queries

Current balance of every open account
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;
Monthly card spending by parent category (reversals excluded)
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;
Accounts with no transactions in the last six months
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;
Joint accounts and their holders
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;
Running balance of one account
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.

ColumnTypeNull?KeyDescription
branch_idintegerno PKUnique number for each branch.
branch_codetextnouniqueShort branch code: city code and a sequence, e.g. 'RUH-01'.
branch_nametextnoBranch name as shown to customers.
branch_typetextno'branch' for a physical branch, 'digital' for the online-only branch.
citytextyesCity of the branch. NULL for the digital branch.
regiontextyesRegion of Saudi Arabia (Central, Western, Eastern, Northern, Southern). NULL for the digital branch.
opened_ondatenoDate the branch opened.
closed_ondateyesDate 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.

ColumnTypeNull?KeyDescription
customer_idintegerno PKUnique number for each customer record (assigned in order of registration).
first_nametextnoGiven name.
last_nametextnoFamily name.
gendertextno'F' or 'M'.
date_of_birthdatenoDate of birth.
emailtextyesContact email. NULL when not provided.
citytextnoCity of the customer's address.
segmenttextnoCustomer segment: retail, premium or private (the wealthiest).
kyc_statustextnoKnow-your-customer check: verified, pending (onboarding not finished) or expired (documents need renewal).
joined_ondatenoDate the customer registered with the bank.
home_branch_idintegerno branches.branch_idBranch 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.

ColumnTypeNull?KeyDescription
product_idintegerno PKUnique number for each product.
product_codetextnouniqueInternal product code, e.g. 'SAV-HSD'.
product_nametextnoProduct name shown to customers.
product_typetextnocurrent, savings, credit_card or personal_finance.
monthly_feenumeric(12,2)noMonthly account fee in SAR (0 if free). Waived when the balance is at least min_balance.
min_balancenumeric(12,2)yesBalance in SAR at or above which the monthly fee is waived. NULL when there is no fee.
annual_feenumeric(12,2)noYearly card fee in SAR, charged on the card's anniversary month (0 if none).
profit_ratenumeric(5,2)yesAnnual profit rate in percent: paid to the customer for savings, charged for personal finance. NULL when not applicable.
is_activebooleannoTRUE 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.

ColumnTypeNull?KeyDescription
account_idintegerno PKUnique number for each account (assigned in order of opening).
account_numbertextnouniqueSynthetic account number shown to customers, e.g. 'RB-100123'.
customer_idintegerno customers.customer_idPrimary holder of the account.
product_idintegerno account_products.product_idWhich product this account is.
branch_idintegerno branches.branch_idBranch where the account was opened (not always the customer's home branch).
opened_ondatenoDate the account was opened.
closed_ondateyesDate the account was closed. NULL while open (active or dormant).
statustextnoactive, dormant (no customer activity for a long time; flagged by the bank, sometimes late) or closed.
credit_limitnumeric(12,2)yesCredit limit in SAR for credit cards. NULL for every other product.
opening_balancenumeric(14,2)noBalance 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.

ColumnTypeNull?KeyDescription
account_idintegerno PK accounts.account_idThe account.
customer_idintegerno PK customers.customer_idA holder of the account.
holder_roletextno'primary' for the main holder, 'joint' for an additional holder.
added_ondatenoDate 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.

ColumnTypeNull?KeyDescription
category_idintegerno PKUnique number for each category.
category_nametextnouniqueCategory name, e.g. 'Groceries'.
parent_category_idintegeryes transaction_categories.category_idThe 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.

ColumnTypeNull?KeyDescription
transaction_idintegerno PKUnique number for each transaction (increases with time).
account_idintegerno accounts.account_idAccount the transaction was posted to.
posted_attimestampnoDate and time the transaction was posted (Saudi local time).
amountnumeric(12,2)noAmount in SAR. Always positive: use direction for the sign.
directiontextno'credit' = money into the account, 'debit' = money out of it (from the customer's point of view).
channeltextnoatm, pos (card in a shop), online, branch, transfer (bank transfer, salary, internal transfer) or system (fees and profit posted by the bank).
merchant_nametextyesMerchant for card and online purchases, exactly as received from the card network (spelling varies). NULL for ATM, transfers, salary, fees and profit.
category_idintegeryes transaction_categories.category_idSpending or income category. NULL when the transaction could not be categorised.
reversal_of_transaction_idintegeryes transactions.transaction_iduniqueFor a reversal: the transaction it cancels. NULL for ordinary transactions.