On this page
Syntax
DATE_TRUNC('unit', timestamp)DATETRUNC(unit, timestamp)DATE_TRUNC('unit', timestamp, timezone) | Parameter | Type | Required | Description |
|---|---|---|---|
unit |
text or keyword | yes | The field to truncate down to: year, quarter, month, week, day, hour, minute or second. In PostgreSQL it is a quoted string; in SQL Server DATETRUNC it is a keyword. |
timestamp |
timestamp or date | yes | The value to floor to the start of the given unit. Everything smaller than the unit is set to its lowest value. |
timezone |
text | no | PostgreSQL 16+ only: truncate a timestamptz relative to the named time zone rather than the session zone. |
How it works
DATE_TRUNC('month', ts) takes a timestamp and throws away everything smaller than the unit you name, returning the very start of that period. Truncating 2026-07-04 13:47:12 to month gives 2026-07-01 00:00:00; truncating it to day gives 2026-07-04 00:00:00. It is a floor operation on time, which is exactly what you want when grouping a time series into equal buckets.
This is the point most people miss: truncation is not the same as pulling out a component. EXTRACT() returns a single number such as the month (7), throwing away the year, so rows from July 2025 and July 2026 collapse together. DATE_TRUNC() keeps a full timestamp, so those two Julys stay in separate buckets. Use EXTRACT for a part, DATE_TRUNC for a bucket.
Support is uneven across engines. PostgreSQL has had DATE_TRUNC(unit, ts) for a long time. SQL Server 2022 added DATETRUNC(unit, ts) with keyword units, and earlier versions must emulate it with DATEADD and DATEDIFF. MySQL has no DATE_TRUNC at all, so you emulate it with DATE_FORMAT() or DATE(). See the window functions guide for pairing truncated buckets with running totals.
Examples
Truncate a timestamp to the start of the month (PostgreSQL)
SELECT DATE_TRUNC('month', TIMESTAMP '2026-07-04 13:47:12') AS month_start;
month_start ------------------- 2026-07-01 00:00:00
Start of the day and start of the hour
SELECT
DATE_TRUNC('day', ts) AS day_start,
DATE_TRUNC('hour', ts) AS hour_start
FROM events
WHERE id = 42;
day_start | hour_start --------------------+-------------------- 2026-07-04 00:00:00 | 2026-07-04 13:00:00
Group revenue into monthly buckets (PostgreSQL)
SELECT DATE_TRUNC('month', created_at) AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;
month | revenue --------------------+--------- 2026-05-01 00:00:00 | 84200 2026-06-01 00:00:00 | 91050 2026-07-01 00:00:00 | 77300
Same monthly bucket in each engine
-- PostgreSQL
SELECT DATE_TRUNC('month', created_at) AS month FROM orders;
-- SQL Server 2022+
SELECT DATETRUNC(month, created_at) AS month FROM orders;
-- SQL Server before 2022 (emulate start of month)
SELECT DATEADD(month, DATEDIFF(month, 0, created_at), 0) AS month FROM orders;
-- MySQL (no DATE_TRUNC; emulate with DATE_FORMAT)
SELECT DATE_FORMAT(created_at, '%Y-%m-01') AS month FROM orders;
month ------------------- 2026-07-01 00:00:00
Common mistakes
-- Wanted July 2026 as its own bucket, but EXTRACT
-- returns just the number 7, merging every July
SELECT EXTRACT(MONTH FROM created_at) AS m, SUM(amount)
FROM orders
GROUP BY EXTRACT(MONTH FROM created_at);
Right
-- DATE_TRUNC keeps the year, so each month is distinct
SELECT DATE_TRUNC('month', created_at) AS month, SUM(amount)
FROM orders
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;
Truncation floors to a full timestamp; EXTRACT() pulls one component and discards the rest. Grouping by EXTRACT(MONTH ...) collapses July 2025 and July 2026 into one row.
-- MySQL has no DATE_TRUNC function
SELECT DATE_TRUNC('month', created_at) FROM orders;
Right
-- Emulate: format to the first of the month,
-- or use DATE() for the start of the day
SELECT DATE_FORMAT(created_at, '%Y-%m-01') AS month,
DATE(created_at) AS day
FROM orders;
MySQL does not ship DATE_TRUNC. Use DATE_FORMAT() for month or year buckets and DATE() for the start of the day.
-- Assuming the week always starts on Monday
SELECT DATE_TRUNC('week', created_at) FROM orders;
Right
-- Know your engine: PostgreSQL weeks start Monday (ISO).
-- If you need Sunday, shift before and after truncating.
SELECT DATE_TRUNC('week', created_at + INTERVAL '1 day')
- INTERVAL '1 day' AS week_start_sunday
FROM orders;
Week boundaries differ by engine and locale. PostgreSQL DATE_TRUNC('week', ...) always starts on Monday, so adjust explicitly when the business week starts on Sunday.
Performance
Wrapping the column in DATE_TRUNC() inside a WHERE clause makes the predicate non-sargable: the database must compute the function for every row and cannot use a plain index on the timestamp. To keep an index range scan, compare the raw column to a range instead, for example created_at >= '2026-07-01' AND created_at < '2026-08-01'.
When you truncate only in the SELECT and GROUP BY, the cost is small and paid once per row. For heavy dashboards, consider an expression index in PostgreSQL, such as CREATE INDEX ON orders (DATE_TRUNC('month', created_at)), or a generated column that stores the bucket so the value is precomputed.
Interview questions
What does DATE_TRUNC() do?
It floors a timestamp to the start of a unit you name, such as year, month, week, day or hour. Everything smaller than the unit is reset to its lowest value, so DATE_TRUNC('month', ts) returns the first instant of that month.
What is the difference between DATE_TRUNC() and EXTRACT()?
DATE_TRUNC returns a full timestamp floored to a unit, keeping everything larger than that unit. EXTRACT() returns a single numeric component and discards the rest. Group by DATE_TRUNC for time buckets, use EXTRACT when you only need the part itself.
SELECT DATE_TRUNC('month', created_at) AS bucket,
EXTRACT(MONTH FROM created_at) AS month_number
FROM orders;
How do you truncate a timestamp to the month in MySQL, which has no DATE_TRUNC?
Emulate it. Use DATE_FORMAT(created_at, '%Y-%m-01') for the first of the month, or DATE(created_at) for the start of the day. MySQL simply does not provide DATE_TRUNC.
SELECT DATE_FORMAT(created_at, '%Y-%m-01') AS month
FROM orders;
How would you get the start of the month in SQL Server before version 2022?
Before SQL Server 2022 added DATETRUNC, you emulate it with DATEADD and DATEDIFF measured from the zero date: DATEADD(month, DATEDIFF(month, 0, created_at), 0). DATEDIFF counts whole months from day zero and DATEADD rebuilds that instant.
SELECT DATEADD(month, DATEDIFF(month, 0, created_at), 0) AS month_start
FROM orders;
Why can DATE_TRUNC() in a WHERE clause hurt performance?
Applying DATE_TRUNC to the column makes the filter non-sargable, so the index on that column cannot be used for a range scan and the engine evaluates the function per row. Compare the raw column to a half open range instead, or add an expression index on the truncated value.