Date Arithmetic: Beginner
Dates Hide in Every Business Question
Spot date arithmetic needs: "last 30 days," "same day last year," "business days only," "fiscal quarter."
Here is the question every reporting interview asks in some form: 'show me revenue rolled up by month for the past 12 months, with each month's number compared to the same month a year ago.' The candidate who writes DATE_TRUNC('month', txn_date) for grouping, txn_date >= CURRENT_DATE - INTERVAL '12 months' for filtering, and txn_date - INTERVAL '1 year' for the year-ago comparison produces a clean query in eight lines. The candidate who tries to do the same thing with string manipulation on the date column, or who uses BETWEEN with hardcoded dates, produces a query that works for today's interview but breaks in production the moment the month rolls over.
- ▸"revenue rolled up by month / week / quarter"
- ▸"users active in the last N days"
- ▸"this month vs the same month last year"
- ▸"cohort defined by signup week"
- ▸"orders placed during business hours"
- ▸Any question involving rolling windows, period truncation, or relative dates
The four-tool date toolkit
- ▸DATE_TRUNC: truncate to a coarser unit (month/week/quarter) for grouping
- ▸INTERVAL: add or subtract time spans (CURRENT_DATE - INTERVAL '30 days')
- ▸EXTRACT: pull a part (YEAR, MONTH, DOW) for filtering or grouping
- ▸DATE_DIFF: difference between two dates in a named unit
- Uses string operations on date columns (LEFT(date_col, 7) for month)
- Hardcodes date ranges that drift as the calendar moves
- Mixes DATE and TIMESTAMP columns without thinking about timezone
- Reaches for engine-specific functions without checking portability
- Uses DATE_TRUNC for any month/week/quarter grouping
- Uses INTERVAL arithmetic for relative date windows
- Names the engine being targeted (Postgres, Snowflake, BigQuery)
- Considers timezone when the column is a timestamp
Why companies care
DATE_TRUNC for Day, Week, and Month Buckets
Write date manipulation using the three core functions and handle dialect differences (DATEADD vs INTERVAL).
- collapses dates within a unit into the same bucket; one row per (region, month_start).
- anchors the start of a rolling window: DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '12 months'.
- names the bucket so the dashboard renders month_start as a date column.
The canonical pattern
Reading the pattern
The available units
The week-boundary trap
Engine-specific syntax
DATE_ADD and DATE_DIFF Basics
Pull date parts with EXTRACT, handle fiscal year offsets, and explain ISO week numbering.
The canonical windowing patterns
INTERVAL units and how engines parse them
BETWEEN vs >= AND <
- ▸BETWEEN A AND B is inclusive on both sides
- ▸BETWEEN on TIMESTAMP misses most of the last day (midnight-only)
- ▸>= A AND < B (half-open) captures the full period regardless of granularity
- ▸Production rule: always half-open for timestamp columns
- Works for date columns (whole-day granularity)
- Misses most of the last day for timestamp columns
- Reads as English but hides the boundary subtlety
- Off-by-one risk when the column type changes
- Works for both date and timestamp columns
- Captures the entire end period regardless of granularity
- Reads as 'starts at A, runs until B' without ambiguity
- The convention in production data engineering
The 'cohort retention window' pattern
Month and year interval gotchas
INTERVAL also accepts compound forms in some engines: INTERVAL '1 year 6 months' on Postgres. This is convenient but engine-specific. For portability, prefer separate INTERVAL clauses chained together: INTERVAL '1 year' + INTERVAL '6 months.' The arithmetic is the same; the syntax is more universal.
Filtering a Date Range Correctly
Convert between UTC and local time, explain why comparing timestamps across zones requires explicit conversion.
The EXTRACT pattern
Available EXTRACT fields
DATE_DIFF for differences
- DATE_DIFF('day', '2024-01-01 23:00', '2024-01-02 01:00') = 1 even though only 2 hours.
- BigQuery: column-first, unquoted unit; Snowflake: unit-first, quoted; Postgres: subtraction.
- use DATE_DIFF for calendar-day counts; use timestamp subtraction (seconds) for elapsed-time calculations.
Engine-specific DATE_DIFF syntax
The 'inclusive vs exclusive end' question
- Counts unit-boundaries crossed, not unit-intervals elapsed
- DAY counts midnight crossings (1 second after midnight is 1 day later)
- MONTH counts month-boundary crossings, not 30-day intervals
- Good for 'days since signup' calendar counts; bad for 'hours of uptime'
- Returns an interval representing elapsed time (Postgres) or numeric seconds (others)
- Captures sub-day precision down to microseconds
- Good for elapsed-time, SLA, and duration calculations
- Convert to days with seconds / 86400 or extract DAY from interval
Filtering by date component vs date range
Half-Open Intervals vs BETWEEN
Create a date spine using recursive CTE or GENERATE_SERIES and LEFT JOIN to fill calendar gaps in sparse data.
The timezone gotcha
Dates and timestamps without timezone (TIMESTAMP, DATE) are ambiguous; their meaning depends on where the database thinks the user is. Timestamps with timezone (TIMESTAMPTZ on Postgres, TIMESTAMP_TZ on Snowflake) are unambiguous; they store an absolute moment in time. When the column is TIMESTAMPTZ, the engine converts to the session timezone for display and arithmetic; when the column is TIMESTAMP, the value is interpreted as-is. Mixing the two columns or comparing across timezones is the single largest source of date bugs in production.
Engine portability summary
| Operation | Postgres | BigQuery | Snowflake |
|---|---|---|---|
| Truncate to month | DATE_TRUNC('month', d) | DATE_TRUNC(d, MONTH) | DATE_TRUNC('month', d) |
| Add 30 days | d + INTERVAL '30 days' | DATE_ADD(d, INTERVAL 30 DAY) | DATEADD('day', 30, d) |
| Extract year | EXTRACT(YEAR FROM d) | EXTRACT(YEAR FROM d) | YEAR(d) or EXTRACT(...) |
| Diff in days | (end - start)::INT | DATE_DIFF(end, start, DAY) | DATEDIFF('day', start, end) |
| Current date | CURRENT_DATE | CURRENT_DATE() | CURRENT_DATE() |
Common bugs and their mitigations
| Situation | Phrasing that flatlines | Phrasing that lands |
|---|---|---|
| You see 'revenue by month for last year' | "GROUP BY MONTH(date)." | "DATE_TRUNC('month', txn_date) in SELECT and GROUP BY, plus a half-open WHERE filter for the time range." |
| The interviewer asks 'last 30 days' | "BETWEEN '2024-01-01' AND '2024-01-31'." | "WHERE txn_date >= CURRENT_DATE - INTERVAL '30 days'. Half-open interval avoids the end-of-day ambiguity; rolling window doesn't drift as the date moves." |
| The data has timestamps in UTC, dashboard in Pacific | "DATE_TRUNC('day', ts)." | "DATE_TRUNC('day', ts AT TIME ZONE 'America/Los_Angeles') if I want Pacific-day buckets. Otherwise UTC truncation, and the consumer's display layer handles timezone." |
| The interviewer asks 'year over year by month' | "BETWEEN dates." | "DATE_TRUNC('month', txn_date) for the bucket; txn_date - INTERVAL '12 months' for the comparison column. Both pre-aggregated in a CTE before the LAG." |
| The engine is BigQuery | "DATE_TRUNC('month', date)." | "DATE_TRUNC(date, MONTH) on BigQuery (column first, unquoted unit). DATE_TRUNC('month', date) is Postgres / Snowflake." |
The closing summary
> You are in a data engineering phone screen at a marketplace company. The interviewer asks: 'Show me revenue rolled up by month for the past 12 months, and add a column comparing each month to the same month a year ago.'
DATE_TRUNC for grouping to a reporting grain, INTERVAL for rolling windows, EXTRACT for filtering by component, and DATE_DIFF for differences.txn_date >= '2024-01-01' AND txn_date < '2024-02-01' captures all of January whether the column is a date or a timestamp, while BETWEEN through '2024-01-31' loses everything after midnight on the last day.CURRENT_DATE - INTERVAL '30 days' or DATE_TRUNC('month', CURRENT_DATE) so the query stays correct without revisiting.DATE_DIFF argument order. Name the target engine when you write the query.INTERVAL '1 month' is calendar arithmetic, so January 31 plus one month three times lands on April 28, not April 30. Use INTERVAL '30 days' when you need a fixed-length window.Every data engineering question involves dates; most candidates fumble timezone math
- Category
- SQL
- Difficulty
- beginner
- Duration
- 25 minutes
- Challenges
- 0 hands-on challenges
Topics covered: Dates Hide in Every Business Question, DATE_TRUNC for Day, Week, and Month Buckets, DATE_ADD and DATE_DIFF Basics, Filtering a Date Range Correctly, Half-Open Intervals vs BETWEEN
Lesson Sections
- Dates Hide in Every Business Question (concepts: sqlDateTrunc)
The four-tool date toolkit Four functions cover 90% of date arithmetic in production SQL. DATE_TRUNC truncates a date or timestamp to a coarser unit; DATE_TRUNC('month', '2024-03-15') returns '2024-03-01.' This is the workhorse for grouping. INTERVAL arithmetic adds or subtracts time spans; CURRENT_DATE - INTERVAL '30 days' returns the date 30 days ago. This is the workhorse for windowing. EXTRACT pulls a part out of a date or timestamp; EXTRACT(YEAR FROM txn_date) returns 2024. This is the work
- DATE_TRUNC for Day, Week, and Month Buckets (concepts: sqlDateTrunc)
DATE_TRUNC is the function you reach for whenever the question asks for 'by month' or 'by week' or 'by quarter.' It takes a unit and a date or timestamp; it returns the date or timestamp truncated to the start of that unit. DATE_TRUNC('month', '2024-03-15') returns '2024-03-01.' DATE_TRUNC('week', '2024-03-15') returns the Monday or Sunday of that week (depending on the engine). DATE_TRUNC('quarter', '2024-03-15') returns '2024-01-01.' The truncated value is what you GROUP BY when the consumer w
- DATE_ADD and DATE_DIFF Basics (concepts: sqlDateAdd)
INTERVAL is the SQL primitive for adding or subtracting time spans. CURRENT_DATE + INTERVAL '7 days' returns next week. signup_date + INTERVAL '90 days' returns the date 90 days after signup. The result is a date or timestamp (depending on the input); the arithmetic respects calendar rules (month and year intervals handle variable-length months correctly). INTERVAL is the workhorse for relative date windows. The canonical windowing patterns INTERVAL units and how engines parse them Most engines
- Filtering a Date Range Correctly (concepts: sqlDateDiff)
EXTRACT pulls a part out of a date or timestamp value. EXTRACT(YEAR FROM txn_date) returns 2024 from the date '2024-03-15.' EXTRACT(MONTH FROM ...) returns 3. EXTRACT(DOW FROM ...) returns the day of week (0-6, with 0 being Sunday on most engines). EXTRACT is the tool for filtering by date component (only December rows; only Mondays) or for grouping by date component (revenue per month-of-year across all years). The EXTRACT pattern Available EXTRACT fields Standard fields: YEAR, QUARTER, MONTH,
- Half-Open Intervals vs BETWEEN (concepts: sqlRecursiveCte)
The two areas where date arithmetic ships the most bugs in production: timezone handling and engine portability. Both have specific patterns and known mitigations. Naming them in the interview, even briefly, reads as production-experience that the basic-syntax candidate does not have. The timezone gotcha Daylight saving time creates a specific failure mode. In US Eastern, the local clock skips 2 AM in March and repeats 1 AM in November. A timestamp '2024-03-10 02:30 US/Eastern' does not exist; e