Re: [External] Join queries slow with predicate, limit, and ordering - Mailing list pgsql-performance

From Jeff Janes
Subject Re: [External] Join queries slow with predicate, limit, and ordering
Date
Msg-id CAMkU=1z3HR8L=h65DR8=5iAgKjnBSs=LnJccLz9VMt=3oMDpQQ@mail.gmail.com
Whole thread Raw
In response to [External] Join queries slow with predicate, limit, and ordering  (Aufar Gilbran <aufar.gilbran@grab.com>)
Responses Re: [External] Join queries slow with predicate, limit, and ordering  (Aufar Gilbran <aufar.gilbran@grab.com>)
List pgsql-performance
On Mon, Dec 2, 2019 at 8:29 AM Aufar Gilbran <aufar.gilbran@grab.com> wrote:
Hello,

I'm trying to figure out how to optimise 3-table (many-to-many relation) joins
with predicate, limit, and ordering, where one of the tables returns at most one
row.

This is the query that I have right now:

SELECT entity.id
FROM (
    SELECT entity_tag.entity_id
    FROM tag
    JOIN entity_tag ON tag.id = entity_tag.tag_id
    WHERE tag.key = 'status'
      AND tag.value = 'SUCCEEDED'
) matched
JOIN entity ON matched.entity_id = entity.id
WHERE entity.type = 'execution'
ORDER BY entity.id DESC
LIMIT 10;

What happens if you set enable_sort to off before running it?
 
        ->  Nested Loop  (cost=1.28..723.38 rows=1 width=4) (actual
time=0.153..5590.717 rows=89222 loops=1)

It thinks it will find 1 row, and actually finds 89,222.  I don't know exactly why that would be, I suppose tag_id has an extremely skewed distribution.  But yeah, that is going to cause some problems.  For one thing, if there was actually just one qualifying row, then it wouldn't get to stop early, as the LIMIT would never be satisfied.  So it thinks that if it choose to walk the index backwards, it would have to walk the **entire** index.


                    ->  Index Only Scan using entity_tag_tag_id_entity_id_idx on public.entity_tag (cost=0.43..711.53 rows=201 width=16) (actual time=0.035..756.829 rows=89222 loops=1)
                          Heap Fetches: 89222

You should vacuum this table.  Doing that (and only that) probably won't make a great deal of difference to this particular query, but still, it will help some.  And might help other ones you haven't noticed yet as well.
 

Both tag_key_value_key and entity_tag_tag_id_entity_id_idx is a UNIQUE
constraint on tag(key,value) and entity_tag(tag_id, entity_id) respectively.

It seems to me that PostgreSQL runs the nested loop against all of the 90K
records because it wants to sort the result before limiting the result.

It doesn't **know** there are going to be 90000 records.  It cannot plan queries based on knowledge it doesn't possess.
 
It
doesn't take into account of the UNIQUE constraint imposed on the table and
thinks that the join being done inside the subquery will change the ordering of
entity_id returned by the subquery, thus prompting the sort.

This seems like rather adventurous speculation.  It does the sort because the horrible estimation makes it think it will be faster that way, not because it thinks it is the only possible way.  Of you set enable_sort = off and it still does a sort, then you know it thinks there is no other way.

 

I believe with how the index sorted, it should be able to just scan the index
backwards because at most only one tag_id will be returned. When I tried
changing the predicate here to filter by ID with the following query:

-- This runs very fast
SELECT entity.id
FROM (
    SELECT entity_tag.entity_id
    FROM tag
    JOIN entity_tag ON tag.id = entity_tag.tag_id
    WHERE tag.id = 24
) matched
JOIN entity ON matched.entity_id = entity.id
WHERE entity.type = 'execution'
ORDER BY entity.id DESC
LIMIT 10;

With this query, it can use the join condition to transfer the knowledge of tag.id=24 to become entity_tag.tag_id=24, and then look up stats on entity_tag.tag_id for the value 24.  When you specify the single row of tag indirectly, it can't do that as it doesn't know what specific value of tag.id is going to be the one it finds (until after the query is done being planned and starts executing, at which point it is too late).  But the row with id=24 doesn't seem to be the same one with "tag.key = 'status' AND tag.value = 'SUCCEEDED'", so you have basically changed the query entirely on us.
 
If you replanned this query with ORDER BY entity.id+0 DESC, (and with the true value of tag_id) that might give you some more insight into the hidden "thought process" behind the planner.

Cheers,

Jeff

pgsql-performance by date:

Previous
From: Eugene Podshivalov
Date:
Subject: Re: Considerable performance downgrade of v11 and 12 on Windows
Next
From: Aufar Gilbran
Date:
Subject: Re: [External] Join queries slow with predicate, limit, and ordering