PostgreSQL: Compute number of hours in the day on daylight savings time - postgresql

I'm trying to come up with a query that will properly count that there are 25 hours on daylight savings. My table has a column of type timestampz called hourly_timestamp. The incorrect answer I have so far looks like this:
select EXTRACT(epoch FROM tomorrow-today)/3600
from(
select date_trunc('day', timezone('America/New_York', hourly_timestamp) as today ,
date_trunc('day', timezone('America/New_York', hourly_timestamp)))
+ '1 day'::interval as tomorrow
)t;
When this query executed during daylight savings time, I still only get 24 hours back and not 25. Any ideas how to do this correctly?

The number of hours varies with the clock.
with hours as (
select (timestamp with time zone '2014-11-01 00:00:00 America/New_York' + (n || ' hour')::interval) as hourly_timestamp
from generate_series(0, 72) n
)
select hourly_timestamp
, hourly_timestamp + interval '1' day as one_day_later
, hourly_timestamp + interval '1' day - hourly_timestamp as elapsed_time
from hours;
hourly_timestamp one_day_later elapsed_time
--
[snip]
2014-11-01 22:00:00-04 2014-11-02 22:00:00-05 1 day 01:00:00
2014-11-01 23:00:00-04 2014-11-02 23:00:00-05 1 day 01:00:00
2014-11-02 00:00:00-04 2014-11-03 00:00:00-05 1 day 01:00:00
2014-11-02 01:00:00-04 2014-11-03 01:00:00-05 1 day 01:00:00
2014-11-02 01:00:00-05 2014-11-03 01:00:00-05 1 day
2014-11-02 02:00:00-05 2014-11-03 02:00:00-05 1 day
2014-11-02 03:00:00-05 2014-11-03 03:00:00-05 1 day
2014-11-02 04:00:00-05 2014-11-03 04:00:00-05 1 day
[snip]
Note that 01:00 repeats, but with a different offset. Daylight savings time ends at 02:00, the clocks fall back and repeat the hour between 01:00 and 02:00, but since daylight savings time has ended, there are now five hours between the UTC and America/New_York time zones.
This similar query displays dates, not timestamps.
with dates as (
select (timestamp with time zone '2014-11-01 00:00:00 America/New_York' + (n || ' day')::interval) as daily_timestamp
from generate_series(0, 2) n
)
select daily_timestamp::date
, (daily_timestamp + interval '1' day)::date as one_day_later
, daily_timestamp + interval '1' day - daily_timestamp as elapsed_time
from dates;
daily_timestamp one_day_later elapsed_time
--
2014-11-01 2014-11-02 1 day
2014-11-02 2014-11-03 1 day 01:00:00
2014-11-03 2014-11-04 1 day
Where did you go wrong? By calculating the elapsed time after you truncated the time information. (Dates don't have time zones associated with them.) If I take the second query and cast "daily_timestamp" to a date in the common table expression, I get 24 hours, too.
with dates as (
select (timestamp with time zone '2014-11-01 00:00:00 America/New_York' + (n || ' day')::interval)::date as daily_timestamp
from generate_series(0, 2) n
)
select daily_timestamp::date
, (daily_timestamp + interval '1' day)::date as one_day_later
, daily_timestamp + interval '1' day - daily_timestamp as elapsed_time
from dates;
daily_timestamp one_day_later elapsed_time
--
2014-11-01 2014-11-02 1 day
2014-11-02 2014-11-03 1 day
2014-11-03 2014-11-04 1 day

You first have to do the extraction to epoch and then the calculations:
WITH test AS (
SELECT '2014-10-26'::timestamptz at time zone 'America/New_York' AS today,
'2014-10-27'::timestamptz at time zone 'America/New_York' AS tomorrow
)
SELECT
extract(epoch from tomorrow) - extract(epoch from today) AS seconds, -- 90000
(extract(epoch from tomorrow) - extract(epoch from today)) / 3600 AS hours -- 25
FROM test;

Related

How are months intervals internally calculated in Postgres?

In PostgreSQL, the interval of '1 month' sometimes counts as 30 days and sometimes counts as 31 days. What are the criteria used to determine this?
I ran the below query to demonstrate my confusion.
select
now() - interval '1 month'
, now() - interval '30 days'
, interval '30 days' = interval '1 month'
, interval '31 days' = interval '1 month'
The query returns:
2022-03-27 21:09:30.933434+00 | 2022-03-28 21:09:30.933434+00 | true | false
I would expect the query to return both days on March 28th, since an interval of one month is equal to an interval of 30 days.
It comes down to the specific vs the general where day is the specific and month is not. The same happens with day and hour as in:
select '2022-03-13 12:00 PDT'::timestamptz - '1 day'::interval;
?column?
------------------------
2022-03-12 12:00:00-08
select '2022-03-13 12:00 PDT'::timestamptz - '24 hours'::interval;
?column?
------------------------
2022-03-12 11:00:00-08
DST occurred morning of 2022-03-13 in PST/PDT. So a day is generalized to the same time a day ago whereas 24 hours ago is actually 24 hours passing.
In your case:
select
now() - interval '1 month'
, now() - interval '30 days';
?column? | ?column?
-------------------------------+-------------------------------
2022-03-27 14:44:33.515669-07 | 2022-03-28 14:44:33.515669-07
The 1 month is going to go back to the same date and time one month back, whereas 30 days is going back an actual 30 days.
In this case:
select '2022-03-30 21:17:05'::timestamp - interval '1 month' ;
?column?
---------------------
2022-02-28 21:17:05
There is no day 30 in February so it goes to the actual end of the month the 28th.

How to select month end data in PostgreSQL?

I'm working on the Accrual Reversal query in PostgreSQL. The system running doesn't have the reversal flag. So I need to consider all the end of the day of previous month accrued invoices as the reversal amount. And need to union them all with the main query. I can do it for last month but invoice date are dynamic, user may give 2 years as invoice period. For those 2 years, all the previous month data should be considered as accrued reversal. Here is the query
select invoicename, * from accountpay where invoice_date between '2020-01-01' and '2021-12-31'
union all
select concat('Accured Reversal', invoicename) as reference, * from accountpay where accrual = true and invoice_date::date = (select concat(date_part('year',((('2021-12-30'::date) - interval '1 month'))), '-', date_part('month',((('2021-12-30'::date) - interval '1 month'))), '-01')::date + interval '1 month' - interval '1 day')
Please help me to do this.
Thanks in Advance
SELECT (
Date_trunc('MONTH',a) + interval '1 month -1 day ')
as last_day_of_month
FROM generate_series(
'2020-01-01 00:00'::timestamp
- interval '12 months',
'2022-01-01 00:00',
'1 month') as dt(a);
get last_day_of_month from '2020-01-01 00:00' till '2022-01-01 00:00'
Then your sql would be
invoice_date in
(SELECT (Date_trunc('MONTH',a) + interval '1 month -1 day ')
as last_day_of_month
FROM generate_series(
'2020-01-01 00:00'::timestamp
- interval '12 months',
'2021-01-01 00:00',
'1 month') as dt(a))
This will get the last day of last 12 months so the number of months will be place holder (dynamic) and all the last day of the months will be in IN clause.
SQL re-written:
WITH date_cte AS
(
SELECT Date_trunc('MONTH',dt)+ interval '1 month -1 day ' last_day_of_month
FROM generate_series('2021-11-30 00:00:00'::timestamp - interval '12 months','2021-11-30 00:00:00','1 month') t(dt))
select invoicename, * from accountpay where invoice_date between '2020-01-01' and '2021-12-31'
union all
select concat('Accured Reversal', invoicename) as reference, * from accountpay where accrual = true and invoice_date::date in (select * from date_cte);
Basically , the last dates are generated this way for 12 months:
WITH date_cte AS
(
SELECT Date_trunc('MONTH',dt)+ interval '1 month -1 day ' last_day_of_month
FROM generate_series('2021-11-30 00:00:00'::timestamp - interval '12 months','2021-11-30 00:00:00','1 month') t(dt))
SELECT *
FROM date_cte;
last_day_of_month
---------------------
2020-11-30 00:00:00
2020-12-31 00:00:00
2021-01-31 00:00:00
2021-02-28 00:00:00
2021-03-31 00:00:00
2021-04-30 00:00:00
2021-05-31 00:00:00
2021-06-30 00:00:00
2021-07-31 00:00:00
2021-08-31 00:00:00
2021-09-30 00:00:00
2021-10-31 00:00:00
2021-11-30 00:00:00
You can replace 12 months by any number of months or you can make it year too like:
...generate_series('2021-11-30 00:00:00'::timestamp - interval '1 year','2021-11-30 00:00:00','1 month')

Get week number, with weeks starting on Sunday, like Excel WEEKNUM

In PostgreSQL (I'm on version 9.6.6), what's the simplest way to get the week number, starting on Sunday?
DATE_PART('week',x) returns:
The number of the ISO 8601 week-numbering week of the year. By definition, ISO weeks start on Mondays and the first week of a year contains January 4 of that year. In other words, the first Thursday of a year is in week 1 of that year. (doc)
Say my query is like:
WITH dates as (SELECT generate_series(timestamp '2014-01-01',
timestamp '2014-01-31',
interval '1 day'
)::date AS date
)
SELECT
date,
TO_CHAR(date,'Day') AS dayname,
DATE_PART('week',date) AS weekofyear
FROM dates
Returns:
date dayname weekofyear
--------------------------------
2014-01-01 Wednesday 1
2014-01-02 Thursday 1
2014-01-03 Friday 1
2014-01-04 Saturday 1
2014-01-05 Sunday 1 <- I want this to be 2
2014-01-06 Monday 2
2014-01-07 Tuesday 2
2014-01-08 Wednesday 2
So far I have tried:
SELECT
date,
TO_CHAR(date,'Day') AS dayname,
DATE_PART('week',date) AS week_iso,
DATE_PART('week',date + interval '1 day') AS week_alt
FROM dates
which won't quite work if the year begins on a Sunday.
Also, I want week 1 to contain January 1 of that year. So if January 1 is a Saturday, I want week 1 to be one day long (instead of being week 53 in the ISO style). This behavior is consistent with the Excel WEEKNUM function.
To get the week number of the year, with weeks starting on Sunday, we need to know how many Sundays between the first day of the year and the target date.
I adapted the solution here by #Erwin Brandstetter. This solution counts Sundays inclusive of the first day of the year and exclusive of the target date.
Then, because I want the first (partial) week to be week one (not zero), I need to add 1 unless the first day of the year is a Sunday (in which case it's already week one).
WITH dates as (SELECT generate_series(timestamp '2014-01-01',
timestamp '2014-01-31',
interval '1 day'
)::date AS date
)
SELECT
date,
TO_CHAR(date,'Day') AS dayname,
DATE_PART('week',date) AS week_iso,
((date - DATE_TRUNC('year',date)::date) + DATE_PART('isodow', DATE_TRUNC('year',date)) )::int / 7
+ CASE WHEN DATE_PART('isodow', DATE_TRUNC('year',date)) = 7 THEN 0 ELSE 1 END
AS week_sundays
FROM dates
Returns
date dayname weekofyear week_sundays
--------------------------------
2014-01-01 Wednesday 1 1
2014-01-02 Thursday 1 1
2014-01-03 Friday 1 1
2014-01-04 Saturday 1 1
2014-01-05 Sunday 1 2
2014-01-06 Monday 2 2
2014-01-07 Tuesday 2 2
To show how this works for years starting on Sunday:
2017-01-01 Sunday 52 1
2017-01-02 Monday 1 1
2017-01-03 Tuesday 1 1
2017-01-04 Wednesday 1 1
2017-01-05 Thursday 1 1
2017-01-06 Friday 1 1
2017-01-07 Saturday 1 1
2017-01-08 Sunday 1 2
The task is not as daunting as it first appears. It mainly requires finding the first Sun on or after the 1-Jan. That date becomes the last day of the first week. From there calculation of subsequent weeks is merely. a matter of addition. The other significant point is with week definition there will always be 53 week per year and the last day of the last week is 31-Dec. The following generates an annual calendar for this week definition.
create or replace function non_standard_cal(year_in integer)
returns table (week_number integer, first_day_of_week date, last_day_of_week date)
language sql immutable leakproof strict rows 53
as $$
with recursive cal as
(select 1 wk, d1 start_of_week, ds end_of_week, de stop_date
from (select d1+substring( '0654321'
, extract(dow from d1)::integer+1
, 1)::integer ds
, d1, de
from ( select make_date (year_in, 1,1) d1
, make_date (year_in+1, 1,1) -1 de
) a
) b
union all
select wk+1, end_of_week+1, case when end_of_week+7 > stop_date
then stop_date
else end_of_week+7
end
, stop_date
from cal
where wk < 53
)
select wk, start_of_week, end_of_week from cal;
$$ ;
As a general rule I avoid magic numbers, but sometimes they're useful; as in this case. In magic number (actually a string) '0654321' each digit represents the number of days needed to reach the first Mon on or after 1-Jan when indexed by the standard day numbering system (0-6 as Sun-Sat). The result is the Mon being the last day of the first week. That generatess the 1st row of the recursive CTE. The remaining rows just add the appropriate number days for each week until the 53 weeks have been generated. The following shows the years needed to ensure each day of week gets it's turn to 1-Jan (yea some days duplicate). Run individual years to validate its calendar.
do $$
declare
cal record;
yr_cal cursor (yr integer) for
select * from non_standard_cal(2000+yr) limit 1;
begin
for yr in 18 .. 26
loop
open yr_cal(yr);
fetch yr_cal into cal;
raise notice 'For Year: %, week: %, first_day: %, Last_day: %, First day is: %'
, 2000+yr
,cal.week_number
,cal.first_day_of_week
,cal.last_day_of_week
,to_char(cal.first_day_of_week, 'Day');
close yr_cal;
end loop;
end; $$;
Following may work - tested with two cases in mind:
WITH dates as (SELECT generate_series(timestamp '2014-01-01',
timestamp '2014-01-10',
interval '1 day'
)::date AS date
union
SELECT generate_series(timestamp '2017-01-01',
timestamp '2017-01-10',
interval '1 day'
)::date AS date
)
, alt as (
SELECT
date,
TO_CHAR(date,'Day') AS dayname,
DATE_PART('week',date) AS week_iso,
DATE_PART('week',date + interval '1 day') AS week_alt
FROM dates
)
select date, dayname,
week_iso, week_alt, case when week_alt <> week_iso
then week_alt
else week_iso end as expected_week
from alt
order by date
Output:
date dayname week_iso week_alt expected_week
2014-01-01 Wednesday 1 1 1
2014-01-02 Thursday 1 1 1
2014-01-03 Friday 1 1 1
2014-01-04 Saturday 1 1 1
2014-01-05 Sunday 1 2 2
2014-01-06 Monday 2 2 2
2014-01-07 Tuesday 2 2 2
....
2017-01-01 Sunday 52 1 1
2017-01-02 Monday 1 1 1
2017-01-03 Tuesday 1 1 1
2017-01-04 Wednesday 1 1 1
2017-01-05 Thursday 1 1 1
2017-01-06 Friday 1 1 1
2017-01-07 Saturday 1 1 1
2017-01-08 Sunday 1 2 2
This query works perfectly replacing monday with sunday as the start of the week.
QUERY
SELECT CASE WHEN EXTRACT(day from '2014-01-05'::date)=4 AND
EXTRACT(month from '2014-01-05'::date)=1 THEN date_part('week',
'2014-01-05'::date) ELSE date_part('week', '2014-01-05'::date + 1)
END;
OUTPUT
date_part
-----------
2
(1 row)

Group by Date and sum of total duration for that day

I am using workbench/j Postgres DB for my query which is as follows -
Input
ID |utc_tune_start_time |utc_tune_end_time
----------------------------------------------
A |04-03-2019 19:00:00 |04-03-2019 20:00:00
----------------------------------------------
A |04-03-2019 23:00:00 |05-03-2019 01:00:00
-----------------------------------------------
A |05-03-2019 10:00:00 |05-03-2019 10:30:00
-----------------------------------------------
Output
ID |Day |Duration in Minutes
----------------------------------------
A |04-03-2019 |120
-----------------------------------
A |05-03-2019 |90
-----------------------------------
I require the duration elapsed from the utc_tune_start_time till the end of the day and similarly, the time elapsed for utc_tune_end_time since the start of the day.
Thanks for your clarifications. This is possible with some case statements. Basically, if utc_tune_start_time and utc_tune_end_time are on the same day, just use the difference, otherwise calculate the difference from the end or start of the day.
WITH all_activity as (
select date_trunc('day', utc_tune_start_time) as day,
case when date_trunc('day', utc_tune_start_time) =
date_trunc('day', utc_tune_end_time)
then utc_tune_end_time - utc_tune_start_time
else date_trunc('day', utc_tune_start_time) +
interval '1 day' - utc_tune_start_time
end as time_spent
from test
UNION ALL
select date_trunc('day', utc_tune_end_time),
case when date_trunc('day', utc_tune_start_time) =
date_trunc('day', utc_tune_end_time)
then null -- we already calculated this earlier
else utc_tune_end_time - date_trunc('day', utc_tune_end_time)
end
FROM test
)
select day, sum(time_spent)
FROM all_activity
GROUP BY day;
day | sum
---------------------+----------
2019-03-04 00:00:00 | 02:00:00
2019-03-05 00:00:00 | 01:30:00
(2 rows)

How many seconds passed by grouped by hour between two dates

Let's suppose I have a start date 2016-06-19 09:30:00 and an end date 2016-06-19 10:20:00
I would like to get the time that elapsed every hour before starting the next hour or before getting to the final time in seconds grouped by hour and date, the result I'm trying to achieve (without having any success) would be something like this:
hour | date | time_elapsed_in_seconds
9 | 2016-06-19 | 1800 (there are 1800 seconds between 09:30:00 and 10:00:00)
10 | 2016-06-19 | 1200 (there are 1200 seconds between 10:00:00 and 10:20:00)
Try this :
with table1 as (
select '2016-06-19 09:30:00'::timestamp without time zone start_date,'2016-06-19 10:20:00'::timestamp without time zone end_date
)
select extract(hour from the_hour) "hour",the_hour::date "date",extract (epoch from (new_end-new_start)) "time_elapsed" from (
select the_hour,CASE WHEN date_trunc('hour',start_date)=the_hour then start_date else the_hour end new_start,
CASE WHEN date_trunc('hour',end_date)=the_hour then end_date else the_hour+'1 hour'::interval end new_end
from (
select generate_series(date_trunc('hour',start_date),end_date,'1 hour'::interval) the_hour,start_date,end_date from table1
) a
) b