Thread: Forcing filter/join order?

Forcing filter/join order?

From
Josh Berkus
Date:
Folks,

Have an interesting issue with a complex query, where apparently I need to
twist the query planner's arm, and am looking for advice on how to do so.

The situation:  I have a table, events, with about 300,000 records.
It does an outer join to a second table, cases, with about 150,000 records.

A very simplified version query would be like

SELECT *
FROM events LEFT OUTER JOIN cases ON events.case_id = cases.case_id
WHERE events.event_date BETWEEN 'date1' AND 'date2'

This join is very expensive, as you can imagine.   Yet I can't seem to force
the query planner to apply the filter conditions to the events table *before*
attempting to join it to cases.  Here's the crucial explain lines:

                                 ->  Merge Left Join  (cost=0.00..11880.82
rows=15879 width=213) (actual time=5.777..901.899 rows=648 loops=1)
                                       Merge Cond: ("outer".case_id =
"inner".case_id)
                                       Join Filter: (("outer".link_type)::text
= 'case'::text)
                                       ->  Index Scan using idx_event_ends on
events  (cost=0.00..4546.15 rows=15879 width=80
) (actual time=4.144..333.769 rows=648 loops=1)
                                             Filter: ((status <> 0) AND
((event_date + duration) >= '2004-02-18 00:00:00'::timestamp without time
zone) AND (event_date <= '2004-03-05 23:59:00'::timestamp without time zone))
                                       ->  Index Scan using cases_pkey on
cases  (cost=0.00..6802.78 rows=117478 width=137) (
actual time=0.139..402.363 rows=116835 loops=1)

As you can see, part of the problem is a pretty drastic (20x) mis-estimation
of the selectivity of the date limits on events -- and results in 90% of the
execution time of my query on this one join.  I've tried raising the
statistics on event_date, duration, and case_id (to 1000), but this doesn't
seem to affect the estimate or the query plan.

In the above test, idx_event_ends indexes (case_id, status, event_date,
(event_date + duration)), but as you can see the planner uses only the first
column.  This was an attempt to circumvent the planner's tendency to
completely ignoring any index on (event_date, (event_date + duration))  --
even though that index is the most selective combination on the events table.

Is there anything I can do to force the query planner to filter on events
before joining cases, other than waiting for version 7.5?

--
-Josh Berkus
 Aglio Database Solutions
 San Francisco


Re: Forcing filter/join order?

From
"Peter Darley"
Date:
Josh,
    I'm sure the big brains have a better suggestion, but in the mean time
could you do something as simple as:

SELECT *
FROM (select * from events where event_date BETWEEN 'date1' AND 'date2') e
LEFT OUTER JOIN cases ON e.case_id = cases.case_id;

Thanks,
Peter Darley

-----Original Message-----
From: pgsql-performance-owner@postgresql.org
[mailto:pgsql-performance-owner@postgresql.org]On Behalf Of Josh Berkus
Sent: Wednesday, February 18, 2004 4:11 PM
To: pgsql-performance
Subject: [PERFORM] Forcing filter/join order?


Folks,

Have an interesting issue with a complex query, where apparently I need to
twist the query planner's arm, and am looking for advice on how to do so.

The situation:  I have a table, events, with about 300,000 records.
It does an outer join to a second table, cases, with about 150,000 records.

A very simplified version query would be like

SELECT *
FROM events LEFT OUTER JOIN cases ON events.case_id = cases.case_id
WHERE events.event_date BETWEEN 'date1' AND 'date2'

This join is very expensive, as you can imagine.   Yet I can't seem to force
the query planner to apply the filter conditions to the events table
*before*
attempting to join it to cases.  Here's the crucial explain lines:

                                 ->  Merge Left Join  (cost=0.00..11880.82
rows=15879 width=213) (actual time=5.777..901.899 rows=648 loops=1)
                                       Merge Cond: ("outer".case_id =
"inner".case_id)
                                       Join Filter:
(("outer".link_type)::text
= 'case'::text)
                                       ->  Index Scan using idx_event_ends
on
events  (cost=0.00..4546.15 rows=15879 width=80
) (actual time=4.144..333.769 rows=648 loops=1)
                                             Filter: ((status <> 0) AND
((event_date + duration) >= '2004-02-18 00:00:00'::timestamp without time
zone) AND (event_date <= '2004-03-05 23:59:00'::timestamp without time
zone))
                                       ->  Index Scan using cases_pkey on
cases  (cost=0.00..6802.78 rows=117478 width=137) (
actual time=0.139..402.363 rows=116835 loops=1)

As you can see, part of the problem is a pretty drastic (20x) mis-estimation
of the selectivity of the date limits on events -- and results in 90% of the
execution time of my query on this one join.  I've tried raising the
statistics on event_date, duration, and case_id (to 1000), but this doesn't
seem to affect the estimate or the query plan.

In the above test, idx_event_ends indexes (case_id, status, event_date,
(event_date + duration)), but as you can see the planner uses only the first
column.  This was an attempt to circumvent the planner's tendency to
completely ignoring any index on (event_date, (event_date + duration))  --
even though that index is the most selective combination on the events
table.

Is there anything I can do to force the query planner to filter on events
before joining cases, other than waiting for version 7.5?

--
-Josh Berkus
 Aglio Database Solutions
 San Francisco


---------------------------(end of broadcast)---------------------------
TIP 7: don't forget to increase your free space map settings


Re: Forcing filter/join order?

From
Josh Berkus
Date:
Folks,

Hmmm posted too soon.  Figured out the problem:

The planner can't, or doesn't want to, use an index on (event_date,
(event_date + duration)) where the first column is an ascending sort and the
second a descending sort.    So I've coded a workaround that's quite
inelegant but does get the correct results in 0.3 seconds (as opposed to the
2.2 seconds taken by the example plan).

Is this the sort of thing which is ever likely to get fixed, or just a
fundamental limitation of index algorithms?   Would using a non B-Tree index
allow me to work around this?

--
-Josh Berkus
 Aglio Database Solutions
 San Francisco


Re: Forcing filter/join order?

From
Josh Berkus
Date:
Peter,

>     I'm sure the big brains have a better suggestion, but in the mean time
> could you do something as simple as:
>
> SELECT *
> FROM (select * from events where event_date BETWEEN 'date1' AND 'date2') e
> LEFT OUTER JOIN cases ON e.case_id = cases.case_id;

Thanks, but that doens't work; the planner will collapse the subquery into the
main query, which most of the time is the right thing to do.

--
-Josh Berkus
 Aglio Database Solutions
 San Francisco


Re: Forcing filter/join order?

From
Stephan Szabo
Date:
On Wed, 18 Feb 2004, Josh Berkus wrote:

> The planner can't, or doesn't want to, use an index on (event_date,
> (event_date + duration)) where the first column is an ascending sort and the
> second a descending sort.    So I've coded a workaround that's quite
> inelegant but does get the correct results in 0.3 seconds (as opposed to the
> 2.2 seconds taken by the example plan).

Can you give more information?  I know that I'm not exactly certain what
the situation is from the above and the original query/explain piece.

Re: Forcing filter/join order?

From
Josh Berkus
Date:
Stephan,

> Can you give more information?  I know that I'm not exactly certain what
> the situation is from the above and the original query/explain piece.
>

Believe me, if I posted the query it wouldn't help.    Heck, I'd have trouble
following it without my notes.

a simplifed version:

SELECT events.*, cases.case_name
FROM events LEFT OUTER JOIN cases ON events.case_id = cases.case_id
WHERE (event_date >= '2004-03-05' OR (event_date + duration) <= '2004-02-18')
    AND events.status <> 0;

... this is to get me all vaild events which overlap with the range
'2004-02-18' to '2004-03-05'.

I had thought, in 7.4, that adding an index on (event_date, (event_date +
duration)) would improve the execution of this query.   It doesn't,
presumably because the multi-column index can't be used for both ascending
and descending sorts at the same time, and event_date >= '2004-03-05' isn't
selective enough.

There was a workaround for this posted on hackers about a year ago as I
recally, that involved creating custom operators for indexing.  Too much
trouble when there's a hackish workaround (due to the fact that events have
to be less than a month long).

--
-Josh Berkus
 Aglio Database Solutions
 San Francisco


Re: Forcing filter/join order?

From
Stephan Szabo
Date:
On Wed, 18 Feb 2004, Josh Berkus wrote:

> Stephan,
>
> > Can you give more information?  I know that I'm not exactly certain what
> > the situation is from the above and the original query/explain piece.
> >
>
> Believe me, if I posted the query it wouldn't help.    Heck, I'd have trouble
> following it without my notes.
>
> a simplifed version:
>
> SELECT events.*, cases.case_name
> FROM events LEFT OUTER JOIN cases ON events.case_id = cases.case_id
> WHERE (event_date >= '2004-03-05' OR (event_date + duration) <= '2004-02-18')
>     AND events.status <> 0;
>
> ... this is to get me all vaild events which overlap with the range
> '2004-02-18' to '2004-03-05'.
>
> I had thought, in 7.4, that adding an index on (event_date, (event_date +
> duration)) would improve the execution of this query.   It doesn't,
> presumably because the multi-column index can't be used for both ascending
> and descending sorts at the same time, and event_date >= '2004-03-05' isn't
> selective enough.

I don't think the direction issue is the problem in the above.  I think
the problem is that given a condition like:
  a>value or b<othervalue
an index on (a,b) doesn't appear to be considered probably since the
b<othervalue wouldn't be indexable by that index and you can't use the
a>value alone since that'd do the wrong thing.

Testing on a two column table, I see behavior like the following (with
seqscan off)

sszabo=# create table q2(a int, b int);
CREATE TABLE
sszabo=# create index q2ind on q2(a,b);
CREATE INDEX
sszabo=# set enable_seqscan=off;
SET
sszabo=# explain select * from q2 where a>3 and b<5;
                            QUERY PLAN
-------------------------------------------------------------------
 Index Scan using q2ind on q2  (cost=0.00..42.79 rows=112 width=8)
   Index Cond: ((a > 3) AND (b < 5))
(2 rows)

sszabo=# explain select * from q2 where a>3 or b<5;
                             QUERY PLAN
--------------------------------------------------------------------
 Seq Scan on q2  (cost=100000000.00..100000025.00 rows=556 width=8)
   Filter: ((a > 3) OR (b < 5))
(2 rows)

sszabo=# create index q2ind2 on q2(b);
CREATE INDEX
sszabo=# explain select * from q2 where a>3 or b<5;
                                QUERY PLAN
---------------------------------------------------------------------------
 Index Scan using q2ind, q2ind2 on q2  (cost=0.00..92.68 rows=556 width=8)
   Index Cond: ((a > 3) OR (b < 5))
(2 rows)



Re: Forcing filter/join order?

From
Tom Lane
Date:
Josh Berkus <josh@agliodbs.com> writes:
> SELECT events.*, cases.case_name
> FROM events LEFT OUTER JOIN cases ON events.case_id = cases.case_id
> WHERE (event_date >= '2004-03-05' OR (event_date + duration) <= '2004-02-18')
>     AND events.status <> 0;

> ... this is to get me all vaild events which overlap with the range
> '2004-02-18' to '2004-03-05'.

Did you mean events that *don't* overlap with the range?  Seems like
what you say you want should be expressed as

event_date <= 'end-date' AND (event_date + duration) >= 'start-date'

This assumes duration is never negative of course.

I think you could make this btree-indexable by negating the second
clause.  Imagine

create index evi on events (event_date, (-(event_date+duration)))

and then transforming the query to

event_date <= 'end-date' AND -(event_date + duration) <= -'start-date'

but that doesn't quite work because there's no unary minus for date or
timestamp types.  Is this too ugly for you?

create index evi on events (event_date, ('ref-date'-event_date-duration))

event_date <= 'end-date'
AND ('ref-date'-event_date-duration) <= 'ref-date'-'start-date'

where 'ref-date' is any convenient fixed reference date, say 1-1-2000.

Now, what this will look like to the planner is a one-sided two-column
restriction, and I'm not certain that the planner will assign a
sufficiently small selectivity estimate.  But in theory it could work.

            regards, tom lane

Re: Forcing filter/join order?

From
Josh Berkus
Date:
Tom,

First off, you are correct, I swapped the dates when typing the simplified
query into e-mail.

> create index evi on events (event_date, ('ref-date'-event_date-duration))
>
> event_date <= 'end-date'
> AND ('ref-date'-event_date-duration) <= 'ref-date'-'start-date'
>
> where 'ref-date' is any convenient fixed reference date, say 1-1-2000.
>
> Now, what this will look like to the planner is a one-sided two-column
> restriction, and I'm not certain that the planner will assign a
> sufficiently small selectivity estimate.  But in theory it could work.

Interesting idea.   I'll try it just to see if it works when I have a chance.

In the meantime, for production, I'll stick with the hackish solution I was
using under 7.2.

Knowing that events are never more than one month long for this application, I
can do:

"WHERE event.event_date >= (begin_date - '1 month) AND event.event_date <=
end_date"

... which works because I have a child table which has event information by
day:

AND events.event_id IN (SELECT event_id FROM event_day
    WHERE calendar_day BETWEEN begin_date AND end_date);

Note that this subselect isn't sufficent on its own, because once again the
query planner is unable to correctly estimate the selectivity of the
subselect.   It needs the "help" of the filter against events.event_date.

This is the workaround I was using with 7.2.   I had just hoped that some of
the improvements that Tom has made over the last two versions would cure the
problem, but no dice.

--
-Josh Berkus
 Aglio Database Solutions
 San Francisco


Re: Forcing filter/join order?

From
Tom Lane
Date:
Josh Berkus <josh@agliodbs.com> writes:
> Knowing that events are never more than one month long for this
> application, I can do:

> "WHERE event.event_date >= (begin_date - '1 month) AND event.event_date <=
> end_date"

> ... which works because I have a child table which has event information by
> day:

Uh, why do you need the child table?  Seems like the correct incantation
given an assumption about maximum duration is

event_date <= 'end-date' AND (event_date + duration) >= 'start-date'
AND event_date >= 'start-date' - 'max-duration'

The last clause is redundant with the one involving the duration field,
but it provides a lower bound for the index scan on event_date.  The
only index you really need here is one on event_date, but possibly one
on (event_date, (event_date + duration)) would be marginally faster.

            regards, tom lane

Re: Forcing filter/join order?

From
Josh Berkus
Date:
Tom,

> Uh, why do you need the child table?

Because there's linked information which needs to be kept by day for multi-day
events.   Also, it makes calendar reports easier, where one wants each day of
a multi-day event to appear on each day of the calendar.

>Seems like the correct incantation
> given an assumption about maximum duration is
>
> event_date <= 'end-date' AND (event_date + duration) >= 'start-date'
> AND event_date >= 'start-date' - 'max-duration'

Hmmmm ... so the same as what I have, only with the extra condition for
event_date+duration and without the IN clause.   I'll try it, thanks!

--
-Josh Berkus
 Aglio Database Solutions
 San Francisco


Re: Forcing filter/join order?

From
Josh Berkus
Date:
Tom,

> event_date <= 'end-date' AND (event_date + duration) >= 'start-date'
> AND event_date >= 'start-date' - 'max-duration'

Great suggestion!   We're down to 160ms, from about 370ms with my subselect
workaround.   Thanks!

--
-Josh Berkus
 Aglio Database Solutions
 San Francisco