DuckDB for Financial Modeling

Build financial models in DuckDB: P&L, cash flow, ARR/MRR, unit economics, scenario analysis. Replace spreadsheets with SQL for more reliable financial analysis.

Mark Rachapoom
Mark Rachapoom
·6 min read
DuckDB for Financial Modeling

Financial models built in spreadsheets have a reliability problem: they break silently, formulas get overwritten, and they don't scale to large datasets. DuckDB gives you SQL-based financial models that are version-controlled, reproducible, and queryable.

Here's how to build the financial models that matter most for SaaS businesses.

Schema Setup#

-- Revenue transactions
CREATE TABLE transactions (
    id VARCHAR PRIMARY KEY,
    customer_id VARCHAR,
    type VARCHAR,  -- 'new_business', 'expansion', 'contraction', 'churn', 'reactivation'
    arr_delta DECIMAL(12, 2),  -- Annual Recurring Revenue change
    mrr_delta DECIMAL(12, 2),  -- Monthly Recurring Revenue change
    occurred_at DATE
);
 
-- Expenses
CREATE TABLE expenses (
    id VARCHAR PRIMARY KEY,
    category VARCHAR,  -- 'cogs', 'sales', 'marketing', 'r_and_d', 'g_and_a'
    description VARCHAR,
    amount DECIMAL(12, 2),
    period DATE,
    is_recurring BOOLEAN
);
 
-- Headcount
CREATE TABLE employees (
    id VARCHAR PRIMARY KEY,
    name VARCHAR,
    department VARCHAR,
    annual_salary DECIMAL(12, 2),
    equity_pct DECIMAL(8, 6),
    start_date DATE,
    end_date DATE
);

ARR / MRR Waterfall#

The SaaS revenue waterfall: starting ARR + new + expansion - contraction - churn = ending ARR.

SELECT 
    DATE_TRUNC('month', occurred_at) AS month,
    SUM(CASE WHEN type = 'new_business' THEN mrr_delta ELSE 0 END) AS new_mrr,
    SUM(CASE WHEN type = 'expansion' THEN mrr_delta ELSE 0 END) AS expansion_mrr,
    SUM(CASE WHEN type = 'contraction' THEN mrr_delta ELSE 0 END) AS contraction_mrr,
    SUM(CASE WHEN type = 'churn' THEN mrr_delta ELSE 0 END) AS churned_mrr,
    SUM(CASE WHEN type = 'reactivation' THEN mrr_delta ELSE 0 END) AS reactivation_mrr,
    SUM(mrr_delta) AS net_new_mrr,
    SUM(SUM(mrr_delta)) OVER (ORDER BY DATE_TRUNC('month', occurred_at) ROWS UNBOUNDED PRECEDING) AS ending_mrr
FROM transactions
GROUP BY month
ORDER BY month;

P&L Statement#

-- Monthly P&L
WITH revenue AS (
    SELECT 
        DATE_TRUNC('month', occurred_at) AS month,
        SUM(SUM(mrr_delta)) OVER (ORDER BY DATE_TRUNC('month', occurred_at) ROWS UNBOUNDED PRECEDING) AS mrr
    FROM transactions GROUP BY month
),
costs AS (
    SELECT 
        period AS month,
        SUM(CASE WHEN category = 'cogs' THEN amount ELSE 0 END) AS cogs,
        SUM(CASE WHEN category = 'sales' THEN amount ELSE 0 END) AS sales_cost,
        SUM(CASE WHEN category = 'marketing' THEN amount ELSE 0 END) AS marketing_cost,
        SUM(CASE WHEN category = 'r_and_d' THEN amount ELSE 0 END) AS r_and_d_cost,
        SUM(CASE WHEN category = 'g_and_a' THEN amount ELSE 0 END) AS g_and_a_cost
    FROM expenses
    GROUP BY period
)
SELECT 
    r.month,
    r.mrr AS revenue,
    c.cogs,
    r.mrr - c.cogs AS gross_profit,
    ROUND((r.mrr - c.cogs) * 100.0 / NULLIF(r.mrr, 0), 1) AS gross_margin_pct,
    c.sales_cost + c.marketing_cost AS s_and_m,
    c.r_and_d_cost AS r_and_d,
    c.g_and_a_cost AS g_and_a,
    r.mrr - c.cogs - c.sales_cost - c.marketing_cost - c.r_and_d_cost - c.g_and_a_cost AS ebitda
FROM revenue r
JOIN costs c ON r.month = c.month
ORDER BY r.month;

Unit Economics: LTV and CAC#

-- LTV by segment
WITH customer_revenue AS (
    SELECT 
        customer_id,
        MIN(occurred_at) AS first_revenue_date,
        MAX(occurred_at) AS last_revenue_date,
        SUM(CASE WHEN type != 'churn' THEN mrr_delta ELSE 0 END) AS total_mrr_added,
        BOOL_OR(type = 'churn') AS churned
    FROM transactions
    GROUP BY customer_id
),
ltv AS (
    SELECT 
        customer_id,
        total_mrr_added * 12 AS arr,
        DATEDIFF('month', first_revenue_date, COALESCE(last_revenue_date, CURRENT_DATE)) AS months_active,
        total_mrr_added AS avg_mrr
    FROM customer_revenue
)
SELECT 
    AVG(avg_mrr) AS avg_monthly_revenue,
    AVG(months_active) AS avg_customer_lifetime_months,
    AVG(avg_mrr * months_active) AS avg_ltv,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY avg_mrr * months_active) AS median_ltv
FROM ltv;

Net Revenue Retention#

-- NRR by cohort
WITH cohort_mrr AS (
    SELECT 
        customer_id,
        DATE_TRUNC('quarter', MIN(occurred_at)) AS cohort_quarter,
        SUM(CASE WHEN occurred_at < DATE_TRUNC('quarter', MIN(occurred_at)) + INTERVAL '3 months'
                 THEN mrr_delta ELSE 0 END) AS starting_mrr
    FROM transactions
    GROUP BY customer_id
),
current_mrr AS (
    SELECT 
        t.customer_id,
        SUM(t.mrr_delta) AS current_mrr
    FROM transactions t
    JOIN cohort_mrr c ON t.customer_id = c.customer_id
    WHERE t.occurred_at >= CURRENT_DATE - INTERVAL '12 months'
    GROUP BY t.customer_id
)
SELECT 
    c.cohort_quarter,
    SUM(c.starting_mrr) AS starting_mrr,
    SUM(cur.current_mrr) AS current_mrr,
    ROUND(SUM(cur.current_mrr) * 100.0 / NULLIF(SUM(c.starting_mrr), 0), 1) AS nrr_pct
FROM cohort_mrr c
LEFT JOIN current_mrr cur ON c.customer_id = cur.customer_id
GROUP BY c.cohort_quarter
ORDER BY c.cohort_quarter;

Headcount Planning#

-- Current headcount and payroll by department
SELECT 
    department,
    COUNT(*) AS headcount,
    SUM(annual_salary) AS annual_payroll,
    AVG(annual_salary) AS avg_salary,
    ROUND(SUM(annual_salary) / SUM(SUM(annual_salary)) OVER () * 100, 1) AS pct_of_total_payroll
FROM employees
WHERE (end_date IS NULL OR end_date > CURRENT_DATE)
    AND start_date <= CURRENT_DATE
GROUP BY department
ORDER BY annual_payroll DESC;
 
-- Monthly burn from headcount
SELECT 
    DATE_TRUNC('month', CURRENT_DATE) AS month,
    SUM(annual_salary / 12) AS monthly_payroll_burn,
    SUM(annual_salary / 12) * 1.25 AS fully_loaded_monthly_burn  -- 25% benefits/overhead
FROM employees
WHERE (end_date IS NULL OR end_date > CURRENT_DATE)
    AND start_date <= CURRENT_DATE;

Cash Flow Model#

WITH monthly_revenue AS (
    SELECT 
        DATE_TRUNC('month', occurred_at) AS month,
        SUM(SUM(mrr_delta)) OVER (ORDER BY DATE_TRUNC('month', occurred_at) ROWS UNBOUNDED PRECEDING) AS mrr
    FROM transactions GROUP BY month
),
monthly_expenses AS (
    SELECT period AS month, SUM(amount) AS total_expenses
    FROM expenses GROUP BY period
),
monthly_payroll AS (
    SELECT 
        DATE_TRUNC('month', CURRENT_DATE) AS month,
        SUM(annual_salary / 12) * 1.25 AS payroll
    FROM employees
    WHERE (end_date IS NULL OR end_date > CURRENT_DATE)
)
SELECT 
    r.month,
    r.mrr AS revenue,
    COALESCE(e.total_expenses, 0) + COALESCE(p.payroll, 0) AS total_expenses,
    r.mrr - (COALESCE(e.total_expenses, 0) + COALESCE(p.payroll, 0)) AS net_cash_flow,
    SUM(r.mrr - (COALESCE(e.total_expenses, 0) + COALESCE(p.payroll, 0))) 
        OVER (ORDER BY r.month ROWS UNBOUNDED PRECEDING) AS cumulative_cash_flow
FROM monthly_revenue r
LEFT JOIN monthly_expenses e ON r.month = e.month
LEFT JOIN monthly_payroll p ON r.month = p.month
ORDER BY r.month;

DenchClaw for Financial Modeling#

DenchClaw uses DuckDB — connect your financial data and the App Builder creates live P&L dashboards, ARR waterfalls, and burn rate charts that update as your data changes. Ask the agent for board-ready financial summaries on demand.

Frequently Asked Questions#

Can DuckDB replace Excel for financial modeling?#

For structured, query-based models, yes. For iterative what-if analysis with formula-based cells, Excel still has advantages. The best approach: use DuckDB for the data and calculations, Excel or Sheets for the presentation layer.

How do I do scenario analysis in DuckDB?#

Create a scenarios table with different assumption sets, then join your model queries to it using a scenario_id filter. Switch scenarios by changing the filter.

Is DuckDB accurate enough for financial calculations?#

Use DECIMAL(18, 4) for all monetary values. This gives you exact decimal arithmetic without floating-point errors.

How do I model revenue recognition for multi-year contracts?#

Create a contract_schedule table with monthly revenue recognition entries per contract, then aggregate by period.

Can I connect DuckDB to accounting software?#

Via CSV export from QuickBooks or Xero, then import to DuckDB. There are also third-party connectors that sync directly.

Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →

Related articles

Keep reading

View all