What is index do i need?

Viewed 92

I have problems with the performance of this query. If I remove Order by section all work well. But I really want it. I tried to use many indexes but have not any results. Can you help me pls?

SELECT *
     FROM "refuel_request" AS "refuel_request"
     LEFT OUTER JOIN "user" AS "user" ON "refuel_request"."user_id" = "user"."user_id"
     LEFT OUTER JOIN "bill_qr" AS "bill_qr" ON "refuel_request"."bill_qr_id" = "bill_qr"."bill_qr_id"
     LEFT OUTER JOIN "car" AS "order.car" ON "refuel_request"."car_id" = "order.car"."car_id" 
     LEFT OUTER JOIN "refuel_request_status" AS "refuel_request_status" ON "refuel_request"."refuel_request_status_id" = "refuel_request_status"."refuel_request_status_id"
     WHERE
      refuel_request."refuel_request_status_id" IN ( '1', '2', '3') 
     ORDER BY "refuel_request".created_at desc
     LIMIT 10 

There is explain of this query

EXPLAIN (ANALYZE, BUFFERS)

Primary Keys and/or Foreign Keys

  1. pk_refuel_request_id
  2. refuel_request_bill_qr_id_fkey
  3. refuel_request_user_id_fkey
4 Answers

All outer joind tables are 1:n related to refuel_request. This means your query is looking for the last ten created refuel requests with status 1 to 3.

You are outer joining the tables, because not every reful_request is related to a user, a bill_qr, a car, and a status. Or you outer join mistakenly. Anyway, none of the joins changes the number of retrieved rows; it's still one row per refuel request. In order to join the other tables' rows the DBMS just needs their primary key indexes. Nothing to worry about.

The only thing we must care about is finding the top reful_request rows for the statuses you are interested in as quickly as possible.

Use a partial index that only contains data for the statuses in question. The column you index is the created_at column, so as to get the top 10 immediately.

CREATE INDEX idx ON refuel_request (created_at DESC)
                 WHERE refuel_request_status_id IN (1, 2, 3);

Partial indexes are explained here: https://www.postgresql.org/docs/current/indexes-partial.html

You need at least the indexes for the joins (do you really need LEFT joins?)

 LEFT OUTER JOIN "user" AS "user" ON "refuel_request"."user_id" = "user"."user_id"

So, refuel_request.user_id must be in the index

 LEFT OUTER JOIN "bill_qr" AS "bill_qr" ON "refuel_request"."bill_qr_id" = 
 LEFT OUTER JOIN "car" AS "order.car" ON "refuel_request"."car_id" = 

bill_qr_id and car_id too

 LEFT OUTER JOIN "refuel_request_status" AS "refuel_request_status" ON "refuel_request"."refuel_request_status_id" =

and refuel_request_status_id

 WHERE
  refuel_request."refuel_request_status_id" IN ( '1', '2', '3') 

refuel_request_status_id must be the first key in the index as we need it in the WHERE

 ORDER BY "refuel_request".created_at desc

and then created_at since it's in the ORDER clause. This will not improve performances per se, but will allow to run the ORDER BY without requiring access to the table data, the same reason why we put the other non-WHERE columns in there. Of course a partial index is even better, we shift the WHERE in the partiality clause and use created_at for the rest (the LIMIT 10 now means that we can do without the extra columns in the index, since retrieving three 1:N rows costs very little; in a different situation we might find it useful to keep those extra columns).

So one index that contains, in this order:

 refuel_request_status_id, created_at, bill_qr_id, car_id too, user_id
 ^ WHERE                   ^ ORDER     ^ used by the JOINS

However, do you really need a SELECT *? I believe you'd get better performances if you only included the fields you're really going to use.

You cannot have an index that supports both the WHERE condition and the ORDER BY, because you are using IN and not =.

The fastest option is to split the query into three parts, so that each part compares refuel_request.refuel_request_status_id with =. Combine these three queries with UNION ALL. Each of the queries has ORDER BY and LIMIT 10, and you wrap the whole thing in an outer query that has another ORDER BY and LIMIT 10.

Then you need these indexes:

CREATE INDEX ON refuel_request (refuel_request_status_id, created_at);
CREATE INDEX ON "user" (user_id);
CREATE INDEX ON bill_qr (bill_qr_id);
CREATE INDEX ON car (car_id);
CREATE INDEX ON refuel_request_status (refuel_request_status_id);

The most effective index for this query would be on refuel_request (refuel_request_status_id, created_at DESC) so that both the main filtering and the ordering can be done using the index. You also want indexes on the columns you're joining, but those tables are small and inconsequential at the moment. In any case, the index I suggest isn't actually going to help much with the performance pain points you're having right now. Here are some suggestions:

  1. Don't use SELECT * unless you really need all of the columns from all of these tables you're joining. Specifying only the necessary columns means postgres can load less data into memory, and work over it faster.

  2. Postgres is spending a lot of time on the joins, joining about a million rows each time, when you're really only interested in ten of those rows. We can encourage it do the order/limit first by rearranging the query somewhat:

WITH refuel_request_subset AS MATERIALIZED (
    SELECT *
    FROM refuel_request
    WHERE refuel_request_status_id IN ('1', '2', '3')
    ORDER BY created_at DESC
    LIMIT 10
)
SELECT *
FROM refuel_request_subset AS refuel_request
LEFT OUTER JOIN user ON refuel_request.user_id = user.user_id
LEFT OUTER JOIN bill_qr ON refuel_request.bill_qr_id = bill_qr.bill_qr_id
LEFT OUTER JOIN car AS "order.car" ON refuel_request.car_id = "order.car".car_id
LEFT OUTER JOIN refuel_request_status ON refuel_request.refuel_request_status_id = refuel_request_status.refuel_request_status_id;

Note: This assumes that the LEFT JOINS will not add rows to the result set, as is the case with your current dataset.

  1. This trick only really works if you have a fixed number of IDs, but you can do the refuel_request_subset query separately for each ID and then UNION the results, as opposed to using the IN operator. That would allow postgres to fully use the index mentioned above.
Related