Subtract hours from the now() function

一笑奈何 提交于 2019-11-29 07:52:44

Answer for timestamp

You need to understand the nature of the data types timestamp without time zone and timestamp with time zone (names can be deceiving). If you don't, read this first:

The AT TIME ZONE construct transforms your timestamp to timestamptz, which is almost certainly the wrong move:

where eventtime at time zone 'CET' between '2015-06-16 06:00:00'
                                       and '2015-06-17 06:00:00'

First, it kills performance. Applying AT TIME ZONE to eventtime makes the expression not sargable. Postgres cannot use a plain index on eventtime. But even without index, sargable expressions are cheaper. Provide bounds adjusted to the values in the table, so you don't have to manipulate every row.
You could compensate with a matching expression index, but it's probably just a misunderstanding and wrong anyway.

What happens in that expression?

  1. AT TIME ZONE 'CET' transforms the timestamp value eventtime to timestamptz by appending the time zone offset of your current time zone. This takes DST (daylight saving time) into account, so you get a different offset for winter timestamps. Basically you get the answer to the question:

    What's the absolute time (UTC timestamp) when the given time zone sees the given timestamp?

    When displaying the result to the user it becomes the according local timestamp for the current time zone of the session with the according time zone offset appended. (May or may not be the same as the one used in the expression).

  2. The string literals on the right side have no data type to them, so they intended type is derived from the assignment in the expression. Since we effectively have timestamptz now, both are cast to timestamptz, assuming the current time zone of the session.

    Give me the UTC timestamp for the moment in time, when the local time looks like the given timestamp.

    The offset varies with DST rules.

Long story short, if you operate with the same time zone everywhere: CET or 'Europe/Berlin', same thing for present-day timestamps, but not for historic or (possibly) future ones, you can just cut the cruft.

The second problem with the expression: BETWEEN is almost always wrong with timestamp values. Details:

SELECT date_trunc('hour', eventtime) AS hour
     , count(DISTINCT serialnumber)  AS ct  -- sure you need distinct?
FROM   t_el_eventlog
WHERE  eventtime >= now()::date - interval '18 hours'
AND    eventtime <  now()::date + interval '6 hours'
AND    sourceid  =  44  -- don't quote the numeric literal
GROUP  BY 1
ORDER  BY 1;

now() is the Postgres implementation of the SQL standard CURRENT_TIMESTAMP. Both return timestamptz (not timestamp!). You can use either.
now()::date is equivalent to CURRENT_DATE. Both depend of the current time zone setting.

You should have an index of the form:

CREATE INDEX foo ON t_el_eventlog(sourceid, eventtime)

Or, to allow index-only scans:

CREATE INDEX foo2 ON t_el_eventlog(sourceid, eventtime, serialnumber)

If you operate in different time zones, things get more complicated and you should use timestamptz for everything.

Alternative for timestamptz

Before the question update, it seemed like time zones matter. When dealing with different time zones, "today" is a functional dependency of the current time zone. People tend to forget that.

To just work with the current time zone setting of the session, use the same query as above. If executed in a different time zone, the results are wrong in actuality. (Applies to the above as well.)

To guarantee a correct result for a given time zone ('Europe/Berlin' in your case) irregardless of the current time zone setting of the session, use this expression instead:

    ((now() AT TIME ZONE 'Europe/Berlin')::date - interval '18 hours')
            AT TIME ZONE 'Europe/Berlin'  -- 2nd time to convert back

Be aware that the AT TIME ZONE construct returns timestamp for timestamptz input and vice-versa.

As mentioned at the outset, all the gory details here:

Your can use CURRENT_DATE:

 select date_trunc('hour', t_el_eventlog.eventtime at time zone 'CET') as hours,
        count(distinct t_el_eventlog.serialnumber) as count
 from t_el_eventlog
 where eventtime at time zone 'CET' between CURRENT_DATE + interval '6 hour' and
                                            CURRENT_DATE + interval '30 hour' and
       sourceid = '44'
 group by hours
 order by hours asc;

EDIT:

Erwin's comment is about the question not this answer. Using between for date/times is a bad idea. I suppose this should be repeated in every question that does this. But the problem is that the date/time values that are boundaries between days are counted twice.

The correct logic is:

 select date_trunc('hour', t_el_eventlog.eventtime at time zone 'CET') as hours,
        count(distinct t_el_eventlog.serialnumber) as count
 from t_el_eventlog
 where eventtime at time zone 'CET' >= CURRENT_DATE + interval '6 hour' and
       eventtime at time zone 'CET' < CURRENT_DATE + interval '30 hour' and
       sourceid = '44'
 group by hours
 order by hours asc;

Note the "<" for the second limit. Here is a good blog on this subject. Although Aaron is focused on SQL Server, the warnings (and some of the solutions) apply to other databases as well.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!