Andrei Pall

Linux Software Engineering

PostgreSQL Time and Temporal Data

PostgreSQL is a powerful, open source object-relational database system. It has more than 15 years of active development and a proven architecture that has earned it a strong reputation for reliability, data integrity, and correctness. PostgreSQL runs on all major operating systems, including Linux, UNIX and Windows.

-- Date construction 
select date '2015-10-21';
select '2015-10-21'::date;
select '2015-21-10'::date;
select date 'Oct 21, 2015';
select date '2015/10/21';
select date '151021';
select date 'Oct 21, 2015 BC';
select date 'J10';
select date 'infinity';
select date '-infinity';
select date 'epoch';

-- Time construction
select time '12:34:56.789';
select '12:34:56.789'::time;
select time '34:56.789';
select time '12:34';
select time (1) '12:34:00.199';
select time '010203';
select time '3:14pm';
select time 'allballs';
select time '0:0';

-- Timestamp construction
select timestamp '2015-10-21 12:34:56.789';
select timestamp '2015-10-21';
select timestamp '-infinity';
select timestamp 'epoch';
select timestamp 'now';

-- Interval construction
select interval '1 year 2 months 3 days';
select interval '1 year 2 months 3 days ago';
select interval '1 12:34';
select interval '1-1 12:34';
select interval '12:34';
select interval 'P1Y2M3DT4H5M6.789S';
select interval 'P1Y2M3M4M5M10Y';
select interval 'P0001-02-03T04:05:06';
select date '2017-02-01' + interval '1 month';
select date '2017-03-01' + interval '1 month';
select interval '1 day' - interval '300 hours';
select interval '1-2 4:5:6.789' year to month;
select interval '1-2 4:5:6.789' day to minute;
create table vehicle_rentals (is_late_by interval hour);
select interval '1-2 4:5:6.789' day to second (1);

-- Date operators
select date '1985-10-21' + 5;
select date '1985-10-26' - 5;
select date '1985-10-21' + interval '30 years';
select date '2015-10-21' - interval '30 years';
select date '2015-10-21' + time '01:00';
select date '2015-10-26' - date '2015-10-21';
select date '2015-10-26' = date '2015-10-21';
select date '2015-10-26' < date '2015-10-21';

-- Date functions
select make_date(2015, 10, 21);
select date_part('year', date '2016-10-21');
select extract('month' from date '2016-10-21');
select date_part('dow', date '2015-10-21');
select date_part('epoch', date '2015-10-21');
select isfinite(date '-infinity');
select to_char(date '2015-10-21', 'Mon-YY');
select to_char(date '2015-10-21', 'Month YYYY BC, day DD');
select to_char(date '2015-10-21', 'Month YYYY BC, "day" DD');
select to_char(date '2015-10-21', 'FMMonth YYYY BC, "day" DD');
select to_char(date '2015-10-21', 'FMMonth YYYY BC, DDth "day"');
select to_char(date '2015-10-21', 'FMMonth YYYY BC, DDth "day" (DAY of "w"eek WW)');
select to_date('21st October 2015 BC', 'DDth Month YYYY BC');
select to_date('2015-20-40', 'YYYY-MM-DD');

-- Time operators
select time '01:00' + interval '4 hours';
select time '15:30' - time '12:00';
select time '15:30' + interval '1 year';
select time '3:00' = time '3:01';
select time '3:00' < time '3:01';

-- Time functions
select make_time(1, 2, 3.456);
select date_part('hour', time '1:2:3.456789');
select date_part('minute', time '1:2:3.456789');
select date_part('second', time '1:2:3.456789');
select date_part('millisecond', time '1:2:3.456789');
select date_part('microsecond', time '1:2:3.456789');
select date_trunc('hour', time '01:02:56.123789');
select date_trunc('millisecond', time '01:02:56.123789');
select to_char(time '15:02:03.456', 'HH.MI AM (SSSS"s" "since midnight")');

-- Timestamp operators
select timestamp '2015-10-21 01:00' + interval '2 days 4 hours';
select timestamp '2015-10-21 01:00' - time '03:30';
select timestamp '2015-10-26 01:00' - timestamp '2015-10-21 03:00';
select timestamp '2015-10-26 01:00' = timestamp '2015-10-21 03:00';
select timestamp '2015-10-26 01:00' < timestamp '2015-10-21 03:00';

-- Timestamp functions
select make_timestamp(2015, 10, 21, 1, 2, 3.4);
select to_timestamp(1490732210.566);
select age('1980-01-01 00:00:00');
select age('2015-10-26 0:0:0', '2015-10-21 2:0:0');
select age('2015-11-22', '2015-10-21');
select date_trunc('year', timestamp '2015-10-21 01:02:03');
select date_trunc('hour', timestamp '2015-10-21 01:02:03');
select date_trunc('month', date '2015-10-21')::date;
select (date_trunc('month', date '2015-10-21') + interval '1 month -1 day')::date;
select to_char(timestamp '2015-10-21 01:02:03', 'Mon-YY HH24:MI');
select to_timestamp('21st October 2015 BC 12:30', 'DDth Month YYYY BC HH24:MI');
select to_timestamp('2015-20-40', 'YYYY-MM-DD');

-- Date and timestamp gotchas
select age(date '2017-02-28', date '2016-02-28');
select date '2017-02-28' - date '2016-02-28';

-- Interval operators
select interval '1 month' + interval '1 hour';
select interval '1 year' - interval '5 months';
select interval '1 year' + interval '4000 days';
select -interval '1 hour';
select 10 * interval '1 hour';
select 0.3 * interval '1 year';
select interval '1 year' / 3.5;
select interval '1 year' = interval '360 days';

-- Interval functions
select make_interval(1, 2, 0, 3, 4, 5, 6.789);
select make_interval(days => 20, months => 2); 
select justify_hours('1 day 49 hours');
select justify_days('35 days');
select justify_interval('35 days 49 hours');
select to_char(interval '15:02:03.456', 'HH.MI AM (SSSS"s" "since midnight")');
-- Range construction
select numrange '[1, 10)';
select numrange(1, 10);
select numrange(1, 10, '[]');
select numrange '[1,)';
select numrange '(,10)';
select numrange '(,)';
select numrange 'empty';

-- Date range construction
select daterange '[today, infinity)';
select daterange '[today,)';
select daterange '[today, 2030-01-01)';
select daterange('yesterday', 'tomorrow', '[]');

-- Range functions
select lower(daterange '[today, 2030-01-01)');
select upper(daterange '[today, 2030-01-01)');
select lower_inc(daterange '[today, 2030-01-01)');
select upper_inc(daterange '[today, 2030-01-01)');
select lower_inf(daterange '[today, 2030-01-01)');
select upper_inf(daterange '[today, 2030-01-01)');
select isempty(daterange '[today, 2030-01-01)');
select range_merge(daterange '[2015-10-21, 2015-10-22)', daterange '[2015-10-25, 2015-10-26)');

-- Range operators
select daterange '[2015-10-21, 2015-10-30)' = daterange '[2015-10-26, 2015-11-30)';
select daterange '[2015-10-21, 2015-10-30)' != daterange '[2015-10-26, 2015-11-30)';
select daterange '[2015-10-21, 2015-10-30)' && daterange '[2015-10-26, 2015-11-30)';
select daterange '[2015-10-21, 2015-10-26)' @> date '2015-10-22';
select date '2015-10-22' <@ daterange '[2015-10-21, 2015-10-26)';
select daterange '[2015-10-21, 2015-10-26)' @> daterange '[2015-10-22, 2015-10-24)';
select daterange '[2015-10-22, 2015-10-24)' <@ daterange '[2015-10-21, 2015-10-26)';
select daterange '[2015-10-21, 2015-10-25)' << daterange '[2015-10-26, 2015-11-30)';
select daterange '[2015-10-26, 2015-11-30)' >> daterange '[2015-10-21, 2015-10-25)';
select daterange '[2015-10-21, 2015-10-25)' &< daterange '[2015-10-26, 2015-11-30)';
select daterange '[2015-10-21, 2015-10-25)' &> daterange '[2015-10-26, 2015-11-30)';
select daterange '[2015-10-21, 2015-10-25)' -|- daterange '[2015-10-25, 2015-10-26)';
select daterange '[2015-10-25, 2015-10-26)' -|- daterange '[2015-10-21, 2015-10-25)';
select daterange '[2015-10-21, 2015-10-23)' + daterange '[2015-10-25, 2015-10-26)';
select daterange '[2015-10-21, 2015-10-23)' * daterange '[2015-10-22, 2015-10-26)';
select daterange '[2015-10-21, 2015-10-31)' - daterange '[2015-10-25, 2015-11-30)';
select daterange '[2015-10-21, 2015-10-31)' - daterange '[2015-10-22, 2015-10-25)';

-- Timestamp range construction
select tsrange '[2015-10-21 00:00:00, 2015-10-26 15:30:45)';
select tsrange '[2015-10-21 00:00:00, 2015-10-26 15:30:45]';

-- Time range construction
create type timerange as range (subtype = time);
select timerange '[03:00, 03:30)' * timerange '[03:20, 04:00)';

-- Overlaps operator
select (date '2015-10-21', date '2015-10-30') overlaps (date '2015-10-26', date '2015-11-30');
select (date '2015-10-21', interval '9 days') overlaps (date '2015-10-26', interval '1 month');

-- Current time 
select date 'today';
select date 'yesterday';
select date 'tomorrow';
select timestamp 'yesterday';
select time 'now';
select timestamp 'now';

begin;
select now();
select now(); 
select now(); 
rollback;

begin;
select clock_timestamp();
select clock_timestamp(); 
select clock_timestamp(); 
rollback;

select statement_timestamp(), pg_sleep(1), statement_timestamp();
select clock_timestamp(), pg_sleep(1), clock_timestamp();

create table aeons (num int, created_at timestamp default 'now', updated_at timestamp default now());
insert into aeons(num) values (1);
insert into aeons(num) values (2);
insert into aeons(num) values (3);

-- Sequence generation
select * from generate_series(timestamp '2015-10-21', timestamp '2015-10-26', interval '2 hours');
select s.d::date from generate_series(timestamp '2015-10-21', timestamp '2015-10-26', interval '1 day') as s(d);

select date '2015-10-21' + s.i 
from generate_series(0, 5, 1) as s(i);

select id, created_at::date, start_at, finish_at 
from vehicle_usage 
where id = 'LP8574' 
order by created_at;

select d::date as created_on 
from generate_series(date '2017-03-01', date '2017-03-10', interval '1 day') s(d);

with dates as (
    select d::date as created_on 
    from generate_series(date '2017-03-01', date '2017-03-10', interval '1 day') s(d)
)
select id, created_on, start_at, finish_at
from dates
left join vehicle_usage on created_on = created_at::date
where id = 'LP8574' or id is null
order by created_on;

with dates as (
    select d::date as created_on 
    from generate_series(date '2017-03-01', date '2017-03-10', interval '1 day') s(d)
)
select id, created_on, start_at, finish_at
from dates
left join vehicle_usage on created_on = created_at::date and id = 'LP8574'
order by created_on;

with dates as (
    select d::date as created_on 
    from generate_series(date '2017-03-01', date '2017-03-10', interval '1 day') s(d)
    where date_part('isodow', d) < 6
)
select id, created_on, start_at, finish_at
from dates
left join vehicle_usage on created_on = created_at::date and id = 'LP8574'
order by created_on;
-- Time zone aware types

select timestamptz '2015-10-21 01:00:00+5:30';
select timestamptz '2015-10-21 01:00:00+1:23:45';
select timestamptz '2015-10-21 01:00:00 America/Los_Angeles';

-- Time zone views

select * from pg_timezone_names;
select * from pg_timezone_abbrevs;

-- Calculations

select make_timestamptz(2015, 10, 21, 1, 2, 3.4);
select make_timestamptz(2015, 10, 21, 1, 2, 3.4, 'NZ');

-- Set time zone

set time zone 'NZ';
set time zone 'US/Pacific';

-- Time zone conversions

set time zone 'NZ';
select timestamp '2015-10-21 10:00:00' at time zone 'PRC';
select timezone('PRC', timestamp '2015-10-21 10:00:00');
select timestamptz '2015-10-21 10:00:00+8' at time zone 'NZ';
select timestamptz '2015-10-21 10:00:00+8' at time zone interval '5:30';

-- Gotchas

set time zone 'US/Eastern';
select age(timestamptz '2017-07-01 12:00:00', timestamptz '2017-03-01 12:00:00'); -- 4 months
select timestamptz '2017-07-01 12:00:00' - timestamptz '2017-03-01 12:00:00'; -- not 4 months

select * from pg_timezone_names where name = 'CET'; -- entry exists
select * from pg_timezone_abbrevs where abbrev = 'CET'; -- also exists
set time zone 'UTC';
select timestamptz '2017-04-10 0:0:0 CET'; -- uses abbreviation, not time zone
-- Using timestamps to record when changes were made to a row
create table loads (
    machine_id bigint not null references machines(machine_id),
    volume double precision not null,
    created_at timestamptz not null default now(),
    updated_at timestamptz not null default now(),
    deleted_at timestamptz 
);

-- Partial index on non-deleted rows
create index loads_created_at_index 
on loads (created_at) 
where deleted_at is null;

-- Example constraints on user defined time columns
alter table people 
add check (birth_date <@ daterange((date 'today' - interval '150 years')::date, 'today', '[]'));

alter table people 
add check (activation_date = date_trunc('month', activation_date));

-- Table with a transaction time period
create table zones (
    zone_id bigserial not null, 
    name varchar not null,
    trans_period tstzrange not null default tstzrange(now(), 'infinity')
);

create extension btree_gist;
alter table zones 
add exclude using gist (zone_id with =, trans_period with &&);

alter table zones add check(
    (lower(trans_period) < now() and upper(trans_period) = now()) 
    or 
    (lower(trans_period) = now() and upper(trans_period) = 'infinity')
) not valid;


-- Testing the exclusion constraint
insert into zones (zone_id, project_id, trans_period)
values (3, 1, tstzrange(now(), 'infinity'));  [a]

insert into zones (zone_id, trans_period)
values (3, 1, tstzrange(now(), 'infinity'));  -- This will fail

-- Testing transaction time constraint
insert into zones (project_id, trans_period) 
values (3, tstzrange(now() - interval '1 hour', now() + interval '1 minute'));  -- This will fail

insert into zones (project_id, trans_period) 
values (3, tstzrange(now() + interval '1 hour', 'infinity'));  -- This will fail

-- Updating a row with transaction time
begin;
update zones set trans_period = tstzrange(lower(trans_period), now()) where zone_id = $1 and upper(trans_period) = 'infinity';

insert into zones (zone_id, name) 
values ($1, $2); 
commit;

-- Adding a current foreign key constraint
create or replace function 
    check_project_is_current() 
returns trigger as
$$
begin
if (select exists (
    select 1 from projects 
    where project_id = new.project_id 
        and upper(trans_period) = 'infinity'
)) then 
    return null;
else
    raise exception 'project % is not current', new.project_id;
end if;
end;
$$
language plpgsql;

create constraint trigger zones_check_project_id 
after insert or update
on zones
from projects
deferrable initially deferred
for each row
execute procedure check_project_is_current();

-- Testing a current foreign key constraint
insert into zones (zone_id, project_id, trans_period) 
values (3, 2, tstzrange(now(), 'infinity'));  -- This will fail if project 3 is non-current

-- Adding a sequenced foreign key constraint
create or replace function 
    check_project_is_sequenced() 
returns trigger as
$$
begin
if (select not exists (
    select 1 from projects 
    where project_id = new.project_id 
        and trans_period @> lower(new.trans_period)
)) then 
    raise exception 'project % doesn''t overlap with %', new.project_id, lower(new.trans_period);

elseif (select not exists (
    select 1 from projects 
    where project_id = new.project_id 
        and (trans_period @> upper(new.trans_period) or upper(trans_period) = upper(new.trans_period))
)) then
    raise exception 'project % doesn''t overlap with %', new.project_id, upper(new.trans_period);

elseif (select exists (
    with subsequent_periods as (
        select trans_period, lead(trans_period, 1) over (order by lower(trans_period)) as next_trans_period
        from projects
        where project_id = new.project_id 
            and trans_period && new.trans_period 
    )
    select 1 
    from subsequent_periods
    where not trans_period -|- next_trans_period
)) then 
    raise exception 'project % has gaps between %', new.project_id, new.trans_period;
else
    return null;
end if;
end;
$$
language plpgsql;

create constraint trigger zones_check_project_id 
after insert or update
on zones
from projects
deferrable initially deferred
for each row
execute procedure check_project_is_sequenced();

-- Adding a current and sequenced uniqueness constraint for valid time
create extension btree_gist;
alter table machine_assignments 
add exclude using gist (machine_id with =, valid_period with &&);

-- Ensuring a gapless sequence of valid periods
create or replace function 
    check_assignments_contiguous() 
returns trigger as
$$
begin
if (select exists (
    with subsequent_periods as (
        select valid_period, lead(valid_period, 1) over (order by lower(valid_period)) as next_valid_period
        from machine_assignments
        where machine_id = new.machine_id 
    )
    select 1 
    from subsequent_periods
    where not valid_period -|- next_valid_period
)) then 
    raise exception 'machine % has gaps in valid_period', new.machine_id;
else
    return null;
end if;
end;
$$
language plpgsql;

create constraint trigger machine_assignments_check_valid_period
after insert or update or delete
on machine_assignments
deferrable initially deferred
for each row
execute procedure check_assignments_contiguous();

-- SQL:2011 temporal features examples
create table vehicles (
    vehicle_id bigserial,
    project_id bigint,
    valid_from date,
    valid_to date,
    period for valid_period(valid_from, valid_to)
);

update vehicles 
    for portion of valid_period
        from date '2015-10-21'
        to date '2015-10-26'
set project_id = 17
where vehicle_id = 5367;
-- Aggregation
select min(created_at) 
from loads 
where vehicle_id = $1 
    and created_at >= date_trunc('month', now());
    
with rows as (values (interval '1 years'), (interval '360 days'), (interval '24 hours 1 minute')) 
select sum(column1) from rows;

select date_trunc('week', created_at) as week, count(*) 
from loads
group by 1 
order by 1 desc;

-- Independence from current time
select * 
from machine_costs
where lower(valid_period) <= coalesce($1, now());

-- Window functions
with gap_records as (
    select upper(valid_period) as period_end, lead(lower(valid_period), 1) over (partition by machine_id order by valid_period) as next_period_start, machine_id
    from machine_assignments  
)
select machine_id, sum(next_period_start - period_end) as idle_time
from gap_records
group by machine_id;

-- Coalescing ranges (range aggregation)

create table ranges (r daterange);

insert into ranges (r) values 
    (daterange('2015-01-10', '2015-01-12')),
    (daterange('2015-01-10', '2015-01-12')),
    (daterange('2015-01-01', '2015-01-05')),
    (daterange('2015-01-01', '2015-01-03')),
    (daterange('2015-01-03', '2015-01-06')),
    (daterange('2015-01-07', '2015-01-09')),
    (daterange('2015-01-08', '2015-01-09')),
    (daterange('2015-01-12', 'infinity'));
    
with a as (
    select r from ranges order by r
) 
, b as (
    select r, 
      case when max(upper(r)) over (rows between unbounded preceding and 1 preceding) >= lower(r) then null 
      else lower(r) end as low
    from a
)
, c as (
    select r, max(low) over (order by r) as low
    from b
)
select daterange(low, max(upper(r))) 
from c
group by low 
order by 1;    

-- Finding gaps between overlapping ranges
with a as (
    select r from ranges order by r
)
, b as (
    select r, 
      case when max(upper(r)) over (rows between unbounded preceding and 1 preceding) >= lower(r) then null 
      else lower(r) end as low
    from a
)
, c as (
    select daterange(lag(upper(r), 1) over (order by r), low) as gap
    from b
)
select gap
from c
where not lower_inf(gap) and not upper_inf(gap);

-- Time slice queries
select name, valid_period 
from machines 
join machine_assignments using (machine_id) 
where now() <@ valid_period 
order by name;

select machines.name, valid_period, zones.name, zones.geometry 
from machines 
join machine_assignments on machines.machine_id = machine_assignments.machine_id 
    and now() <@ machine_assignments.valid_period 
join zones on machine_assignments.zone_id = zones.zone_id and now() <@ zones.trans_period
order by machines.name;

-- Sequenced queries - selection
select * 
from machine assignments
where machine_id = 1;

-- Sequenced queries - projection
select zone_id, valid_period 
from machine_assignments
where machine_id = 1;

-- Sequenced queries - sorting
select * 
from machine_assignments 
order by zone_id, machine_id, valid_period;

select * 
from machine_assignments 
where now() <@ valid_period
order by zone_id, machine_id, valid_period;

-- Sequenced queries - joins
select ma.machine_id, cost_per_day, 
    tstzrange(greatest(lower(ma.valid_period), lower(mc.valid_period)), 
        least(upper(ma.valid_period), upper(mc.valid_period)))
from machine_assignments ma
join machine_costs mc using (machine_id)
where greatest(lower(ma.valid_period), lower(mc.valid_period)) < 
    least(upper(ma.valid_period), upper(mc.valid_period));
    
-- Sequenced queries - difference
select ma.machine_id, tstzrange(lower(ma.valid_period), lower(mr.valid_period)) as valid_period
from machine_assignments ma
join machine_repairs mr using (machine_id)
where zone_id = $1
    and lower(ma.valid_period) < lower(mr.valid_period)  -- exclude invalid ranges
    and int8range(lower(ma.valid_period), lower(mr.valid_period)) <@ ma.valid_period  -- exclude impossible ranges
    and not exists (
        select 1 
        from machine_repairs mr2
        where mr2.machine_id = ma.machine_id 
            and lower(ma.valid_period) < upper(mr2.valid_period) 
            and lower(mr2.valid_period) < lower(mr.valid_period)
    )

union

select ma.machine_id, tstzrange(upper(mr.valid_period), upper(ma.valid_period)) as valid_period
from machine_assignments ma
join machine_repairs mr using (machine_id)
where zone_id = $1
    and upper(mr.valid_period) < upper(ma.valid_period)  
    and int8range(upper(mr.valid_period), upper(ma.valid_period)) <@ ma.valid_period  
    and not exists (
        select 1 
        from machine_repairs mr2
        where mr2.machine_id = ma.machine_id 
            and upper(mr.valid_period) < upper(mr2.valid_period) 
            and lower(mr2.valid_period) < upper(ma.valid_period)
    )

union

select ma.machine_id, tstzrange(upper(mr.valid_period), lower(mr2.valid_period)) as valid_period
from machine_assignments ma
join machine_repairs mr using (machine_id)
join machine_repairs mr2 using (machine_id)
where zone_id = $1
    and upper(mr.valid_period) < lower(mr2.valid_period)  
    and int8range(upper(mr.valid_period), lower(mr2.valid_period)) <@ ma.valid_period  
    and not exists (
        select 1 
        from machine_repairs mr3
        where mr3.machine_id = ma.machine_id 
            and upper(mr.valid_period) < upper(mr3.valid_period) 
            and lower(mr3.valid_period) < lower(mr2.valid_period)
    )

union

select ma.machine_id, valid_period
from machine_assignments ma
where zone_id = $1
    and not exists (
        select 1 
        from machine_repairs mr
        where mr.machine_id = ma.machine_id 
            and lower(ma.valid_period) < upper(mr.valid_period) 
            and lower(mr.valid_period) < upper(ma.valid_period)
    )

order by machine_id, valid_period;    

-- Sequenced queries - union
select * 
from machine_assignments
where zone_id = 1
union all
select * 
from machine_assignments
where zone_id = 2;

-- Current modifications - insert
insert into machine_assignments (machine_id, project_id, valid_period) 
values (100, 1, tstzrange(now(), 'infinity'));

-- Current modifications - delete
begin;
update machine_assignments 
set valid_period = tstzrange(lower(valid_period), now()) 
where now() <@ valid_period and project_id = 1;

delete from machine_assignments 
where now() < lower(valid_period) and project_id = 1;
commit;

-- Current modifications - update, no future rows
begin;
update machine_assignments
set valid_period = tstzrange(lower(valid_period), now())
where upper(valid_period) = 'infinity' and machine_id = 100 ;

insert into machine_assignments (machine_id, project_id, valid_period) 
values (100, 2, tstzrange(now(), 'infinity'));
commit;

-- Current modifications - update, general case
begin;
insert into machine_assignments (machine_id, project_id, valid_period) 
select machine_id, 2 as project_id, tstzrange(now(), upper(valid_period))
from machine_assignments 
where now() <@ valid_period and machine_id = 100;

update machine_assignments
set valid_period = tstzrange(lower(valid_period), now())
where lower(valid_period) < now() and upper(valid_period) > now() and machine_id = 100;

update machine_assignments 
set project_id = 2 
where now() < lower(valid_period) and machine_id = 100;
commit;

-- Sequenced modifications - deletion
begin;
insert into machine_assignments (machine_id, project_id, valid_period) 
select machine_id, project_id, tstzrange('2017-03-01 0:0:0', upper(valid_period))
from machine_assignments 
where lower(valid_period) < timestamptz '2017-03-01 0:0:0' and upper(valid_period) > timestamptz '2017-03-31 0:0:0' and machine_id = 100;

update machine_assignments
set valid_period = tstzrange(lower(valid_period), '2017-03-01 0:0:0')
where lower(valid_period) < '2017-03-01 0:0:0' and upper(valid_period) >= '2017-03-01 0:0:0' and machine_id = 100;

update machine_assignments
set valid_period = tstzrange('2017-04-01 0:0:0', upper(valid_period))
where lower(valid_period) < '2017-04-01 0:0:0' and upper(valid_period) >= '2017-04-01 0:0:0' and machine_id = 100;

delete from machine_assignments 
where lower(valid_period) >= timestamptz '2017-03-01 0:0:0' and upper(valid_period) <= timestamp '2017-04-01 0:0:0' and machine_id = 100;
commit;

-- Sequenced modifications - update
begin;
insert into machine_assignments (machine_id, project_id, valid_period)
select machine_id, project_id, tstzrange(lower(valid_period), timestamptz '2017-03-01 0:0:0')
from machine_assignments 
where lower(valid_period) < timestamptz '2017-03-01 0:0:0' and upper(valid_period) > timestamptz '2017-03-01 0:0:0'
and machine_id = 100;

insert into machine_assignments (machine_id, project_id, valid_period)
select machine_id, project_id, tstzrange(timestamptz '2017-04-01 0:0:0', upper(valid_period))
from machine_assignments 
where lower(valid_period) < timestamptz '2017-04-01 0:0:0' and upper(valid_period) > timestamptz '2017-04-01 0:0:0' and machine_id = 100;

update machine_assignments 
set project_id = 2
where lower(valid_period) < timestamptz '2017-04-01 0:0:0' and upper(valid_period) > timestamptz '2017-03-01 0:0:0' and machine_id = 100;

update machine_assignments
set valid_period = tstzrange('2017-03-01 0:0:0', upper(valid_period))
where lower(valid_period) < timestamptz '2017-03-01 0:0:0' and upper(valid_period) > timestamptz '2017-03-01 0:0:0' and machine_id = 100;

update machine_assignments
set valid_period = tstzrange(lower(valid_period), '2017-04-01 0:0:0')
where lower(valid_period) < timestamptz '2017-04-01 0:0:0' and upper(valid_period) > timestamptz '2017-04-01 0:0:0' and machine_id = 100;
commit;

-- Performance considerations
create index zones_trans_period_index on zones 
using spgist(trans_period);

create index zones_curr_trans_period_index on zones 
using gist(trans_period);
where upper(trans_period) = 'infinity';

alter table zones 
cluster on zones_zone_id_trans_period_key;
-- Calculating an MD5 hash

select md5(json_agg(z)::text) 
from (
    select zone_id, 
        to_char(lower(trans_period), 'YYYY-MM-DD HH24:MI:SSZ') as updated_at 
    from zones
    where upper(trans_period) = 'infinity'
) z;


-- Preventing an error on duplicate inserts
insert into zones (zone_id, project_id) 
values ($1, $2) 
on conflict 
    on constraint zones_zone_id_trans_period_excl do nothing;
    
    
-- Delaying execution
select *
from zones, pg_sleep(1);

select *
from zones, pg_sleep_for('1 minute');

select *
from zones, pg_sleep_until(now() + '1 minute');


-- Generating test data
select setseed(random());
select timestamp '2017-05-01 0:0:0' + random() * interval '1 day' 
from generate_series(1, 20);

select date '2017-01-01' + floor(random() * (date '2017-02-01' - date '2017-01-01'))::int 
from generate_series(1, 10);
-- Configuration options

set datestyle = sql,dmy;
select date '2017-01-16'; 
select date '16-01-2017'; 

set datestyle = sql,mdy;
select date '01-16-2017';

set datestyle = sql,ymd;
select date '2017-01-16';

set intervalstyle = postgres;
select interval '1 year 2 months 3 days 12:34:56';

set intervalstyle = postgres_verbose;
select interval '1 year 2 months 3 days 12:34:56';

set intervalstyle = sql_standard;
select interval '1 year 2 months 3 days 12:34:56';

set intervalstyle = iso_8601;
select interval '1 year 2 months 3 days 12:34:56';

set intervalstyle = sql_standard;
select interval '-1 year 2 months 3 days 12:34:56';
set intervalstyle = postgres;
select interval '-1 year 2 months 3 days 12:34:56';