Home Functions DATE_TRUNC()
SQL Date & Time Function

DATE_TRUNC()

DATE_TRUNC() floors a timestamp down to the start of a chosen unit such as month or hour, which makes it the standard tool for building time buckets in reports.

PostgreSQLSQL ServerMySQL
Returns: A timestamp (or date) floored to the start of the requested unit. Returns NULL when the input timestamp is NULL.

Syntax

DATE_TRUNC('unit', timestamp)DATETRUNC(unit, timestamp)DATE_TRUNC('unit', timestamp, timezone)
ParameterTypeRequiredDescription
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;
Result
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;
Result
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;
Result
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;
Result
month
-------------------
2026-07-01 00:00:00

Common mistakes

Wrong
-- 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.

Wrong
-- 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.

Wrong
-- 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.

Master SQL, one function at a time

Browse the full SQL functions library, or learn the fundamentals with our free, structured courses.