Growth compares this year’s year-to-date total with last year’s over the same dates: (this year − last year) / last year. The formula fits on one line. The work is in the rows where it does not apply, such as a product that did not exist last year.
A small example
Three products. Apples sold in both years, Pears are new this year and Plums sold only last year. The orders after 15 May fall outside the comparison in both years.
CREATE TEMP TABLE Sales (OrderDate DATE, Product VARCHAR, Amount NUMBER(10,2));
INSERT INTO Sales VALUES
('2025-02-10', 'Apples', 90), ('2025-04-20', 'Apples', 110),
('2025-05-16', 'Apples', 500), ('2025-03-01', 'Plums', 60),
('2026-01-15', 'Apples', 120), ('2026-04-02', 'Apples', 130),
('2026-03-10', 'Pears', 70), ('2026-05-20', 'Apples', 400);
The query
The periods are the same as in how to compare YTD with the same period last year: 1 January to 15 May of each year.
WITH Params AS (
SELECT '2026-05-15'::DATE AS AsOf -- CURRENT_DATE() in a live report
),
Periods AS (
SELECT 'This year' AS Period,
DATE_TRUNC('year', AsOf) AS StartDate,
AsOf AS EndDate
FROM Params
UNION ALL
SELECT 'Last year',
DATE_TRUNC('year', DATEADD(year, -1, AsOf)),
DATEADD(year, -1, AsOf)
FROM Params
),
Totals AS (
SELECT s.Product,
SUM(IFF(p.Period = 'This year', s.Amount, 0)) AS YTD,
SUM(IFF(p.Period = 'Last year', s.Amount, 0)) AS YTDLastYear
FROM Periods p
JOIN Sales s
ON s.OrderDate >= p.StartDate
AND s.OrderDate < DATEADD(day, 1, p.EndDate)
GROUP BY ROLLUP (s.Product)
)
SELECT COALESCE(t.Product, 'Total') AS Product,
t.YTD,
t.YTDLastYear,
ROUND(100 * (t.YTD - t.YTDLastYear) / NULLIF(t.YTDLastYear, 0), 1) AS GrowthPct
FROM Totals t
ORDER BY t.Product NULLS LAST;
| Product | YTD | YTDLastYear | GrowthPct |
|---|---|---|---|
| Apples | 250.00 | 200.00 | 25.0 |
| Pears | 70.00 | 0.00 | NULL |
| Plums | 0.00 | 60.00 | -100.0 |
| Total | 320.00 | 260.00 | 23.1 |
NULLIF(YTDLastYear, 0)turns a zero base intoNULL, and a division byNULLreturnsNULL. Pears get no growth figure, which is correct: growth from nothing is not a percentage. A report can label the row “New” instead.- Plums show −100%: the product sold nothing this year.
ROLLUPadds the total row, withNULLinProduct. Its growth comes from the summed totals, 320 against 260. An average of the product percentages would be a different and meaningless number, and Pears have none to average.
Why not DIV0
DIV0(a, b) returns 0 when b is 0. For growth that would show Pears at 0%, “no change”, which is
the opposite of what happened. Keep the NULL and let the report decide how to show it.
Ratio or percentage
If the result goes to a BI tool that formats percentages, return the ratio,
(YTD - YTDLastYear) / NULLIF(YTDLastYear, 0), without 100 * and without ROUND. The tool
multiplies by 100 itself, and a number rounded in SQL loses precision in any later calculation.
A negative base
Refunds and returns can make last year’s net total negative. With −50 last year and 20 this year, the
formula gives −140%, a decline, although the result improved by 70. Dividing by
ABS(YTDLastYear) gives +140%, so the sign follows the direction of the change. Many teams prefer to
show no percentage at all when the base is negative.