How does Postgresql round half-microseconds in timestamps? - postgresql

I was under the impression that PostgreSQL rounded half-microseconds in timestamps to the nearest even microsecond. E.g.:
> select '2000-01-01T00:00:00.0000585Z'::timestamptz;
timestamptz
-------------------------------
2000-01-01 01:00:00.000058+01
(1 row)
> select '2000-01-01T00:00:00.0000575Z'::timestamptz;
timestamptz
-------------------------------
2000-01-01 01:00:00.000058+01
(1 row)
Then I discovered that:
> select '2000-01-01T00:00:00.5024585Z'::timestamptz;
timestamptz
-------------------------------
2000-01-01 01:00:00.502459+01
(1 row)
Does anybody know the rounding algorithm Postgresql uses for timestamps?
For your information, here's the version of Postgresql I'm running:
> select version();
version
----------------------------------------------------------------------------------------------------------------
PostgreSQL 9.6.1 on x86_64-apple-darwin15.6.0, compiled by Apple LLVM version 8.0.0 (clang-800.0.42.1), 64-bit
(1 row)

All the PostgreSQL time types have a microsecond resolution, six decimal places. Rounding to the nearest even microsecond would not be microsecond resolution.
Its behavior looks consistent with round half-up to me, the usual way to round. >= 0.5 round up, else round down.
0.5024585 rounded half-up to 6 decimal places rounds up to 0.502459 because the 7th digit is 5.
test=# select '2000-01-01T00:00:00.5024585Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502459
(1 row)
0.5024584999999 rounds down to 0.502458 because the 7th digit is 4.
test=# select '2000-01-01T00:00:00.5024584999999Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502458
(1 row)
Nevermind, the above appears to be anomalous. Stepping through '2000-01-01T00:00:00.5024235Z' to '2000-01-01T00:00:00.5024355Z' is consistent with half-even rounding.
I'm going to guess the anomalies are due to floating point error converting from floating point seconds in the input to the integer microseconds that timestamp uses.
test=# select '2000-01-01T00:00:00.5024235Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502424
(1 row)
test=# select '2000-01-01T00:00:00.5024245Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502425
(1 row)
test=# select '2000-01-01T00:00:00.5024255Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502425
(1 row)
test=# select '2000-01-01T00:00:00.5024265Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502426
(1 row)
test=# select '2000-01-01T00:00:00.5024275Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502428
(1 row)
test=# select '2000-01-01T00:00:00.5024285Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502428
(1 row)
test=# select '2000-01-01T00:00:00.5024295Z'::timestamp;
timestamp
---------------------------
2000-01-01 00:00:00.50243
(1 row)
test=# select '2000-01-01T00:00:00.5024305Z'::timestamp;
timestamp
---------------------------
2000-01-01 00:00:00.50243
(1 row)
test=# select '2000-01-01T00:00:00.5024315Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502432
(1 row)
test=# select '2000-01-01T00:00:00.5024325Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502432
(1 row)
test=# select '2000-01-01T00:00:00.5024335Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502434
(1 row)
test=# select '2000-01-01T00:00:00.5024345Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502434
(1 row)
test=# select '2000-01-01T00:00:00.5024355Z'::timestamp;
timestamp
----------------------------
2000-01-01 00:00:00.502436
(1 row)
This also plays out with interval N microsecond. Less decimal places means less floating point error.
test=# select interval '0.5 microsecond';
interval
----------
00:00:00
(1 row)
test=# select interval '1.5 microsecond';
interval
-----------------
00:00:00.000002
(1 row)
test=# select interval '2.5 microsecond';
interval
-----------------
00:00:00.000002
(1 row)
test=# select interval '3.5 microsecond';
interval
-----------------
00:00:00.000004
(1 row)
test=# select interval '4.5 microsecond';
interval
-----------------
00:00:00.000004
(1 row)
test=# select interval '5.5 microsecond';
interval
-----------------
00:00:00.000006
(1 row)
test=# select interval '6.5 microsecond';
interval
-----------------
00:00:00.000006
(1 row)
A small C program confirms there's a floating point accuracy problem with single precision floats at 7 decimal places that would affect rounding.
#include <math.h>
#include <stdio.h>
int main() {
float nums[] = {
0.5024235f,
0.5024245f,
0.5024255f,
0.5024265f,
0.5024275f,
0.5024285f,
0.5024295f,
0.5024305f,
NAN
};
for( int i = 0; !isnan(nums[i]); i++ ) {
printf("%0.8f\n", nums[i]);
}
}
This produces:
0.50242352
0.50242448
0.50242549
0.50242651
0.50242752
0.50242847
0.50242949
0.50243050
Whereas with doubles, there's no problem.
0.50242350
0.50242450
0.50242550
0.50242650
0.50242750
0.50242850
0.50242950
0.50243050

Related

Efficient time rounding function

I am writing a custom postgresql function to round TIMESTAMPTZ fields to an arbitrary interval with the basic algorithm of round(timestamp / interval) * interval and some research, I found a solution:
SELECT to_timestamp(round((extract('epoch' from timestamp)) / interval) * interval)
it works. My question: is there a more efficient way of doing this?
Look this like a template:
Round a timestamp to the nearest 5-minute mark.
The sample usage:
postgres=# select now(), round_time('2010-09-17 16:48');
now | round_time
-------------------------------+------------------------
2010-09-19 08:36:31.701919+02 | 2010-09-17 16:50:00+02
(1 row)
postgres=# select now(), round_time('2010-09-17 16:58');
now | round_time
-------------------------------+------------------------
2010-09-19 08:36:43.860858+02 | 2010-09-17 17:00:00+02
(1 row)
postgres=# select now(), round_time('2010-09-17 16:57');
now | round_time
-------------------------------+------------------------
2010-09-19 08:36:53.273612+02 | 2010-09-17 16:55:00+02
(1 row)
postgres=# select now(), round_time('2010-09-17 23:58');
now | round_time
------------------------------+------------------------
2010-09-19 08:37:09.41387+02 | 2010-09-18 00:00:00+02
(1 row)

postgresql error : Multiple decimal points

So I'm having this query:
SELECT
TO_CHAR(date_part('hour', created_at), 'YYYY-MM-DD HH24'),
to_char(created_at, 'day') ",
COUNT(*) AS "
FROM table
GROUP BY 1,2
ORDER BY 1 DESC
When I execute the query I get this:
ERROR: multiple decimal points
Searching stackoverflow I found some recommendations here:
How to format bigint field into a date in Postgresql? but I don't get why do I have to divide by 1000 and how this would apply in the case of the date_part function.
I assume created_at is a timestamp?.. I'm choosing from date_part(text, timestamp) and date_part(text, interval), if so date_part will return a double precision, to which you try to apply the mask 'YYYY-MM-DD HH24', eg:
v=# select date_part('hour', now());
date_part
-----------
9
and I don't see how you could possibly get year, month, day and hour from nine...
Yet I assume you wanted to apply the mask against truncated date to the hour precision, which is done with date_trunc(text, timestamp):
v=# select date_trunc('hour', now());
date_trunc
------------------------
2017-06-20 09:00:00+01
(1 row)
so now you can apply the time format:
v=# select to_char(date_trunc('hour', now()),'YYYY-MM-DD HH24');
to_char
---------------
2017-06-20 09
(1 row)
but if this is what you want, then you don't need to truncate time at all:
v=# select to_char(now(),'YYYY-MM-DD HH24');
to_char
---------------
2017-06-20 09
(1 row)
https://www.postgresql.org/docs/current/static/functions-datetime.html

PGSQL - How do I Add 5 hours to DATE in WHERE Clause?

I either need to add 5 hours or convert from GMT to EST. The return is currently showing everything from 7p and later yesterday...
WHERE
incident.initial_symptom = 'Chrome Upgrade' AND
DATE(incident.install_completed) = CURRENT_DATE;
Instead of manually adding interval to get wanted time zone, use at time zone, eg:
t=# select now(), now() at time zone 'est';
now | timezone
------------------------------+---------------------------
2017-04-07 07:07:39.17234+00 | 2017-04-07 02:07:39.17234
(1 row)
Depending on your timezone, exactly same statement adding interval to your date gives different result, eg at DST shift hour:
t=# set timezone TO 'WET';
SET
t=# select '2017-03-26 00:00:00'::timestamptz + '1 hour'::interval;
?column?
------------------------
2017-03-26 02:00:00+01
(1 row)
t=# set timezone TO 'EET';
SET
t=# select '2017-03-26 00:00:00'::timestamptz + '1 hour'::interval;
?column?
------------------------
2017-03-26 01:00:00+02
(1 row)

Why does postgres date_trunc return the hour 24?

Exactly what the question says.
mydb=> select '2016-01-03 24:00'::timestamp;
timestamp
---------------------
2016-01-04 00:00:00
(1 row)
That's what I expected.
mydb=> select date_trunc('seconds', '2016-01-03 23:59.9999999999'::timestamp);
date_trunc
---------------------
2016-01-03 00:24:00
(1 row)
Um. Wait, what?
It has nothing to do with date_trunc ... once you introduce the decimal point, 23:59.9999999999 is being interpreted as minutes and seconds rather than hours and minutes.
Without decimal point
db=# select '2016-01-03 23:59'::timestamp;
timestamp
---------------------
2016-01-03 23:59:00
(1 row)
With decimal point
db=# select '2016-01-03 23:59.9999999'::timestamp;
timestamp
---------------------
2016-01-03 00:24:00
(1 row)
It's understandable, given what you were expecting to get back, but you seem to have misread 24 minutes as 24 hours in the result here.
As a side note, the rounding kicks in once you go past six digits (i.e. microseconds) after the decimal place:
db=# select '2016-01-03 23:59.999999'::timestamp;
timestamp
----------------------------
2016-01-03 00:23:59.999999
(1 row)

Getting different results when converting timestamp to text

I'm tying to convert timestamp field in format (YYYY-MM-DD HH:MI:SS.MS) to it's text representation. But for some reason getting different results:
If I'm trying to convert timestamp from table:
create table test_dt (dt timestamp);
insert into test_dt values ('2016-04-14 17:10:33.007');
insert into test_dt values ('2016-04-14 17:10:33');
Timestamps are getting truncated up to the seconds:
select dt::text from test_dt;
dt
---------------------
2016-04-14 17:10:33
2016-04-14 17:10:33
(2 rows)
But if Im using direct select statement, everything works:
select '2016-04-14 17:10:33.007'::timestamp::text;
varchar
-------------------------
2016-04-14 17:10:33.007
(1 row)
The question is not how to convert it to the text from table and include precision, but rather:
what am I doing wrong?
why those 2 approaches returns different result?
what's the rational behind this behaviour?
UPDATE
as #muistooshort suggested the following command gives the correct result:
select c::text from (select '2016-04-14 17:10:33.007'::timestamp union select '2016-04-14 17:10:33'::timestamp ) as t(c);
c
-------------------------
2016-04-14 17:10:33
2016-04-14 17:10:33.007
(2 rows)
and yes test_dt does have .007 :
select * from test_dt;
dt
-------------------------
2016-04-14 17:10:33
2016-04-14 17:10:33.007
(2 rows)
Also to_char gives milliseconds from the table:
select to_char(dt, 'YYYY-MM-DD HH:MI:SS.MS') from test_dt;
to_char
-------------------------
2016-04-14 05:10:33.000
2016-04-14 05:10:33.007
(2 rows)