SQL Date Functions: DATE_TRUNC, EXTRACT, DATEDIFF (2026)

You'll reach for DATE_TRUNC more times in your first month on the job than any other date function. Cohort retention, weekly active users, monthly revenue rollups: all of them are one DATE_TRUNC call away. This page is the reference you keep open in a second tab. Postgres, Snowflake, BigQuery, and MySQL syntax side by side so you don't have to hunt for the right flavor.

5
Date functions cover 95% of needs
4
Major SQL dialects
UTC
Where to store all timestamps
IANA
Timezone naming standard

DATE_TRUNC

-- DATE_TRUNC rounds a timestamp DOWN to the specified precision.
-- 2024-03-15 14:30:00 → DATE_TRUNC('month', ...) → 2024-03-01 00:00:00

-- PostgreSQL / Snowflake / Redshift: DATE_TRUNC(precision, timestamp)
SELECT DATE_TRUNC('month', '2024-03-15 14:30:00'::timestamp);
-- 2024-03-01 00:00:00

SELECT DATE_TRUNC('week', '2024-03-15'::date);
-- 2024-03-11 (Monday of that week in Postgres ISO)

SELECT DATE_TRUNC('quarter', '2024-08-20'::date);
-- 2024-07-01

SELECT DATE_TRUNC('year', '2024-08-20'::date);
-- 2024-01-01

-- BigQuery reverses argument order: DATE_TRUNC(date, part)
SELECT DATE_TRUNC('2024-03-15', MONTH);  -- 2024-03-01

-- MySQL has NO DATE_TRUNC. Use DATE_FORMAT to truncate:
SELECT DATE_FORMAT('2024-03-15', '%Y-%m-01');  -- '2024-03-01'

-- The most common pattern: group time series by month
SELECT
  DATE_TRUNC('month', order_date) AS month,
  COUNT(*) AS order_count,
  SUM(revenue) AS total_revenue
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;

Rounds a timestamp down to a unit. The single most-used date function in analytical SQL. Powers every group-by-month and group-by-week query.

EXTRACT

-- EXTRACT pulls ONE numeric component from a date or timestamp.

SELECT EXTRACT(YEAR FROM '2024-03-15'::date);     -- 2024
SELECT EXTRACT(MONTH FROM '2024-03-15'::date);    -- 3
SELECT EXTRACT(DAY FROM '2024-03-15'::date);      -- 15
SELECT EXTRACT(HOUR FROM '2024-03-15 14:30:00'::timestamp);  -- 14
SELECT EXTRACT(DOW FROM '2024-03-15'::date);      -- 5 (Friday, 0=Sun in Postgres)
SELECT EXTRACT(ISODOW FROM '2024-03-15'::date);   -- 5 (Friday, 1=Mon)
SELECT EXTRACT(EPOCH FROM '2024-03-15'::date);    -- 1710460800 (unix)

-- Find peak hours
SELECT
  EXTRACT(HOUR FROM event_time) AS hour_of_day,
  COUNT(*) AS event_count
FROM events
GROUP BY 1
ORDER BY 2 DESC;

-- Day-of-week distribution
SELECT
  EXTRACT(ISODOW FROM order_date) AS day_of_week,
  CASE EXTRACT(ISODOW FROM order_date)
    WHEN 1 THEN 'Monday'  WHEN 2 THEN 'Tuesday' WHEN 3 THEN 'Wednesday'
    WHEN 4 THEN 'Thursday' WHEN 5 THEN 'Friday' WHEN 6 THEN 'Saturday'
    WHEN 7 THEN 'Sunday'
  END AS day_name,
  COUNT(*) AS order_count
FROM orders
GROUP BY 1
ORDER BY 1;

-- CLASSIC TRAP: don't use EXTRACT(MONTH ...) to group time series.
-- EXTRACT returns just the month number (1-12). Jan 2024 and Jan 2025
-- both become 1 and get aggregated together. Use DATE_TRUNC for grouping.
-- EXTRACT is for cross-year analysis ("which month has the most sales").

Pulls one numeric component from a timestamp. Don't use it for time-series grouping — it strips the year. Use it for cross-year patterns like seasonality.

Date functions across SQL dialects

The same operation in four major engines. State your dialect before writing in an interview.

OperationPostgreSQLSnowflake / SQL ServerBigQueryMySQL
Truncate to unitDATE_TRUNC('month', col)DATE_TRUNC('month', col)DATE_TRUNC(col, MONTH)DATE_FORMAT(col, '%Y-%m-01')
Extract componentEXTRACT(YEAR FROM col)EXTRACT(YEAR FROM col)EXTRACT(YEAR FROM col)YEAR(col)
Date subtractioncol1 - col2 (returns days)DATEDIFF('day', col2, col1)DATE_DIFF(col1, col2, DAY)DATEDIFF(col1, col2)
Add intervalcol + INTERVAL '7 days'DATEADD('day', 7, col)DATE_ADD(col, INTERVAL 7 DAY)DATE_ADD(col, INTERVAL 7 DAY)
Current dateCURRENT_DATECURRENT_DATE()CURRENT_DATE()CURDATE()
Convert timezonecol AT TIME ZONE 'America/New_York'CONVERT_TIMEZONE('UTC', 'America/New_York', col)TIMESTAMP(col, 'America/New_York')CONVERT_TZ(col, 'UTC', 'America/New_York')
Day of weekEXTRACT(ISODOW FROM col) -- 1=MonDAYOFWEEK(col) -- 1=SunEXTRACT(DAYOFWEEK FROM col) -- 1=SunDAYOFWEEK(col) -- 1=Sun
ISO weekEXTRACT(WEEK FROM col)WEEK(col, 1)EXTRACT(ISOWEEK FROM col)WEEK(col, 3)

DATEDIFF and date arithmetic

-- Calculating the difference between two dates varies more across
-- databases than any other date function. Memorize the argument order.

-- PostgreSQL: subtract dates directly, returns INTEGER days
SELECT '2024-03-15'::date - '2024-01-01'::date;  -- 74

-- PostgreSQL: subtract timestamps, returns INTERVAL
SELECT '2024-03-15 14:00'::timestamp - '2024-03-15 10:30'::timestamp;
-- '03:30:00'

-- Snowflake / Redshift: DATEDIFF(unit, start, end)
SELECT DATEDIFF('day',   '2024-01-01', '2024-03-15');  -- 74
SELECT DATEDIFF('month', '2024-01-01', '2024-03-15');  -- 2
SELECT DATEDIFF('hour',  '2024-03-15 10:00', '2024-03-15 14:30');  -- 4

-- BigQuery: DATE_DIFF(end, start, unit) — different argument order
SELECT DATE_DIFF('2024-03-15', '2024-01-01', DAY);   -- 74
SELECT DATE_DIFF('2024-03-15', '2024-01-01', MONTH); -- 2

-- MySQL: DATEDIFF(end, start) returns days only
SELECT DATEDIFF('2024-03-15', '2024-01-01');  -- 74
-- For other units: TIMESTAMPDIFF(MONTH, start, end)
SELECT TIMESTAMPDIFF(MONTH, '2024-01-01', '2024-03-15');  -- 2

-- SQL Server: DATEDIFF(unit, start, end) — like Snowflake
SELECT DATEDIFF(day, '2024-01-01', '2024-03-15');  -- 74

Calculating the gap between two dates is where dialects diverge the most. Memorize the argument order for the engines you target.

INTERVAL syntax

-- INTERVAL is the cleanest way to add/subtract time periods.

-- Standard syntax (Postgres, Snowflake, BigQuery, MySQL all accept this)
SELECT CURRENT_DATE + INTERVAL '7 days';        -- 7 days from now
SELECT CURRENT_DATE - INTERVAL '1 month';       -- 1 month ago
SELECT CURRENT_TIMESTAMP + INTERVAL '3 hours';  -- 3 hours from now
SELECT CURRENT_TIMESTAMP - INTERVAL '15 minutes';

-- Filter the last N days
SELECT *
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '30 days';

-- Snowflake also accepts DATEADD for portability
SELECT DATEADD('day', 7, CURRENT_DATE);
SELECT DATEADD('month', -1, CURRENT_DATE);

-- Retention pattern: days from signup to first purchase
SELECT
  u.user_id,
  u.signup_date,
  MIN(o.order_date) AS first_order_date,
  MIN(o.order_date) - u.signup_date AS days_to_first_order
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.signup_date;

Adding or subtracting time periods. The 'INTERVAL N days' syntax is the most portable form across all major engines.

CURRENT_DATE and CURRENT_TIMESTAMP

-- Current date/time at query execution time.
-- No parentheses required in standard SQL.

SELECT CURRENT_DATE;       -- 2024-03-15
SELECT CURRENT_TIMESTAMP;  -- 2024-03-15 14:30:00+00
SELECT NOW();              -- Postgres alias for CURRENT_TIMESTAMP

-- Engine-specific equivalents
-- Snowflake: CURRENT_DATE() / CURRENT_TIMESTAMP()
-- SQL Server: GETDATE() / SYSDATETIME()
-- BigQuery: CURRENT_DATE() / CURRENT_TIMESTAMP()
-- MySQL: CURDATE() / NOW()

-- Common patterns

-- Today's orders
SELECT * FROM orders WHERE order_date = CURRENT_DATE;

-- Last 24 hours
SELECT *
FROM orders
WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours';

-- Account age in days
SELECT
  user_id,
  signup_date,
  CURRENT_DATE - signup_date AS account_age_days
FROM users;

-- ETL load stamp
INSERT INTO load_log (table_name, loaded_at, row_count)
VALUES ('orders', CURRENT_TIMESTAMP, 1523);

-- WARNING: CURRENT_DATE is evaluated per-query, not per-row.
-- If you split a long ETL across queries, each gets a different
-- CURRENT_DATE if it spans midnight. Capture once at the top:
WITH run_meta AS (SELECT CURRENT_TIMESTAMP AS run_ts)
INSERT INTO log SELECT *, (SELECT run_ts FROM run_meta) FROM staging;

Returns the current date or timestamp at query execution time. The midnight-edge case (CURRENT_DATE changes mid-pipeline) is a real production gotcha worth capturing into a CTE.

Four timezone rules for production pipelines

The rules that prevent the DST and 'wrong-day' incidents that haunt analytics teams. Each rule is independent — apply whichever your code touches.

Rule 1

Store everything in UTC

All timestamps in the warehouse must be UTC. Convert to local time only at the query layer or in the application. Storing in local time produces daylight-saving-time bugs that are catastrophic to debug because they only manifest on specific calendar weekends.

TIMESTAMPTZ in Postgres, TIMESTAMP_TZ in Snowflake
Rule 2

Use named timezones, never offsets

'America/New_York' handles DST automatically. 'EST' or '-05:00' do NOT — they're fixed. New York is UTC-5 in winter, UTC-4 in summer. If you write code with 'EST', that code is wrong for half the year. Always use IANA timezone names.

IANA tzdata is the source of truth
Rule 3

Convert BEFORE truncating

If your business operates in US Eastern and you need daily metrics, convert UTC → Eastern before applying DATE_TRUNC. Otherwise an order at 11pm Eastern (4am UTC next day) lands in the wrong day. This is the most common timezone bug in analytics pipelines.

AT TIME ZONE then DATE_TRUNC, in that order
Rule 4

Beware DST transition days

Two days a year, a clock day in a DST-observing zone has 23 or 25 hours. A 'last 24 hours' query around DST transitions has ambiguous semantics. For business-critical metrics, prefer fixed-window queries anchored at midnight UTC, then convert for display. Avoid INTERVAL '24 hours' near DST boundaries if hour precision matters.

March and November are when you get paged

Timezone conversion

-- Timezones are one of the hardest problems in data engineering.
-- The rules below are battle-tested across many production pipelines.

-- PostgreSQL: AT TIME ZONE converts between zones
-- A bare timestamp interpreted as the specified zone -> converts to UTC
SELECT '2024-03-15 14:30:00'::timestamp AT TIME ZONE 'America/New_York';
-- 2024-03-15 18:30:00+00

-- A timestamptz displayed in the specified zone
SELECT '2024-03-15 18:30:00+00'::timestamptz AT TIME ZONE 'America/New_York';
-- 2024-03-15 14:30:00

-- Snowflake: CONVERT_TIMEZONE(from, to, timestamp)
SELECT CONVERT_TIMEZONE('UTC', 'America/New_York', created_at);

-- BigQuery: TIMESTAMP function with zone argument
SELECT TIMESTAMP(created_at, 'America/New_York');

-- MOST COMMON PATTERN: group by LOCAL day, not UTC day
SELECT
  DATE_TRUNC('day', created_at AT TIME ZONE 'America/New_York') AS local_day,
  COUNT(*) AS order_count
FROM orders
GROUP BY 1
ORDER BY 1;
-- An order at 11pm Eastern = 4am UTC the next day. Without timezone
-- conversion, this order lands in the wrong day. Real example: a US
-- retailer's "daily sales" report was off by ~2% until someone added
-- AT TIME ZONE here. This is a real-world incident DataDriven seeded
-- one of its debrief problems from.

The most common timezone bug: grouping by UTC day when the business reports in local time. AT TIME ZONE must come before DATE_TRUNC.

Three patterns from production pipelines

-- Three date patterns that appear in production dbt models and ETL.

-- Pattern 1: Cohort retention by signup month
SELECT
  DATE_TRUNC('month', u.signup_date) AS cohort_month,
  EXTRACT(MONTH FROM AGE(o.order_date, u.signup_date)) AS months_since_signup,
  COUNT(DISTINCT o.user_id) AS active_users
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY 1, 2
ORDER BY 1, 2;

-- Pattern 2: 7-day rolling average revenue
SELECT
  date,
  revenue,
  AVG(revenue) OVER (
    ORDER BY date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS rolling_7d_avg
FROM daily_revenue
ORDER BY date;

-- Pattern 3: Data freshness monitoring
SELECT
  table_name,
  MAX(loaded_at) AS last_load,
  EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - MAX(loaded_at))) / 3600
    AS hours_since_last_load,
  CASE
    WHEN MAX(loaded_at) < CURRENT_TIMESTAMP - INTERVAL '2 hours'
      THEN 'STALE'
    ELSE 'FRESH'
  END AS freshness_status
FROM load_log
GROUP BY table_name
ORDER BY hours_since_last_load DESC;

Cohort retention, rolling averages, and freshness monitoring. These three patterns cover most date-heavy analytical workloads.

Three interview question patterns

When you get a date question, identify the underlying pattern first. These three cover most of what data engineering interviews ask about dates.

Date spine + LEFT JOIN

Q1: Monthly revenue, gap-filled

'Show monthly revenue for the last 12 months, including months with zero revenue.' Tests DATE_TRUNC for monthly grouping, date spine generation (generate_series or recursive CTE), and COALESCE for zero-filling. Approach: build a 12-row date spine, LEFT JOIN aggregated orders, COALESCE revenue to 0.

Date spines are the gap-fill trick
Date arithmetic + ratios

Q2: 7-day retention rate

'What percent of January 2024 signups made a purchase within 7 days?' Tests date arithmetic (signup_date + 7), range filtering, and ratio calculation with conditional counts. Approach: filter signups to January, LEFT JOIN activity where activity_date BETWEEN signup_date AND signup_date + 7, divide distinct active users by total signups.

Always state the denominator clearly
LAG + date subtraction

Q3: Average time between orders per customer

Tests LAG window function combined with date subtraction. Approach: LAG(order_date) OVER (PARTITION BY customer_id ORDER BY order_date) gives the previous order date. Subtract to get days between. AVG the per-customer differences. Edge case: customers with one order have NULL interval — exclude or handle explicitly.

The LAG + window pattern handles many time-series questions

SQL date functions FAQ

What are the most important SQL date functions for data engineers?+
DATE_TRUNC and EXTRACT are the two most-used in data engineering. DATE_TRUNC rounds a timestamp down to a precision (day, week, month, quarter, year) and powers time-series grouping. EXTRACT pulls a specific component (year, month, day, hour). After those: DATEDIFF (or subtraction), CURRENT_DATE/CURRENT_TIMESTAMP, and INTERVAL. These five cover 95% of date manipulation in analytical SQL.
How do date functions differ across SQL databases?+
The concepts are the same; the syntax diverges. DATE_TRUNC works in Postgres, Snowflake, BigQuery, and Redshift but MySQL has no DATE_TRUNC. DATEDIFF takes different argument orders (Postgres uses subtraction; Snowflake and SQL Server use DATEDIFF(unit, start, end); BigQuery uses DATE_DIFF(end, start, unit); MySQL DATEDIFF returns days only). EXTRACT is mostly consistent but field names like DOW vs DAYOFWEEK vs ISODOW vary.
How should I handle timezones in SQL?+
Store timestamps in UTC. Always. Convert to local at query time using AT TIME ZONE (Postgres, Snowflake) or CONVERT_TIMEZONE (Snowflake) or TIMESTAMP(timestamp, timezone) (BigQuery). This avoids DST ambiguity. When grouping by day, specify whose day: DATE_TRUNC('day', created_at AT TIME ZONE 'America/New_York') groups by US Eastern days, not UTC days. Mentioning timezone handling unprompted in an interview signals real pipeline experience.
Why does my time-series GROUP BY miss months with no data?+
GROUP BY only produces rows for groups that exist in the data. If no orders happened in February, February gets no row. To include zero-data periods, generate a date spine (a list of every expected period) and LEFT JOIN your aggregated data onto it. COALESCE the metric to 0 for missing periods. This pattern is essential for dashboard queries that must show every period regardless of activity.
What's the difference between DATE_TRUNC and EXTRACT for grouping?+
DATE_TRUNC preserves the year. EXTRACT does not. DATE_TRUNC('month', '2024-03-15') returns 2024-03-01. EXTRACT(MONTH FROM '2024-03-15') returns just 3. If you GROUP BY EXTRACT(MONTH FROM date), January 2024 and January 2025 collapse into the same group. Use DATE_TRUNC for time-series grouping. Use EXTRACT for cross-year analysis like 'which month of the year has the most sales.'
02 / Why practice

Time-series queries, one at a time

  1. 01

    Active recall beats re-reading by 50%

    Cognitive-science meta-reviews (Dunlosky et al., 2013) rank practice testing as a top-tier study technique, while re-reading and highlighting rank near the bottom

  2. 02

    76% of hiring managers reject on the coding task, not the resume

    From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice

  3. 03

    Five problem shapes cover 80% of data engineer loops

    Dedup, sessionization, top-N-per-group, slowly-changing dimensions, partition tricks. Writing the shapes by hand turns the unfamiliar into pattern recognition

Related SQL prep