Month-over-Month Growth in SQL

Month-over-month growth compares each month's figure with the month before it, as a percentage change.

Example query

This example assumes: orders(id, order_date date, total_amount numeric). It is written in PostgreSQL syntax, and your table and column names will differ from these.

WITH monthly AS (
  SELECT DATE_TRUNC('month', order_date)::date AS month,
         SUM(total_amount) AS sales
  FROM orders
  GROUP BY 1
)
SELECT month,
       sales,
       ROUND(100.0 * (sales - LAG(sales) OVER (ORDER BY month))
             / NULLIF(LAG(sales) OVER (ORDER BY month), 0), 1) AS mom_growth_pct
FROM monthly
ORDER BY month;

How it works

  • The CTE gives one row per month with that month's sales.
  • LAG(sales) fetches the previous month's sales from the row above, in month order.
  • The percentage change is the difference divided by the previous month, with NULLIF avoiding a division by zero.

MySQL, BigQuery, SQL Server and SQLite differences

  • Replace DATE_TRUNC with the equivalent for your database (see group by month); the LAG logic is the same in MySQL 8+, SQL Server, BigQuery and SQLite 3.25+.

Common mistakes

  • If a month has no rows, LAG compares with the month before it, not the calendar month before; fill the missing months first.
  • The latest month is usually incomplete and will look like a decline.
  • ROUND needs a numeric type in PostgreSQL; cast a floating-point column to numeric first.

Ask for it in plain English

Instead of writing the query yourself, you can type the question. SchemaChat turns it into SQL against your connected database or uploaded file, runs it, and shows the SQL and the figures it used so you can check the answer before you share it. Check that the query matches the definition your business uses.

  • What was our month-over-month sales growth for the last 12 months?
  • Which months had negative growth?

Frequently asked questions

What is month-over-month growth in SQL?
Month-over-month growth compares each month's figure with the month before it, as a percentage change.
How does the month-over-month growth query work?
The CTE gives one row per month with that month's sales. LAG(sales) fetches the previous month's sales from the row above, in month order. The percentage change is the difference divided by the previous month, with NULLIF avoiding a division by zero.
What commonly goes wrong with month-over-month growth in SQL?
If a month has no rows, LAG compares with the month before it, not the calendar month before; fill the missing months first.