In PostgreSQL, converting a timestamp (with or without time zone) to a date removes its time-of-day component and returns just the calendar date, most often done with the ::date cast or the DATE() function.
Converting a timestamp to a date in PostgreSQL is as simple as a cast, but doing it safely across time zones, performance-friendly in large datasets, and readable for future maintainers takes some nuance.
This glossary entry shows you why and how to strip the time portion from a timestamp, explores multiple techniques, explains hidden edge cases like daylight-saving shifts, and provides production-grade patterns you can paste directly into Galaxy’s SQL editor.
Most analytical questions revolve around calendar days—daily active users, orders per day, or error counts by deployment date. Yet event logs and transactional tables typically record timestamps, precise to the millisecond (or even microsecond). Dropping the time component ensures that identical calendar days group together, which simplifies joins, aggregations, and partition pruning.
::date
)The quickest approach is a direct cast:
SELECT created_at::date AS order_date
FROM orders;
This is syntactic sugar for the SQL standard CAST(created_at AS date)
. PostgreSQL simply truncates the time portion in the session’s current time zone.
DATE()
FunctionPostgreSQL provides a date()
wrapper, functionally identical to the cast:
SELECT date(created_at) AS order_date
FROM orders;
Some teams prefer the function form because it looks explicit in report definitions.
If your created_at
column stores TIMESTAMP WITH TIME ZONE
(alias timestamptz
) and your analysis must be done in a specific zone, apply AT TIME ZONE
before casting:
SELECT (created_at AT TIME ZONE 'America/Los_Angeles')::date AS order_date_pst
FROM orders;
Why? The implicit cast converts in the session’s zone, which may vary by user or by application pool. Being explicit eliminates surprises, especially around daylight-saving transitions.
date_trunc()
for Readabilitydate_trunc('day', ts)
returns the timestamp at midnight. You still need ::date
to get a date
type:
SELECT date_trunc('day', created_at)::date AS order_date
FROM orders;
This pattern is handy when you need both the date and the truncated midnight timestamp in the same query.
Casting a timestamp column in a WHERE
clause forces PostgreSQL to compute the cast row by row, bypassing column statistics and indexes. The classic anti-pattern looks like:
-- ☹️ Slow! Cannot use an index on created_at
SELECT *
FROM orders
WHERE created_at::date = CURRENT_DATE - INTERVAL '1 day';
Instead, cast the parameter or use a half-open date range:
-- 😊 Index-friendly
SELECT *
FROM orders
WHERE created_at >= (CURRENT_DATE - INTERVAL '1 day')::timestamp
AND created_at < CURRENT_DATE::timestamp;
With this pattern, PostgreSQL can leverage a B-tree index on created_at
and prune partitions effectively.
Message queues and telemetry systems often store epoch seconds as integers. Convert them to date in two steps:
SELECT to_timestamp(epoch_seconds)::date AS metric_date
FROM events;
Skipping to_timestamp
and casting the integer directly to date
yields nonsense dates around 1970-01-01.
Suppose you want to count how many users returned one day after signup. You need both signup date and first-return date, both derived from a timestamptz
field in your events
table.
WITH signup AS (
SELECT user_id,
MIN(event_time) AS first_seen,
MIN(event_time)::date AS signup_date
FROM events
WHERE event_name = 'signup'
GROUP BY user_id
),
next_visit AS (
SELECT user_id,
MIN(event_time) AS second_seen
FROM events
WHERE event_name = 'page_view'
GROUP BY user_id
)
SELECT signup_date,
COUNT(*) FILTER (
WHERE (second_seen::date - signup_date) = 1) AS retained_next_day,
COUNT(*) AS total_signed_up
FROM signup
LEFT JOIN next_visit USING (user_id)
GROUP BY signup_date
ORDER BY signup_date;
The query uses explicit ::date
casts to align sign-up and follow-up events on calendar days.
Galaxy’s AI copilot autocompletes ::date
, suggests index-friendly filtering patterns, and warns when a conversion might miss a time-zone nuance. In a shared Collection, your team can endorse the above cohort query so that others reuse the proven date-handling logic without hunting through Slack threads.
AT TIME ZONE
when user context matters.>=
start, <
next day) for roll-up queries.Mistake: Ignoring the session zone when casting timestamptz
to date
.
Fix: Pin the zone with AT TIME ZONE
or set SET TIME ZONE 'UTC';
in the session.
Mistake: Wrapping the column in ::date
in filters, killing index usage.
Fix: Filter on the raw timestamp with a range boundary.
date_trunc
Mistake: Assuming date_trunc('day', ts)
returns a date
; it returns timestamp
.
Fix: Append ::date
or use DATE(ts)
directly.
Daily reports, cohort analyses, and partition pruning all rely on consistent date values. Mishandling the conversion can introduce silent off-by-one-day errors, break index usage, or inflate query costs. Data engineers must master this basic yet deceptively tricky task to ensure accurate analytics pipelines and dashboard metrics.
Yes. ::date
is PostgreSQL’s shorthand for the SQL-standard CAST(… AS date)
. Both call the same internal cast function.
Absolutely. PostgreSQL truncates the time portion directly. The key risk is with TIMESTAMPTZ
, where the session’s time-zone setting affects the result.
Yes. Galaxy’s AI copilot autocompletes casts, flags anti-patterns like casting in WHERE
clauses, and lets you save best-practice snippets inside shared Collections.
When you cast a timestamptz
during the hour skipped or repeated by DST, PostgreSQL first converts the instant to the session (or explicit) zone and then strips the time. Being explicit with AT TIME ZONE
guarantees you get the expected calendar day.