table (simplified)

                                        Table "public.events"
      Column      |            Type             |                       Modifiers                        
 id               | integer                     | not null default nextval('events_id_seq'::regclass)
 duration         | integer                     | not null
 start_at         | timestamp without time zone | 
    "events_pkey" PRIMARY KEY, btree (id)
    "my_idx" gist (tsrange(start_at, end_at(events.*), '[)'::text))


CREATE FUNCTION end_at(rec events)
  RETURNS timestamp without time zone
AS $$
  SELECT $1.start_at + ($1.duration * ('00:00:01'::interval));

what I am already doing successfully

The index is used for queries like this:

-- check if current time is within the start and end times
-- of event
where localtimestamp <@ tsrange(start_at, events.end_at, '[)')

And it works well.

what I want to do

I want to query for events where the current time is after they have ended. Ways I know of how to do this:

  • where tsrange(localtimestamp, localtimestamp, '[]') >> tsrange(start_at, events.end_at, '[)'). I'm pretty sure this is the semantics I want, and explain analyze says it's using the index, but it's a bit ugly and I'm wondering if there's a better way to express this (and also am vaguely uncertain it's the semantics I want, as I am new to ranges).
  • where localtimestamp > upper(tsrange(start_at, events.end_at, '[)')) + a btree index on upper(tsrange(start_at, events.end_at, '[)')). This will work well, but requires keeping another index around.
  • where localtimestamp > events.end_at. + a btree index on events.end_at. Same situation as above.

Is there a more elegant (or correct) way to achieve the first bullet point above?

Any other ideas for how to go about this?


2 Answers 2


Postgres doesn't support any operators for this. As pointed out by @evan-carroll, it probably could and should.

So, the best solution is

where tsrange(localtimestamp, localtimestamp, '[]') >> tsrange(start_at, events.end_at, '[)')

I want to query for events where the current time is after they have ended. Ways I know of how to do this:

You can see the GiST operators here. You can see the list in range_ops,

&& &> &< >> << <@ -|- = @> @>

As you indicated the one you probably want is <<, >>. I would use <<.

Is it ugly? Yes. The range type is great if you're working with ranges on both sides, here you're not. You're not even storing the range type on the table. I would recommend you do that.

I'm not sure this index condition even works,

"my_idx" gist (tsrange(start_at, end_at(events.*), '[)'::text))

Have you tested that? I mean, PostgreSQL can verify the start_at works, my assumption is that it's not smart enough to work with end_at with events.* (what if one of the values (columns) used in the end_at is changed from table creation to call? Even if never happens, how would PostgreSQL know that). I would store the tsrange on the table and drop all those associated columns.

Ignore the below, looking into it.

Also you can consider making it shorter by not constructing the rhs range.

SELECT tsrange('yesterday', 'tomorrow') @> timestamp without time zone 'now';
SELECT tsrange('yesterday', 'tomorrow') @> 'now'::timestamp without time zone;

Or the like if you prefer that format. Or the following, which seems like an anti-pattern.

SELECT tsrange('yesterday', 'tomorrow') @> '[now,now]';

Or you can use tstzrange (which is probably an all around better idea),

SELECT tstzrange('yesterday', 'tomorrow') @> now();

  • Ahh, I see they don't define those >> or << for tstzrange >> timestamp with time zone That's likely a bug. Commented Jan 11, 2018 at 21:14
  • i'm talking about your examples with @> -- that operator tests for inclusion. if it returns false, we don't know if the tested element comes before or after the range. my queries need to know this. Commented Jan 11, 2018 at 21:15
  • 1
    Right, I'm telling you that you should be able to replace it with << or >> that you can't makes me think it is a bug. I just wrote you pseduocode to show that range oppr item should work on GiST but it doesn't work for all the operators, and I think that's unintended. Commented Jan 11, 2018 at 21:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.