I have a table of events that each take place during a time range, and I'm trying to create a template for a query which can find events that take place within a specified time period of correlated events which can in turn be specified to take place within a time period of other events. I also need to be able apply a boolean expression to these constraints.
I figure the best way to do this is to use nested exist statements, one for each event in the graph of events. This at least works better than doing inner joins with DISTINCT
.
I've read that the Postgres optimizer is quite smart, so rather than trying to parse and optimize the boolean expression myself I was hoping to be able to throw all the constraints into the bottom of the nested queries and have the optimizer do most of the heavy lifting. While simple, disjoint, logic would be easy to parse more complex logic would essentially require writing a mini optimizer or arbitrarily choosing between different logical forms and the corresponding queries.
Unfortunately, the optimizer doesn't seem to optimize in the way I hoped it would. Even in the simplest form with straight AND
statements and relatively few conditions, the conditions aren't getting pulled up.
Here is an example of how i'm generating the query and the explain. The CTE provided a substantial performance boost for this particular query, and as far as I know with this type of query the index on the timerange column is unlikely to be used anyways:
EXPLAIN ANALYZE
WITH event AS (
SELECT *,
int4range(
lower(event.tickrange),
upper(event.tickrange) + 500
) AS newrange
FROM event
)
SELECT event_1.id AS event_1_id
FROM event AS event_1
WHERE EXISTS (
SELECT 1
FROM event AS event_2
WHERE EXISTS (
SELECT 1
FROM event AS event_3
WHERE event_1.a_id = 189
AND event_3.tickrange <@ event_2.newrange
AND event_1.id != event_3.id
AND event_2.id != event_3.id
AND event_2.tickrange <@ event_1.newrange
AND event_1.id != event_2.id
)
)
Nested Loop Semi Join (cost=117.64..51683659.30 rows=2141 width=4) (actual time=0.075..11060.537 rows=158 loops=1)
Join Filter: (SubPlan 2)
Rows Removed by Join Filter: 17782972
CTE event
-> Seq Scan on event (cost=0.00..117.64 rows=4282 width=30) (actual time=0.033..9.446 rows=4282 loops=1)
-> CTE Scan on event event_1 (cost=0.00..85.64 rows=4282 width=40) (actual time=0.039..0.992 rows=4282 loops=1)
-> CTE Scan on event event_2 (cost=0.00..85.64 rows=4282 width=68) (actual time=0.000..0.541 rows=4153 loops=4282)
SubPlan 2
-> Result (cost=0.01..117.76 rows=21 width=0) (actual time=0.000..0.000 rows=0 loops=17783130)
One-Time Filter: ((event_1.a_id = 189) AND (event_2.tickrange <@ event_1.newrange) AND (event_1.id <> event_2.id))
-> CTE Scan on event event_3 (cost=0.01..117.76 rows=21 width=0) (actual time=0.173..0.173 rows=1 loops=160)
Filter: ((tickrange <@ event_2.newrange) AND (event_1.id <> id) AND (event_2.id <> id))
Rows Removed by Filter: 830
Planning time: 0.322 ms
Execution time: 11060.848 ms
And here's the same query properly specified with the WHERE
conditions where they should be. You'll notice that along with a massive performance difference, the filters are applied sequentially as they should be.
EXPLAIN ANALYZE
WITH event AS (
SELECT *,
int4range(
lower(event.tickrange),
upper(event.tickrange) + 500
) AS newrange
FROM event
)
SELECT event_1.id AS event_1_id
FROM event AS event_1
WHERE event_1.a_id = 189
AND EXISTS (
SELECT 1
FROM event AS event_2
WHERE EXISTS (
SELECT 1
FROM event AS event_3
WHERE (event_3.tickrange <@ event_2.newrange)
AND event_1.id != event_3.id
AND event_2.id != event_3.id
)
AND (event_2.tickrange <@ event_1.newrange)
AND event_1.id != event_2.id
)
Nested Loop Semi Join (cost=117.64..506774.59 rows=1 width=4) (actual time=0.070..111.936 rows=158 loops=1)
Join Filter: ((event_2.tickrange <@ event_1.newrange) AND (event_1.id <> event_2.id) AND (SubPlan 2))
Rows Removed by Join Filter: 136850
CTE event
-> Seq Scan on event (cost=0.00..117.64 rows=4282 width=30) (actual time=0.032..6.944 rows=4282 loops=1)
-> CTE Scan on event event_1 (cost=0.00..96.34 rows=21 width=36) (actual time=0.040..0.958 rows=161 loops=1)
Filter: (a_id = 189)
Rows Removed by Filter: 4121
-> CTE Scan on event event_2 (cost=0.00..85.64 rows=4282 width=68) (actual time=0.001..0.139 rows=851 loops=161)
SubPlan 2
-> CTE Scan on event event_3 (cost=0.00..117.76 rows=21 width=0) (actual time=0.290..0.290 rows=1 loops=160)
Filter: ((tickrange <@ event_2.newrange) AND (event_1.id <> id) AND (event_2.id <> id))
Rows Removed by Filter: 830
Planning time: 0.446 ms
Execution time: 112.160 ms
So I have two (if I may be so bold) questions:
- Is there a better way to write the query other than nested exist statements?
- Is there a way I can convince the optimizer to do my work, i.e. pull up conditions so that the filters are applied as soon as possible.
I'm using Postgres 9.5
Edit: My admittedly long post could use some clarification it appears so here's the summary.
- My goal is to create the query In such a way that I don't have to parse the logic and apply it at different levels of the statements. I'm thinking there might be something I'm not doing that would get the optimizer to do this.
- The second query is a version of the first where I have parsed the logic and put the statements at the correct level. I showed this as a comparison for how the optimizer handles a more proper query.
- The 500 added to the
tickrange
in my example is representative of the type of query I would like to run. Its value will change across queries and even across different events within the same query.