Have you considered PG 9.2's range type instead of two separate timestamp fields?
http://www.postgresql.org/docs/9.2/static/rangetypes.html
Something like:
CREATE TABLE availability (
zone_name varchar, nodeid int, nodelabel varchar, during tsrange
);
INSERT INTO availability
VALUES (zone1, 3, 'srv1', '[2013-01-01 14:30, 2013-01-01 15:30)');
Unless I'm mistaking, you'd then be able to work with unions, intersections and such, which should make your work simpler. There are likely a few aggregate functions I'm unfamiliar with that cater to the latter, too.
If needed, additionally look into with statements and window functions for more complex queries:
http://www.postgresql.org/docs/9.2/static/tutorial-window.html
http://www.postgresql.org/docs/9.2/static/functions-window.html
Some testing reveals that sum() doesn't work with tsrange types.
That being said, the sql schema used in the follow-up queries:
drop table if exists nodes;
create table nodes (
zone int not null,
node int not null,
uptime tsrange
);
-- this requires the btree_gist extension:
-- alter table nodes add exclude using gist (uptime with &&, zone with =, node with =);
The data (slight variation from your sample):
insert into nodes values
(1, 1, '[2013-02-20 00:00:00, 2013-02-21 09:40:00)'),
(1, 1, '[2013-02-21 09:48:48, 2013-02-21 10:04:56)'),
(1, 1, '[2013-02-21 10:09:27, 2013-02-21 10:14:01)'),
(1, 1, '[2013-02-22 10:24:20, 2013-02-22 10:26:29)'),
(1, 1, '[2013-02-22 11:25:15, 2013-02-22 11:27:24)'),
(1, 1, '[2013-02-28 15:52:59, 2013-02-28 16:24:59)'),
(1, 1, '[2013-02-28 16:40:18, 2013-02-28 16:56:19)'),
(1, 1, '[2013-02-28 17:00:00, infinity)'),
(1, 2, '[2013-02-20 00:00:01, 2013-02-21 12:15:00)'),
(1, 2, '[2013-02-21 12:26:04, 2013-02-21 13:15:53)'),
(1, 2, '[2013-02-22 10:21:14, 2013-02-23 13:23:10)'),
(1, 2, '[2013-02-23 13:33:32, 2013-02-24 13:35:23)'),
(1, 2, '[2013-02-25 14:25:51, 2013-02-26 15:17:25)'),
(1, 2, '[2013-02-28 15:43:01, 2013-02-28 18:49:56)'),
(2, 3, '[2013-02-20 00:00:01, 2013-02-22 09:01:00)'),
(2, 3, '[2013-02-22 10:19:13, 2013-02-22 17:23:59)'),
(2, 3, '[2013-02-28 16:13:48, 2013-02-28 16:54:27)');
Raw data in order (for clarity):
select *
from nodes
order by zone, uptime, node;
Yields:
zone | node | uptime
------+------+-----------------------------------------------
1 | 1 | ["2013-02-20 00:00:00","2013-02-21 09:40:00")
1 | 2 | ["2013-02-20 00:00:01","2013-02-21 12:15:00")
1 | 1 | ["2013-02-21 09:48:48","2013-02-21 10:04:56")
1 | 1 | ["2013-02-21 10:09:27","2013-02-21 10:14:01")
1 | 2 | ["2013-02-21 12:26:04","2013-02-21 13:15:53")
1 | 2 | ["2013-02-22 10:21:14","2013-02-23 13:23:10")
1 | 1 | ["2013-02-22 10:24:20","2013-02-22 10:26:29")
1 | 1 | ["2013-02-22 11:25:15","2013-02-22 11:27:24")
1 | 2 | ["2013-02-23 13:33:32","2013-02-24 13:35:23")
1 | 2 | ["2013-02-25 14:25:51","2013-02-26 15:17:25")
1 | 2 | ["2013-02-28 15:43:01","2013-02-28 18:49:56")
1 | 1 | ["2013-02-28 15:52:59","2013-02-28 16:24:59")
1 | 1 | ["2013-02-28 16:40:18","2013-02-28 16:56:19")
1 | 1 | ["2013-02-28 17:00:00",infinity)
2 | 3 | ["2013-02-20 00:00:01","2013-02-22 09:01:00")
2 | 3 | ["2013-02-22 10:19:13","2013-02-22 17:23:59")
2 | 3 | ["2013-02-28 16:13:48","2013-02-28 16:54:27")
(17 rows)
Nodes available @ 2013-02-21 09:20:00:
with upnodes as (
select zone, node, uptime
from nodes
where '2013-02-21 09:20:00'::timestamp <@ uptime
)
select *
from upnodes
order by zone, uptime, node;
Yields:
zone | node | uptime
------+------+-----------------------------------------------
1 | 1 | ["2013-02-20 00:00:00","2013-02-21 09:40:00")
1 | 2 | ["2013-02-20 00:00:01","2013-02-21 12:15:00")
2 | 3 | ["2013-02-20 00:00:01","2013-02-22 09:01:00")
(3 rows)
Nodes available from 2013-02-21 00:00:00 incl to 2013-02-24 00:00:00 excl:
with upnodes as (
select zone, node, uptime
from nodes
where '[2013-02-21 00:00:00, 2013-02-24 00:00:00)'::tsrange && uptime
)
select * from upnodes
order by zone, uptime, node;
Yields:
zone | node | uptime
------+------+-----------------------------------------------
1 | 1 | ["2013-02-20 00:00:00","2013-02-21 09:40:00")
1 | 2 | ["2013-02-20 00:00:01","2013-02-21 12:15:00")
1 | 1 | ["2013-02-21 09:48:48","2013-02-21 10:04:56")
1 | 1 | ["2013-02-21 10:09:27","2013-02-21 10:14:01")
1 | 2 | ["2013-02-21 12:26:04","2013-02-21 13:15:53")
1 | 2 | ["2013-02-22 10:21:14","2013-02-23 13:23:10")
1 | 1 | ["2013-02-22 10:24:20","2013-02-22 10:26:29")
1 | 1 | ["2013-02-22 11:25:15","2013-02-22 11:27:24")
1 | 2 | ["2013-02-23 13:33:32","2013-02-24 13:35:23")
2 | 3 | ["2013-02-20 00:00:01","2013-02-22 09:01:00")
2 | 3 | ["2013-02-22 10:19:13","2013-02-22 17:23:59")
(11 rows)
Zones available from 2013-02-21 00:00:00 incl to 2013-02-24 00:00:00 excl'
with upnodes as (
select zone, node, uptime
from nodes
where '[2013-02-21 00:00:00, 2013-02-24 00:00:00)'::tsrange && uptime
),
upzones_max as (
select u1.zone, tsrange(lower(u1.uptime), max(upper(u2.uptime))) as uptime
from upnodes as u1
join upnodes as u2 on u2.zone = u1.zone and u2.uptime && u1.uptime
group by u1.zone, lower(u1.uptime)
),
upzones as (
select u1.zone, tsrange(min(lower(u2.uptime)), upper(u1.uptime)) as uptime
from upzones_max as u1
join upzones_max as u2 on u2.zone = u1.zone and u2.uptime && u1.uptime
group by u1.zone, upper(u1.uptime)
)
select zone, uptime, upper(uptime) - lower(uptime) as duration
from upzones
order by zone, uptime;
Yields:
zone | uptime | duration
------+-----------------------------------------------+-----------------
1 | ["2013-02-20 00:00:00","2013-02-21 12:15:00") | 1 day 12:15:00
1 | ["2013-02-21 12:26:04","2013-02-21 13:15:53") | 00:49:49
1 | ["2013-02-22 10:21:14","2013-02-23 13:23:10") | 1 day 03:01:56
1 | ["2013-02-23 13:33:32","2013-02-24 13:35:23") | 1 day 00:01:51
2 | ["2013-02-20 00:00:01","2013-02-22 09:01:00") | 2 days 09:00:59
2 | ["2013-02-22 10:19:13","2013-02-22 17:23:59") | 07:04:46
(6 rows)
There might be a better way to write the latter query if you write (or find) a custom aggregate function that sums overlapping range types -- the non-trivial issue that I ran into was to isolate an adequate group by clause; I ended up settling with two nested group by clauses.
The queries could also be rewritten to accommodate your current schema, either by replacing the uptime field by an expression such as tsrange(start_date, end_date), or by writing a view that does so.