Skip to content

How to Answer Any “Show Me the Last 3 Months” Question With SQL

    Hello ,

    “Show me bookings from the last 90 days.”

    “How does this quarter compare to last quarter?”

    “What’s the week-over-week trend?”

    Time-based questions come up constantly in business. And most analysts handle them the worst possible way: by hardcoding dates.

    -- Don't do this
    WHERE booking_date >= '2026-01-01' AND booking_date < '2026-04-01'

    This works today. Next quarter, you have to remember to change the dates. You won’t remember. And the report will silently show stale data until someone notices.

    Here are three date function patterns that make your time-based queries automatic.

    Pattern 1: Rolling Windows With INTERVAL

    “Show me the last N days/months/years” — this is probably the most common time-based request.

    -- Bookings from the last 90 days
    SELECT 
        b.booking_id,
        c.first_name || ' ' || c.last_name AS customer_name,
        e.expedition_name,
        b.booking_date::date AS booked_on,
        b.total_amount
    FROM bookings b
        INNER JOIN customers c ON b.customer_id = c.customer_id
        INNER JOIN expedition_instances ei ON b.instance_id = ei.instance_id
        INNER JOIN expeditions e ON ei.expedition_id = e.expedition_id
    WHERE b.booking_date >= CURRENT_DATE - INTERVAL '90 days'
        AND b.status IN ('completed', 'confirmed')
    ORDER BY b.booking_date DESC;

    I ran this against the Summit Adventures database (the fake adventure tourism company I created to help people learn business analytics). The key line:

    WHERE b.booking_date >= CURRENT_DATE - INTERVAL '90 days'

    `CURRENT_DATE` always returns today’s date. `INTERVAL ’90 days’` subtracts 90 days from it. Run this query on March 1st, it looks back to December 1st. Run it on June 15th, it looks back to March 17th. No manual updates needed.

    Common INTERVAL values:
    `INTERVAL ‘7 days’` — last week
    `INTERVAL ’30 days’` — last month (approximate)
    `INTERVAL ’90 days’` — last quarter (approximate)
    `INTERVAL ‘6 months’` — last half year
    `INTERVAL ‘1 year’` — last year

    Pattern 2: Grouping by Time Period With DATE_TRUNC

    “Show me revenue by month” or “bookings by week” — you need to group timestamps into periods.

    -- Weekly booking volume for the last 3 months
    SELECT 
        DATE_TRUNC('week', b.booking_date)::date AS week_starting,
        COUNT(*) AS bookings_this_week,
        SUM(b.total_amount) AS weekly_revenue,
        ROUND(AVG(b.total_amount), 2) AS avg_booking_value
    FROM bookings b
    WHERE b.booking_date >= CURRENT_DATE - INTERVAL '3 months'
        AND b.status IN ('completed', 'confirmed')
    GROUP BY DATE_TRUNC('week', b.booking_date)
    ORDER BY week_starting DESC;

    `DATE_TRUNC(‘week’, booking_date)` takes any timestamp and rounds it down to the start of that week. So bookings on Monday, Wednesday, and Friday of the same week all get grouped together.

    DATE_TRUNC options:
    `’day’` — group by day
    `’week’` — group by week (starting Monday)
    `’month’` — group by month (starting on the 1st)
    `’quarter’` — group by quarter (Q1, Q2, Q3, Q4)
    `’year’` — group by year

    Pattern 3: Comparing Time Periods

    “How did this month compare to last month?” — this combines date functions with the comparison techniques we covered a few weeks ago.

    -- This month vs last month: booking volume and revenue
    WITH this_month AS (
        SELECT 
            COUNT(*) AS bookings,
            COALESCE(SUM(total_amount), 0) AS revenue
        FROM bookings
        WHERE booking_date >= DATE_TRUNC('month', CURRENT_DATE)
            AND status IN ('completed', 'confirmed')
    ),
    last_month AS (
        SELECT 
            COUNT(*) AS bookings,
            COALESCE(SUM(total_amount), 0) AS revenue
        FROM bookings
        WHERE booking_date >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'
            AND booking_date < DATE_TRUNC('month', CURRENT_DATE)
            AND status IN ('completed', 'confirmed')
    )
    SELECT 
        tm.bookings AS this_month_bookings,
        lm.bookings AS last_month_bookings,
        tm.bookings - lm.bookings AS booking_change,
        tm.revenue AS this_month_revenue,
        lm.revenue AS last_month_revenue,
        CASE 
            WHEN lm.revenue > 0 
            THEN ROUND((tm.revenue - lm.revenue) * 100.0 / lm.revenue, 1)
            ELSE NULL 
        END AS revenue_change_pct
    FROM this_month tm, last_month lm;

    This query always compares the current month-to-date with the previous full month. No hardcoded dates. It updates automatically every time you run it.

    Putting It All Together: The Executive Dashboard Query

    Here’s how these patterns combine into a practical business report:

    -- Executive summary: current performance with trend context
    WITH current_quarter AS (
        SELECT 
            COUNT(DISTINCT b.booking_id) AS bookings,
            COUNT(DISTINCT b.customer_id) AS unique_customers,
            SUM(p.amount) AS revenue
        FROM bookings b
            INNER JOIN payments p ON b.booking_id = p.booking_id
        WHERE b.booking_date >= DATE_TRUNC('quarter', CURRENT_DATE)
            AND p.payment_status = 'completed'
            AND b.status IN ('completed', 'confirmed')
    ),
    previous_quarter AS (
        SELECT 
            COUNT(DISTINCT b.booking_id) AS bookings,
            COUNT(DISTINCT b.customer_id) AS unique_customers,
            SUM(p.amount) AS revenue
        FROM bookings b
            INNER JOIN payments p ON b.booking_id = p.booking_id
        WHERE b.booking_date >= DATE_TRUNC('quarter', CURRENT_DATE) - INTERVAL '3 months'
            AND b.booking_date < DATE_TRUNC('quarter', CURRENT_DATE)
            AND p.payment_status = 'completed'
            AND b.status IN ('completed', 'confirmed')
    )
    SELECT 
        cq.bookings AS current_qtr_bookings,
        pq.bookings AS prev_qtr_bookings,
        cq.revenue AS current_qtr_revenue,
        pq.revenue AS prev_qtr_revenue,
        cq.unique_customers AS current_qtr_customers,
        CASE 
            WHEN pq.revenue > 0 
            THEN ROUND((cq.revenue - pq.revenue) * 100.0 / pq.revenue, 1) 
            ELSE NULL 
        END AS revenue_change_pct
    FROM current_quarter cq, previous_quarter pq;

    This gives your CEO everything in one query: current quarter performance, previous quarter comparison, and the percentage change. All automatic.

    The Key Principle

    Never hardcode dates. Use these three tools instead:

    | Need | Tool | Example |
    |——|——|———|
    | Rolling window | `CURRENT_DATE – INTERVAL` | Last 90 days |
    | Group by period | `DATE_TRUNC()` | Revenue by month |
    | Period boundaries | `DATE_TRUNC() ± INTERVAL` | This quarter vs last |

    Once you internalize these patterns, every “show me the last X” question becomes a 2-minute query.

    Try This At Your Job

    Take a report you update manually (changing date ranges each time) and replace the hardcoded dates with CURRENT_DATE and INTERVAL. You’ll never have to touch those dates again.

    Until next time,
    Brian ([say hi on twitter!](https://twitter.com/briangraves))

    P.S. Time-based analysis is the focus of Module 3 of SQL for Business Impact, where we use the Heartrate Monitor Blueprint to understand booking patterns over time. If you want to go deeper into trend analysis, seasonal patterns, and time-series queries, check it out at [sqlforbusinessimpact.com](https://sqlforbusinessimpact.com).

    P.P.S. Do you have reports with hardcoded dates that break every month? Reply and tell me about them — I might feature the fix in a future email. I read every response.


    Want More SQL Insights Like This?

    Join thousands of analysts getting weekly tips on turning data into business impact.

    Subscribe to Analytics in Action →

    Leave a Reply

    Your email address will not be published. Required fields are marked *