Sqlism
Loading…
Learning path

Become a Financial / FP&A Analyst with SQL

Finance teams live in spreadsheets until the data outgrows them. Analysts who can pull actuals straight from the warehouse, compare them with budget, and reconcile them to the ledger themselves become the fastest, most trusted people in the room.

8 Modules
~6 hrs, self-paced
3 Portfolio Projects
0 of 8 modules complete

This path covers the SQL behind monthly close and planning work: rolling transactions up to P&L lines, handling fiscal calendars, calculating growth, year-to-date and variance, laying numbers out like a financial statement, and proving your totals tie back to the general ledger. Every module maps to a report you'd otherwise build by hand in Excel.

  1. 1

    Aggregation for P&L-Style Reports

    30 min

    A P&L is a GROUP BY: transactions rolled up to account lines, then to sections like revenue, cost of sales and operating expenses. Map accounts to report lines with a lookup table rather than hard-coding account numbers in CASE statements.

    SELECT m.report_section, m.report_line,
           SUM(g.amount) AS actual
    FROM gl_entries g
    JOIN account_mapping m ON m.account_no = g.account_no
    WHERE g.period = '2026-09'
    GROUP BY m.report_section, m.report_line
    ORDER BY m.report_section, m.report_line;
    Keep the account-to-line mapping in a table that finance owns. When the chart of accounts changes, the report updates without anyone editing SQL.
  2. 2

    Date Truncation & Fiscal Calendars

    30 min

    Finance rarely runs January to December. Truncating dates to month is easy; mapping them to fiscal periods (April-March in India, July-June elsewhere) is best done with a calendar table that holds fiscal year, quarter and period for every date. Join to it instead of recalculating fiscal logic in every query.

    SELECT c.fiscal_year, c.fiscal_quarter, SUM(s.amount) AS revenue
    FROM sales s
    JOIN dim_calendar c ON c.calendar_date = s.invoice_date
    GROUP BY c.fiscal_year, c.fiscal_quarter
    ORDER BY c.fiscal_year, c.fiscal_quarter;
    A calendar table also solves missing months: left-join from the calendar so a month with zero sales still appears as zero instead of vanishing from the report.
  3. 3

    Month-over-Month & Year-over-Year Growth

    35 min

    Growth compares a period with an earlier one. LAG() fetches the previous month, or the same month last year with an offset of 12, without a self-join. Guard the division so a zero base doesn't break the query.

    SELECT month, revenue,
           LAG(revenue, 12) OVER (ORDER BY month) AS revenue_last_year,
           ROUND(100.0 * (revenue - LAG(revenue, 12) OVER (ORDER BY month))
                 / NULLIF(LAG(revenue, 12) OVER (ORDER BY month), 0), 1) AS yoy_pct
    FROM monthly_revenue;
    LAG(revenue, 12) assumes no missing months. Build from a calendar table first, or a gap silently shifts every comparison by a month.
  4. 4

    Running Totals & Year-to-Date

    30 min

    Year-to-date is a running total that restarts each fiscal year. A windowed SUM() with PARTITION BY fiscal_year gives you YTD for every month in one pass, which is exactly the column a monthly finance pack needs.

    SELECT fiscal_year, fiscal_period, revenue,
           SUM(revenue) OVER (PARTITION BY fiscal_year
                              ORDER BY fiscal_period
                              ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS revenue_ytd
    FROM monthly_revenue;
    Always spell out the frame (ROWS BETWEEN ...). The default frame on some engines treats tied dates as one group and can make running totals jump unexpectedly.
  5. 5

    Budget vs Actual Variance

    35 min

    Variance analysis joins actuals with budget at the same grain (department and month, say) and calculates the absolute and percentage difference. Aggregate each side to that grain before joining, or a department with many transactions will multiply its budget.

    WITH a AS (SELECT dept, period, SUM(amount) AS actual FROM gl_entries GROUP BY dept, period),
         b AS (SELECT dept, period, SUM(amount) AS budget FROM budget_lines GROUP BY dept, period)
    SELECT COALESCE(a.dept, b.dept) AS dept, COALESCE(a.period, b.period) AS period,
           a.actual, b.budget,
           a.actual - b.budget AS variance,
           ROUND(100.0 * (a.actual - b.budget) / NULLIF(b.budget, 0), 1) AS variance_pct
    FROM a FULL OUTER JOIN b ON b.dept = a.dept AND b.period = a.period;
    Use a FULL OUTER JOIN: unbudgeted spend and budget with no spend are both findings a finance reviewer needs to see.
  6. 6

    Percent of Total, Mix & Contribution

    25 min

    "What share of revenue comes from each region?" is a percent-of-total question. A window SUM() with an empty OVER() gives the grand total on every row, so the share is a simple division with no subquery.

    SELECT region, SUM(amount) AS revenue,
           ROUND(100.0 * SUM(amount) / SUM(SUM(amount)) OVER (), 1) AS pct_of_total
    FROM sales
    GROUP BY region
    ORDER BY revenue DESC;
    Decide how NULL or "Unassigned" regions are handled before sharing a mix analysis. Dropping them quietly makes the percentages add up to less than 100.
  7. 7

    Pivoting Into Financial Statement Layouts

    30 min

    Finance packs show months or quarters as columns. Conditional aggregation (SUM(CASE WHEN ... )) pivots periods into columns in plain SQL, producing a layout that pastes straight into a board pack or feeds a Power BI matrix.

    SELECT m.report_line,
           SUM(CASE WHEN g.period = '2026-07' THEN g.amount ELSE 0 END) AS jul,
           SUM(CASE WHEN g.period = '2026-08' THEN g.amount ELSE 0 END) AS aug,
           SUM(CASE WHEN g.period = '2026-09' THEN g.amount ELSE 0 END) AS sep,
           SUM(g.amount) AS q2_total
    FROM gl_entries g
    JOIN account_mapping m ON m.account_no = g.account_no
    WHERE g.period BETWEEN '2026-07' AND '2026-09'
    GROUP BY m.report_line;
    Check that the pivoted columns add up to the unpivoted total. A typo in one CASE period quietly drops a month.
  8. 8

    Reconciling to the Ledger & Avoiding Wrong Totals

    35 min

    A finance number is only accepted once it ties out. Reconcile your report total to the general ledger trial balance for the same period, investigate any difference to the account level, and watch for the classic causes: join fan-out, NULLs dropped by WHERE, and period cut-off mismatches.

    SELECT r.account_no, r.report_total, t.ledger_balance,
           r.report_total - t.ledger_balance AS difference
    FROM (SELECT account_no, SUM(amount) AS report_total FROM report_extract GROUP BY account_no) r
    JOIN trial_balance t ON t.account_no = r.account_no AND t.period = '2026-09'
    WHERE ABS(r.report_total - t.ledger_balance) > 0.01;
    Reconcile at the account level, not just the grand total. Two offsetting errors can make the totals match while two accounts are both wrong.

Practice on real projects

Practice building a monthly finance pack from raw transactions.

Get interview-ready

Finance analyst SQL interviews focus on aggregation accuracy, growth and running-total calculations, and explaining why two numbers don't match.

Validate what you've learned

25 questions covering everything above. Score 80% or higher to earn your Financial / FP&A Analyst badge on your dashboard.

Upgrade to Sqlism Pro to take the validation quiz

25 questions · pass at 80%+ to earn your Financial / FP&A Analyst badge on your dashboard. One-time payment, lifetime access.

Get Sqlism Pro