Repository navigation
Support date_bin on timestamps with timezone, properly accounting for Daylight Savings Time #10602
Description
Is your feature request related to a problem or challenge?
Summary
Given this data in UTC timestamps
> select * from t_utc;
+----------------------+
| column1 |
+----------------------+
| 2024-04-30T21:30:00Z |
| 2024-04-30T22:30:00Z |
| 2024-04-30T23:30:00Z |
| 2024-05-01T00:00:00Z |
| 2024-05-01T00:30:00Z |
| 2024-05-01T10:30:00Z |
| 2024-05-01T20:30:00Z |
+----------------------+
7 row(s) fetched.
Elapsed 0.002 seconds.How to create the data
-- Dates right around the first of May
create or replace table t AS
VALUES
('2024-04-30T21:30:00'),
('2024-04-30T22:30:00'),
('2024-04-30T23:30:00'),
('2024-05-01T00:00:00'),
('2024-05-01T00:30:00'),
('2024-05-01T10:30:00'),
('2024-05-01T20:30:00')
;
create or replace view t_utc as
select column1::timestamp AT TIME ZONE 'UTC' as "column1"
from t;I would like to be able to get this output (bin the data by their day in Brussels, properly accounting for daylight savings time).
+---------------------------+---------------------+
| time in Brussels | date_bin |
+---------------------------+---------------------+
| 2024-04-30T23:30:00+02:00 | 2024-04-30T00:00:00 | <-- in 2024-04-01
| 2024-05-01T00:30:00+02:00 | 2024-05-01T00:00:00 | <-- note this is in 2024-05-01
| 2024-05-01T01:30:00+02:00 | 2024-05-01T00:00:00 |
| 2024-05-01T02:00:00+02:00 | 2024-05-01T00:00:00 |
| 2024-05-01T02:30:00+02:00 | 2024-05-01T00:00:00 |
| 2024-05-01T12:30:00+02:00 | 2024-05-01T00:00:00 |
| 2024-05-01T22:30:00+02:00 | 2024-05-01T00:00:00 |
+---------------------------+---------------------+
7 row(s) fetched.
Elapsed 0.004 seconds.We can't figure out how to do this at the moment
Details
Broken out from @Abdullahsab3's great ticket #10368
We would like to apply date_bin and have it bin the dates in local time, properly including daylight savings time calculations
For example, given the following data at 12:01AM in the morning on the first day of each month,I would like to bin it into the first day of each month.
create or replace table t AS
VALUES
('2024-01-01T00:00:01'),
('2024-02-01T00:00:01'),
('2024-03-01T00:00:01'),
('2024-04-01T00:00:01'),
('2024-05-01T00:00:01'),
('2024-06-01T00:00:01'),
('2024-07-01T00:00:01'),
('2024-08-01T00:00:01'),
('2024-09-01T00:00:01'),
('2024-10-01T00:00:01'),
('2024-11-01T00:00:01'),
('2024-12-01T00:00:01')
;This works as expected when there is no timestamp (all timestamps end up in the correct day)
> select arrow_typeof(column1::timestamp) as arrow_type, column1::timestamp, date_bin(interval '1 day', column1::timestamp) as "date_bin" from t;
+-----------------------------+---------------------+---------------------+
| arrow_type | t.column1 | date_bin |
+-----------------------------+---------------------+---------------------+
| Timestamp(Nanosecond, None) | 2024-01-01T23:59:00 | 2024-01-01T00:00:00 | <-- binned correctly
| Timestamp(Nanosecond, None) | 2024-02-01T23:59:00 | 2024-02-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-03-01T23:59:00 | 2024-03-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-04-01T23:59:00 | 2024-04-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-05-01T23:59:00 | 2024-05-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-06-01T23:59:00 | 2024-06-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-07-01T23:59:00 | 2024-07-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-08-01T23:59:00 | 2024-08-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-09-01T23:59:00 | 2024-09-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-10-01T23:59:00 | 2024-10-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-11-01T23:59:00 | 2024-11-01T00:00:00 |
| Timestamp(Nanosecond, None) | 2024-12-01T23:59:00 | 2024-12-01T00:00:00 |
+-----------------------------+---------------------+---------------------+
12 row(s) fetched.
Elapsed 0.004 seconds.However, once the timestamp is in a timezone it has problems:
> create or replace view t_timezone
as
select column1::timestamp AT TIME ZONE 'Europe/Brussels' as "column1"
from t;
0 row(s) fetched.
Elapsed 0.001 seconds.The timestamps now binned at incorrectly (the are binned based on UTC midnight, not midnight in Brussels) so for example a time in 2024-01-01 goes into a 2023-12-31 bucket:
> select
arrow_typeof(column1) as arrow_type,
column1,
date_bin(interval '1 day', column1) as "date_bin"
from t_timezone;
+------------------------------------------------+---------------------------+---------------------------+
| arrow_type | column1 | date_bin |
+------------------------------------------------+---------------------------+---------------------------+
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-01-01T00:00:01+01:00 | 2023-12-31T01:00:00+01:00 | <-- binned in previous day
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-02-01T00:00:01+01:00 | 2024-01-31T01:00:00+01:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-03-01T00:00:01+01:00 | 2024-02-29T01:00:00+01:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-04-01T00:00:01+02:00 | 2024-03-31T01:00:00+01:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-05-01T00:00:01+02:00 | 2024-04-30T02:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-06-01T00:00:01+02:00 | 2024-05-31T02:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-07-01T00:00:01+02:00 | 2024-06-30T02:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-08-01T00:00:01+02:00 | 2024-07-31T02:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-09-01T00:00:01+02:00 | 2024-08-31T02:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-10-01T00:00:01+02:00 | 2024-09-30T02:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-11-01T00:00:01+01:00 | 2024-10-31T01:00:00+01:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-12-01T00:00:01+01:00 | 2024-11-30T01:00:00+01:00 |
+------------------------------------------------+---------------------------+---------------------------+The third argument to date_bin allows changing the starting offset for a day, so let's set it to midnight in Europe/Brussels time. This almost works but does not take into account daylight savings time so the timestamps are binned incorrectly during DST:
> select
arrow_typeof(column1) as arrow_type,
column1,
date_bin(interval '1 day', column1, '2020-01-01T00:00:00Z'::timestamp AT TIME ZONE 'Europe/Brussels') as "date_bin"
from t_timezone;
+------------------------------------------------+---------------------------+---------------------------+
| arrow_type | column1 | date_bin |
+------------------------------------------------+---------------------------+---------------------------+
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-01-01T00:00:01+01:00 | 2024-01-01T00:00:00+01:00 | <-- correct!
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-02-01T00:00:01+01:00 | 2024-02-01T00:00:00+01:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-03-01T00:00:01+01:00 | 2024-03-01T00:00:00+01:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-04-01T00:00:01+02:00 | 2024-03-31T00:00:00+01:00 | <-- DST! Binned into previous day
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-05-01T00:00:01+02:00 | 2024-04-30T01:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-06-01T00:00:01+02:00 | 2024-05-31T01:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-07-01T00:00:01+02:00 | 2024-06-30T01:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-08-01T00:00:01+02:00 | 2024-07-31T01:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-09-01T00:00:01+02:00 | 2024-08-31T01:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-10-01T00:00:01+02:00 | 2024-09-30T01:00:00+02:00 |
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-11-01T00:00:01+01:00 | 2024-11-01T00:00:00+01:00 | <-- DST over, now correct again
| Timestamp(Nanosecond, Some("Europe/Brussels")) | 2024-12-01T00:00:01+01:00 | 2024-12-01T00:00:00+01:00 |
+------------------------------------------------+---------------------------+---------------------------+
12 row(s) fetched.
Elapsed 0.003 seconds.Describe the solution you'd like
Describe alternatives you've considered
No response
Additional context
No response
Activity
alamb commentedon May 21, 2024
The way you can perform this binning in postgres is somewhat paradoxically to convert a timestamp with a timezone back to a timestamp without timezone and then apply
date_bin.The syntax to convert a timestamp to something without timezone is 🤯 : you apply
AT TIME ZONEto a timestamp with a timezone and then it has no timezone) as @mhilton notes in #10368 (comment)alamb commentedon May 21, 2024
If we cast using arrow_cast back to
Timestamp(Nanosecond, None)the binning does appear to work correctlyalamb commentedon May 21, 2024
Given the statement in the description, here is the best I can come up with using
arrow_castalamb commentedon May 21, 2024
@mhilton and I agree that if we had the functionality suggested by @Abdullahsab3's on #10368 (comment)
We think we could get the right value out of
date_bin.For example, If we had a function like
remove_timezonethat behaved likeNote that this is different than
arrow_cast("time in Brussels", 'Timestamp(Nanosecond, None)')becausearrow_castwill adjust the timestamp valuesalamb commentedon May 21, 2024
My suggested next steps for this ticket:
date_binstrip_timezonefunction, and/or update thearrow_castkernel, and/or add syntax likeWITHOUT TIME ZONE)appletreeisyellow commentedon May 21, 2024
I'd like to work on this issue 🙋♀️
tustvold commentedon May 22, 2024
The challenge with an approach that relies on stripping the timezone, is you end up back with the ambiguous timestamp issue. The UX of relying on users to strip the timezone is also rather unfortunate. I'm curious as to why we wouldn't want to just make date_bin timezone aware? This would appear to have a lot less potential edge cases
alamb commentedon May 22, 2024
I believe @mhilton had thought about this and maybe has some thoughts
mhilton commentedon May 23, 2024
I'm not sure that it's obvious how a time-zone aware date_bin would behave. For example On the day in october when many time zones repeat an hour would the two hours count into a single bin? What about if you had hour long bins that start 30 minutes into the hour? Given that the two hours are easily distinguishable the answer is probably not, and if you really want that behviour then stripping time zone information would be a reasonable way to get it.
If we restricted timezone awareness to intervals of unit days then that would be much more reasonable. We could make it so that date_bin understands the number of hours in any given day in a time zone and bins a time accordingly. Anyone needing a strict 24 hour binning could specify the interval in hour units.
tustvold commentedon May 23, 2024
This would be the intuitive thing for it to do IMO, if people don't want this, they should cast to a timezone that doesn't have DST
appletreeisyellow commentedon May 23, 2024
To make
date_bintimezone aware, there are some edge cases we need to consider when design it:date_binwith 10 min interval, how to handle the returned data? Aggregate them? Return two sets of data?'2021-03-28T02:30:00' AT TIME ZONE 'Europe/Brussels'does not existappletreeisyellow commentedon May 23, 2024
I want to highlight this open-ended question and hear how others think
Abdullahsab3 commentedon May 25, 2024
Thanks for filing the ticket and for all the detailed explanations! very enriching
I wonder whether the Postgres behavior is actually that bad. Though it looks weird, it still is generic enough to make it widely applicable. The issue with making
date_binspecifically timezone-aware in my opinion is the fact that future or similar grouping functions (or rolling windows functions) will also have to be implemented in a timezone-aware way, accounting for similar pitfalls and edge cases. The same problem will also occur in other functionalities; for example if you would like to return local time. I personally think that the problem might be better tackled as an additional time functionality, which may be the same way that Postgres does it.The postgres way of converting UTC to local time of a given timezone is:
As already mentioned by Andrew, the
AT TIME ZONEoperator in postgres converts a timezone-aware timestamp to local time (with no offset or timezone information), and local time to a timezone-aware timestamp. Though the overloaded functionalities of theAT TIME ZONEoperator in Postgres are weird, they are definitely usable in my opinion. I also like the idea of having a separateto_local_time/strip_timezonefunction, though this would break Datafusions compatibility with Postgres.4 remaining items
Abdullahsab3 commentedon May 30, 2024
Very nice! Thanks for verifying @appletreeisyellow. I was able to do some minimal validations today on our systems and it seems indeed to be binning correctly. Happy to see this as an alternative to what I was doing as a fix for this issue
My main concerns with the use of
to_charare:to_charwill parse the given timestamp in the provided format, and return a string, which will be coerced to a (localtime) timestamp (which I assume includes another parsing step).%F %Xwill always produce the correct localtime. My concern is mostly the %F partalamb commentedon May 31, 2024
@appletreeisyellow and I just had a chat about what to do about this ticket. It is great that we seem to have found a workaround there are at least two potential issues we see with the workaround:
date_binon timestamps with timezone, properly accounting for Daylight Savings Time #10602 (comment)We think it would be great to improve DataFusion so future users (both of InfluxDB and other systems) who use
date_binhave fast queries and are not confused.Ideal Behavior
We think the ideal user experience would be as @tustvold suggests for
date_binto be timezone aware and just "do the right thing and bin dates respecting the timezone of the timestamp".While this might be different than what postgres does, the underlying treatment if timestamps is already different, so I don't think we need to be quirk for quirk compatible.
Proposal
Thus we propose @appletreeisyellow will try and complete her prototype work to make
date_bindo the intuitive thing with timestamps with timezonesIt is likely we can't devote a large amount of time to this project, so if the implementation is taking too long we may have to abandon this effort or go with a less optimal route (e.g. a function to strip off timezones like Postgres)
alamb commentedon May 31, 2024
I filed apache/arrow-rs#5826 to document more precisely what is going on with casting
appletreeisyellow commentedon Jun 4, 2024
I wrote up a high-level doc on how to implement the
date_binwith timezone awareness. If you cannot access the document, feel free to request access and I'll give you the permission to view/comment.Abdullahsab3 commentedon Jun 5, 2024
Another thing that needs to be taken into consideration: I think the returned timestamp from
date_binneeds to be in the timezone that was provided as an argument (in the internal Arrow represenation). For example:The returned timestamp is in correct local time, but the underlying Arrow kernel has no timezone information about it (which I think means that the timestamp will be interpreted as being in UTC)
When we add the timezone information:
The timezone information there is important, as the timestamp will be otherwise interpreted by external integrations (such as Grafana) as being in UTC.
Note that applying
at time zone 'Europe/Brussels'on the results ofto_charwill never fail during daylight savings hours, as the returned value fromto_charis always in localtime, meaning that the 'missing/additional hour' should not be an issue for theat time zoneoperator.TLDR; if you're using the
to_charworkaround now for localisation, you should add the timezone information to the resultsappletreeisyellow commentedon Jun 6, 2024
Thanks for highlighting it. Yes, this is the expected behavior. The timezone representation of the returned timestamp will be the same as the input timestamp. For example:
None:UTC, then the date_bin returns timezone asSome('UTC')Europe/Brussels, then the date_bin returns timezone asSome('Europe/Brussels')findepi commentedon Jul 3, 2024
I probably missed some prior discussion. Do we want
date_binsyntax?Can we support more standard
date_trunc('day', timestamp_tz)?alamb commentedon Jul 5, 2024
I think we already have date_trunc
docs: https://datafusion.apache.org/user-guide/sql/scalar_functions.html#date-trunc
code:
datafusion/datafusion/functions/src/datetime/date_trunc.rs
Line 361 in dce77db
I think the difference between
date_truncanddate_binis that date_bin can bin to dates that are not aligned with the start/stop of the interval (e.g. you could bin days at noon rather than at midnight)However it is a good question if
date_truncwould be a better way to get the desired behavioralamb commentedon Jul 15, 2024
Update: @appletreeisyellow merged an workaround --
to_local_timein #11401She has no further current concrete plans to work on this ticket at this time
Omega359 commentedon Jul 23, 2024
FYI - I came across the Jiff crate the other day that looks to have some nice support for tz aware calendar handling. From the benchmarks I've run it's not quite as quick as chrono for some things and about equivalent to others. I would recommend anyone who wants to tackle this issue to have a close look at it to see if it solves some of the intricacies around tz conversion and DST arithmetic and rounding.
BurntSushi commentedon Jul 27, 2024
Author of Jiff here. I don't have a ton of context on the specific problem in this issue, but I'd be happy to field questions about DST safe arithmetic. Jiff in particular is heavily inspired by Temporal, and Temporal (and Jiff) both intend to match RFC 5545 for duration handling.
(It's still early days and I think there is a fair bit of room for improvement for Jiff in terms of perf. Both in micro-optimizing its existing code and in adding new APIs to facilitate faster ops when the use case demands it.)
adriangb commentedon Sep 11, 2026
Two data points for this thread.
to_local_time(#11401) was merged here as a workaround, and the issue was deliberately left open for native support, so this is not a request to close.1. The
to_local_timeworkaround is correct across DST transitions, including the repeated hour. The verification earlier in this thread usedto_char, on a monthly dataset that crosses transitions without landing on them. I checkedto_local_timeitself on DataFusion 55.0.0, grouping UTC instants by local day inEurope/Brussels:t_utcdataset from the issue body2024-04-302024-05-012024-03-302024-03-312024-04-012024-10-272024-10-28The first rows match the output requested in the issue body. In the fall-back set, both instants that read
02:30locally (one at+02:00, one at+01:00) bin to2024-10-27.2. #25165 would make the PostgreSQL spelling work, which addresses the discoverability concern raised here. @alamb's first comment on this issue notes that in PostgreSQL you get local-day bins by using
AT TIME ZONEto turn atimestamptzback into atimestamp, then applyingdate_bin. DataFusion cannot express that today, becauseAT TIME ZONEon a timezone-aware value returns a timezone-aware value (#12218). #25165 changes that. With it applied,gives byte-identical results to the
to_local_timespelling above for all three datasets, and the existingto_local_timespelling keeps working unchanged. Users could then write the idiom they already know from PostgreSQL instead of having to discoverto_local_time.That still is not a timezone-aware
date_bin, which is the remaining ask of this issue, so it should stay open. #25165 is under review and has a known limitation withCASEinputs, described at the top of that PR.Related: #25167 (
date_binanddate_truncdisagree on timezone-aware input) and #25168 (aligningdate_binto local midnight with an explicit origin drifts by an hour across a DST transition).mhilton commentedon Sep 26, 2026
Seeing as there has been some activity around this recently I've been trying to think about what reasonable semantics should be for a
date_binthat adjusts for DST changes. These are some suggestions designed to promote discussion rather than dictate policy.1.
date_bins should never jump backwardsA naive implementation that simply shifts to the timestamp to local time, performs the
data_binand then shifts back the local time using the timezone offset can produce surprising results where the bins overlap the discontinuity. Consider the following times when used with such a naive implementation ofdate_bin(INTERVAL '1 HOUR', t, '1970-01-01T00:30:00Z'):As can be seen around the discontinuity it is possible for an absolute timestamp to be put in a bin earlier than a smaller absolute timestamp. In my opinion a
date_binimplementation should be written to avoid this happening. In this particular case I think it would be better for a DST awaredate_binto follow how UTC would bin the values:2. An
origin_timestampparameter without a timezone offset acts as if it is in the binned timestamp's timezoneMost implementations I can think of would do this for free anyway, but it is worth stating explicitly. If the
origin_timestampdoes not explicitly say what time zone it is in then it is treated as if it is in the timezone of the value being binned. For example when binning timestamps in theAmerican/Los_Angelestimezone then an origin timestamp of1970-01-01T00:30:00works as if it were the absolute value1970-01-01T00:30:00-08:00.The default
origin_timestampshould be defined to be1970-01-01T00:00:00in local time, which is the start of the year 1970 in that timezone. So forAustralia/Adelaidethis will map to1970-01-01T00:00:00+09:30or1969-12-31T14:30:00Z.If the specified
origin_timestampis undefined or ambiguous in the binned timestamp's timezone then I thinkdate_binshould output an error.3. A day in a timezone starts at the first instance that reports the date as being that day
This might seem self-evident, but it is a looser definition than days start at midnight. This in necessary because historically there have been timezones where midnight does not exist in local time. For example in
America/Sao_Paulothe timestamp2018-10-03T23:59:59.999999999-03jumps straight to2018-10-04T01:00:00.000000000-02.A similar rule needs to apply to the start of months. In the same timezone
2015-10-31T23:59:59:.999999999-03jumped straight to2015-11-01T01:00:00.000000000-02.4. The value returned from
date_bincannot be after the timestamp being binnedThe timestamp value output from
date_binshould be lower than or equal to the timestamp being binned. This is another one that might be obvious but the arithmetic can get tricky when one starts dealing with negative values.5. Intervals adjusted for daylight savings should not be adjusted further than the offset change
Where an interval stretches over a discontinuity then the interval may be grown or shrunk to accommodate the discontinuity. Effort should be made to avoid an interval's length being further away from requested than is needed for the daylight savings adjustment.
6. Using an
expressionwith a timezone is sufficient to opt in to DST adjustmentI hold this loosely as it might give confusing results for existing
date_binusers. However the only alternatives I can think of either restrict the DST adjustment to intervals specified in months or days, or require an additional parameter. Seeing as changing timestamp arrays between two defined timezones is simply a metadata change it seems reasonable that users can cast toTimestamp(ns, Some("UTC"))if they want to avoid DST adjustment.I haven't actually put much thought into how one would implement all of this. I'd be interested to hear any other's thoughts.
alamb commentedon Sep 28, 2026
I think the 6 principles sound good to me -- maybe we should also do some investigation into other implementations of date_bin (I know you say postgres is not a good example, but has any other system figured out how to do this nicely?)