What Moved — Monthly P&L Driver Analysis
What Moved — Monthly P&L Driver Analysis: a 4-step workflow for QuickBooks / spreadsheet data over columns like section, account, 2025-08, 2025-09. Every step is plain SQL you can read — open it, connect your data, and make it your own. No data is shared in this template, only the recipe.
4 steps · August 4, 2026
No data is shared in this template. It contains only the recipe — column names and SQL logic. When you run it, your data is processed in your own browser and never leaves your machine.
What data it expects
QBO_ProfitAndLoss_Month.csv
section · VARCHARaccount · VARCHAR2025-08 · DOUBLE2025-09 · DOUBLE2025-10 · DOUBLE2025-11 · DOUBLE2025-12 · DOUBLE2026-01 · DOUBLE2026-02 · DOUBLE2026-03 · DOUBLE2026-04 · DOUBLE2026-05 · DOUBLE2026-06 · DOUBLE2026-07 · DOUBLETotal · DOUBLE
Connect QuickBooks Online or drop a CSV / Excel export with a similar layout — the AI adapts the workflow if your columns differ.
How it works — every step, readable
- 011) Load QuickBooks P&L Grid
SELECT * FROM read_csv_auto('QBO_ProfitAndLoss_Month.csv', header=true) - 022) Unpivot Months to Long Formsorts the output
SELECT section, account, period, ROUND(COALESCE(TRY_CAST(REPLACE(CAST(amount AS VARCHAR), ',', '') AS DOUBLE), 0), 2) AS amount FROM ( UNPIVOT input_1 ON COLUMNS(* EXCLUDE (section, account, Total)) INTO NAME period VALUE amount ) ORDER BY period, section, account - 033) Monthly Summary by Sectionaggregates rows into summary totals · buckets values by condition · appends result sets (e.g. a TOTAL row)
WITH section_months AS ( SELECT CASE WHEN lower(section) IN ('income', 'revenue') THEN 'Income' WHEN lower(section) IN ('cost of goods sold', 'cogs') THEN 'Cost of Goods Sold' WHEN lower(section) IN ('expenses', 'expense', 'operating expenses') THEN 'Expenses' ELSE section END AS section, period, SUM(amount) AS amount FROM input_1 WHERE account NOT ILIKE '%total%' GROUP BY 1, 2 ), pnl AS ( SELECT 10 AS sort_order, 'Income' AS section, period, SUM(CASE WHEN section = 'Income' THEN amount ELSE 0 END) AS amount FROM section_months GROUP BY period UNION ALL SELECT 20 AS sort_order, 'Cost of Goods Sold' AS section, period, SUM(CASE WHEN section = 'Cost of Goods Sold' THEN amount ELSE 0 END) AS amount FROM section_months GROUP BY period UNION ALL SELECT 30 AS sort_order, 'Expenses' AS section, period, SUM(CASE WHEN section = 'Expenses' THEN amount ELSE 0 END) AS amount FROM section_months GROUP BY period UNION ALL SELECT 40 AS sort_order, 'Net' AS section, period, SUM(CASE WHEN section = 'Income' THEN amount ELSE 0 END) - SUM(CASE WHEN section = 'Cost of Goods Sold' THEN amount ELSE 0 END) - SUM(CASE WHEN section = 'Expenses' THEN amount ELSE 0 END) AS amount FROM section_months GROUP BY period ) SELECT section, ROUND(SUM(CASE WHEN period = '2025-08' THEN amount ELSE 0 END), 0) AS "2025-08", ROUND(SUM(CASE WHEN period = '2025-09' THEN amount ELSE 0 END), 0) AS "2025-09", ROUND(SUM(CASE WHEN period = '2025-10' THEN amount ELSE 0 END), 0) AS "2025-10", ROUND(SUM(CASE WHEN period = '2025-11' THEN amount ELSE 0 END), 0) AS "2025-11", ROUND(SUM(CASE WHEN period = '2025-12' THEN amount ELSE 0 END), 0) AS "2025-12", ROUND(SUM(CASE WHEN period = '2026-01' THEN amount ELSE 0 END), 0) AS "2026-01", ROUND(SUM(CASE WHEN period = '2026-02' THEN amount ELSE 0 END), 0) AS "2026-02", ROUND(SUM(CASE WHEN period = '2026-03' THEN amount ELSE 0 END), 0) AS "2026-03", ROUND(SUM(CASE WHEN period = '2026-04' THEN amount ELSE 0 END), 0) AS "2026-04", ROUND(SUM(CASE WHEN period = '2026-05' THEN amount ELSE 0 END), 0) AS "2026-05", ROUND(SUM(CASE WHEN period = '2026-06' THEN amount ELSE 0 END), 0) AS "2026-06", ROUND(SUM(CASE WHEN period = '2026-07' THEN amount ELSE 0 END), 0) AS "2026-07" FROM pnl GROUP BY sort_order, section ORDER BY sort_order - 044) Driver Analysis — Latest Month vs Prior Monthaggregates rows into summary totals · buckets values by condition · computes running / windowed totals
WITH periods AS ( SELECT MAX(period) AS latest_period, (SELECT MAX(period) FROM input_1 WHERE period < (SELECT MAX(period) FROM input_1)) AS prior_period FROM input_1 ), account_months AS ( SELECT section, account, SUM(CASE WHEN period = (SELECT prior_period FROM periods) THEN amount ELSE 0 END) AS prior_amount, SUM(CASE WHEN period = (SELECT latest_period FROM periods) THEN amount ELSE 0 END) AS latest_amount FROM input_1 WHERE account NOT ILIKE '%total%' AND account NOT ILIKE 'net%' AND account NOT ILIKE 'gross profit%' GROUP BY section, account ), ranked AS ( SELECT section, account, ROUND(prior_amount, 0) AS "prior month", ROUND(latest_amount, 0) AS "latest month", ROUND(latest_amount - prior_amount, 0) AS delta, CASE WHEN prior_amount = 0 AND latest_amount <> 0 THEN NULL WHEN prior_amount = 0 THEN 0 ELSE ROUND(((latest_amount - prior_amount) / ABS(prior_amount)) * 100, 1) END AS "% change", ROW_NUMBER() OVER (ORDER BY ABS(latest_amount - prior_amount) DESC) AS rank FROM account_months ) SELECT section, account, "prior month", "latest month", delta, "% change" FROM ranked WHERE rank <= 15 ORDER BY ABS(delta) DESC
Run this on your books
Free to try — no sign-up, no card. The workflow runs in your browser; your data never leaves your machine.
Make it your own →