Back to The Times of Claw

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

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 →

Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA