Recipe Tableau and SQL
How do I compare this year to date with last year, fairly?
Anchor on the last date in the data with { FIXED : MAX([order_date]) }, sum this year up to that date, and sum last year up to the same day: DATEADD('year', -1, [Max Date]). Comparing whole months instead goes wrong mid-month: in this shop it reads −2.9%, while on matched days sales grew 3.7%.
The data
Fern & Fig plant shop, daily sales to 17 October 2025
An online houseplant shop with a spring rush and a bigger October, when people bring plants indoors. The data was last refreshed on 17 October 2025.
order_date | orders | revenue |
|---|---|---|
| 2023-01-01 | 50 | 2002.44 |
| 2023-01-02 | 41 | 1493.18 |
| 2023-01-03 | 39 | 1488.62 |
| 2023-01-04 | 42 | 1748.48 |
Step 1
Each year, month by month
2025 tracks a little above 2024 most of the year, then falls off a cliff in the last month. Nothing happened to the shop: that month simply isn't over.
Tableau
SUM([revenue])
// MONTH([order_date]) on Columns, YEAR([order_date]) on ColorSQL
WITH d AS (
SELECT *
FROM input
WHERE order_date <= DATE '{knob.asof}'
)
SELECT
CAST (year(order_date) AS VARCHAR) AS year,
month(order_date) AS month_of_year,
strftime(MIN(order_date), '%b') AS month_name,
SUM(revenue) AS revenue
FROM d
GROUP BY 1, 2
ORDER BY 1, 2Rows
| Year | Month of year | Month name | Revenue |
|---|---|---|---|
| 2023 | 1 | Jan | $52,766 |
| 2024 | 1 | Jan | $58,336 |
| 2025 | 1 | Jan | $60,671 |
| 2023 | 2 | Feb | $56,267 |
| 2024 | 2 | Feb | $60,613 |
| 2025 | 2 | Feb | $59,437 |
| 2023 | 3 | Mar | $88,814 |
| 2024 | 3 | Mar | $90,901 |
| 2025 | 3 | Mar | $92,491 |
| 2023 | 4 | Apr | $98,273 |
| 2024 | 4 | Apr | $102,244 |
| 2025 | 4 | Apr | $112,592 |
| 2023 | 5 | May | $98,582 |
| 2024 | 5 | May | $104,074 |
| 2025 | 5 | May | $110,958 |
| 2023 | 6 | Jun | $72,705 |
| 2024 | 6 | Jun | $76,382 |
| 2025 | 6 | Jun | $79,769 |
| 2023 | 7 | Jul | $66,609 |
| 2024 | 7 | Jul | $70,366 |
| 2025 | 7 | Jul | $69,378 |
| 2023 | 8 | Aug | $62,598 |
| 2024 | 8 | Aug | $67,006 |
| 2025 | 8 | Aug | $68,746 |
| 2023 | 9 | Sep | $84,058 |
| 2024 | 9 | Sep | $87,184 |
| 2025 | 9 | Sep | $91,432 |
| 2023 | 10 | Oct | $115,661 |
| 2024 | 10 | Oct | $119,410 |
| 2025 | 10 | Oct | $67,008 |
| 2023 | 11 | Nov | $84,600 |
| 2024 | 11 | Nov | $90,915 |
| 2023 | 12 | Dec | $78,385 |
| 2024 | 12 | Dec | $81,674 |
Step 2
Compare calendar years
A part year against a whole one. Down 19.5% looks like a crisis, and it's only the calendar.
Tableau
SUM([revenue])
// YEAR([order_date]) on ColumnsSQL
WITH d AS (
SELECT *
FROM input
WHERE order_date <= DATE '{knob.asof}'
), y AS (
SELECT
CAST (year(order_date) AS VARCHAR) AS year,
SUM(revenue) AS revenue
FROM d
GROUP BY 1
)
SELECT
year,
revenue,
revenue / LAG(revenue) OVER (ORDER BY year) - 1 AS yoy
FROM y
ORDER BY yearRows
| Year | Revenue | YoY |
|---|---|---|
| 2023 | $959,318 | – |
| 2024 | $1,009,104 | 0.05 |
| 2025 | $812,482 | −0.19 |
Step 3
Compare each month with the same month last year
Month against month is fair for every finished month. The last one compares 17 days with a whole month and shows −43.9%.
Tableau
SUM([revenue]) / LOOKUP(SUM([revenue]), -12) - 1
// continuous MONTH([order_date]) on Columns; Compute Using: order_dateSQL
WITH d AS (
SELECT *
FROM input
WHERE order_date <= DATE '{knob.asof}'
), m AS (
SELECT
CAST (date_trunc('month', order_date) AS DATE) AS month,
SUM(revenue) AS revenue
FROM d
GROUP BY 1
)
SELECT
month,
revenue,
revenue / LAG(revenue, 12) OVER (ORDER BY month) - 1 AS yoy
FROM m
ORDER BY monthRows
| Month | Revenue | YoY |
|---|---|---|
| 2023-01-01 | $52,766 | – |
| 2023-02-01 | $56,267 | – |
| 2023-03-01 | $88,814 | – |
| 2023-04-01 | $98,273 | – |
| 2023-05-01 | $98,582 | – |
| 2023-06-01 | $72,705 | – |
| 2023-07-01 | $66,609 | – |
| 2023-08-01 | $62,598 | – |
| 2023-09-01 | $84,058 | – |
| 2023-10-01 | $115,661 | – |
| 2023-11-01 | $84,600 | – |
| 2023-12-01 | $78,385 | – |
| 2024-01-01 | $58,336 | 10.6% |
| 2024-02-01 | $60,613 | 7.7% |
| 2024-03-01 | $90,901 | 2.3% |
| 2024-04-01 | $102,244 | 4% |
| 2024-05-01 | $104,074 | 5.6% |
| 2024-06-01 | $76,382 | 5.1% |
| 2024-07-01 | $70,366 | 5.6% |
| 2024-08-01 | $67,006 | 7% |
| 2024-09-01 | $87,184 | 3.7% |
| 2024-10-01 | $119,410 | 3.2% |
| 2024-11-01 | $90,915 | 7.5% |
| 2024-12-01 | $81,674 | 4.2% |
| 2025-01-01 | $60,671 | 4% |
| 2025-02-01 | $59,437 | −1.9% |
| 2025-03-01 | $92,491 | 1.7% |
| 2025-04-01 | $112,592 | 10.1% |
| 2025-05-01 | $110,958 | 6.6% |
| 2025-06-01 | $79,769 | 4.4% |
| 2025-07-01 | $69,378 | −1.4% |
| 2025-08-01 | $68,746 | 2.6% |
| 2025-09-01 | $91,432 | 4.9% |
| 2025-10-01 | $67,008 | −43.9% |
Is the shop up or down this year?
Up. With data through 17 October 2025, 2025 is 3.75% ahead of the same days in 2024. The usual shortcut, filtering both years to January through October, says sales fell 2.87%: it keeps all 31 days of last October and only 17 of this one. October is this shop's biggest month, so the missing days outweigh a year of growth. Set Data runs to to 30 June and the two methods agree: at a month end there is no partial month to distort.
Common mistake
Filtering to whole months
Filtering both years to January through the current month keeps all of last year's last month and only part of this year's. Mid-month, that alone can turn growth into decline. Cut both years at the same day, not the same month.
Tableau
MONTH([order_date]) <= MONTH([Max Date])
// on FiltersSQL
WITH b AS (
SELECT MAX(order_date) AS max_date
FROM input
)
SELECT year(order_date) AS year, SUM(revenue) AS revenue
FROM input, b
WHERE month(order_date) <= month(max_date)
GROUP BY 1
ORDER BY 1Tableau
DATEADD('year', YEAR([Max Date]) - YEAR([order_date]), [order_date]) <= [Max Date]
// on FiltersSQL
WITH b AS (
SELECT MAX(order_date) AS max_date
FROM input
)
SELECT year(order_date) AS year, SUM(revenue) AS revenue
FROM input, b
WHERE order_date + to_years(CAST (year(max_date) - year(order_date) AS INTEGER)) <= max_date
GROUP BY 1
ORDER BY 1Step 4
Anchor on the last date in the data
One date drives the whole comparison: this year runs to it, last year runs to the same day a year earlier. A FIXED expression with no dimension returns the same value on every row, so filters on the view don't move it. If you want a date filter to move the anchor, add it to context.
Tableau
{ FIXED : MAX([order_date]) }
// name it [Max Date]SQL
WITH d AS (
SELECT *
FROM input
WHERE order_date <= DATE '{knob.asof}'
)
SELECT
MAX(order_date) AS max_date,
CAST (MAX(order_date) - INTERVAL 1 YEAR AS DATE) AS prior_year_cutoff
FROM dRows
| Order date | Prior year cutoff |
|---|---|
| 2025-10-17 | 2024-10-17 |
Step 5
Keep the same days in every year
Move every date into the latest year and keep it only if it lands on or before the anchor. Each year now covers the same stretch of the calendar, so the bars compare like with like, and it works for any number of years.
Tableau
IF DATEADD('year', YEAR([Max Date]) - YEAR([order_date]), [order_date]) <= [Max Date] THEN [revenue]
END
// YEAR([order_date]) on ColumnsSQL
WITH d AS (
SELECT *
FROM input
WHERE order_date <= DATE '{knob.asof}'
), b AS (
SELECT MAX(order_date) AS max_date
FROM d
), y AS (
SELECT
CAST (year(order_date) AS VARCHAR) AS year,
SUM(revenue) AS ytd_revenue
FROM d, b
WHERE order_date + to_years(CAST (year(max_date) - year(order_date) AS INTEGER)) <= max_date
GROUP BY 1
)
SELECT
year,
ytd_revenue,
ytd_revenue / LAG(ytd_revenue) OVER (ORDER BY year) - 1 AS yoy
FROM y
ORDER BY yearRows
| Year | Revenue | YoY |
|---|---|---|
| 2023 | $744,809 | – |
| 2024 | $783,135 | 0.05 |
| 2025 | $812,482 | 0.04 |
Step 6
YTD, last year to date, and growth
The two KPI fields most dashboards need. Because each is a row-level IF, they add up correctly at any level: by product, by region, by month.
YoY
3.7%
Tableau
SUM([YTD]) / SUM([PYTD]) - 1
// YTD: IF YEAR([order_date]) = YEAR([Max Date]) AND [order_date] <= [Max Date] THEN [revenue] END; PYTD: IF YEAR([order_date]) = YEAR([Max Date]) - 1 AND [order_date] <= DATEADD('year', -1, [Max Date]) THEN [revenue] ENDSQL
WITH d AS (
SELECT *
FROM input
WHERE order_date <= DATE '{knob.asof}'
), b AS (
SELECT MAX(order_date) AS max_date
FROM d
), k AS (
SELECT
SUM(revenue) FILTER (WHERE year(order_date) = year(max_date)) AS ytd,
SUM(revenue) FILTER (WHERE year(order_date) = year(max_date) - 1 AND order_date <= max_date - INTERVAL 1 YEAR) AS pytd
FROM d, b
)
SELECT ytd, pytd, ytd / pytd - 1 AS yoy
FROM kRows
| YTD | Pytd | YoY |
|---|---|---|
| 81248153% | 78313515% | 3.7% |