1

I'm having a little problem at work, the ORM is generating a very strange query and I need to optimize its result a little. I wrote a more readable version of the query and the result is similar, but the execution time is still the same.

Original Query
Explain Verbose

This is the current formatted query, it returns the correct value but takes about 13 seconds to execute.

select count(*)
from reservation as r
 JOIN companyclient as cc ON r.companyclientid = cc.companyclientid
 JOIN reservationitem as ri ON r.reservationid = ri.reservationid
 LEFT JOIN billingaccount as bi
 ON (r.reservationid = bi.reservationid AND cc.companyclientid = bi.companyclientid AND
 bi.propertyid = '84'
 AND NOT bi.isdeleted
 AND bi.groupkey = bi.billingaccountid
 AND bi.billingaccounttypeid = '3'
 AND bi.reservationid IS NOT NULL
 AND bi.statusid = '1')
WHERE r.propertyid = '84'
 AND NOT r.isdeleted
 AND r.companyclientid is not null
 AND ri.tenantid = '025aa64f-67fb-4c23-b975-2b0fc3f5d65a'
 AND NOT ri.isdeleted
 AND ri.reservationitemstatusid NOT IN (6, 3, 7, 8);

Explain Verbose

This is the version I wrote in an attempt to optimize (from 13 seconds to 5), avoiding the conditions inside the left join, but the result is different from the original query. The first query returns 29490 and the second query returns 29397.

select count(*)
from reservation as r
 JOIN companyclient as cc ON r.companyclientid = cc.companyclientid
 JOIN reservationitem as ri ON r.reservationid = ri.reservationid
 LEFT JOIN billingaccount as bi
 ON (r.reservationid = bi.reservationid AND cc.companyclientid = bi.companyclientid)
WHERE r.propertyid = '84'
 AND NOT r.isdeleted
 AND r.companyclientid is not null
 AND ri.tenantid = '025aa64f-67fb-4c23-b975-2b0fc3f5d65a'
 AND NOT ri.isdeleted
 AND ri.reservationitemstatusid NOT IN (6, 3, 7, 8)
 AND (bi is null or bi.propertyid = '84'
 AND NOT bi.isdeleted
 AND bi.groupkey = bi.billingaccountid
 AND bi.billingaccounttypeid = '3'
 AND bi.reservationid IS NOT NULL
 AND bi.statusid = '1')

Explain Verbose

My question is, how can I optimize the first query, I've tried some methods but I haven't had much success. I understand that the count is linear and its time is based on the size of the query return, but I imagine it's too slow for a 30K rows query.

In this specific case, I need the total of items to calculate the number of pages in the limit offset pagination.

Thanks in advance to everyone who had the patience to read, I accept any help.

Size of tables used in the example:

  • reservation: 288549 rows
  • companyclient: 50614 rows
  • reservationitem: 387820 rows
  • billingaccount: 772521 rows
asked Sep 20, 2022 at 19:36
2
  • Occasionally, I've sped things up by grouping the id's in an inner query and then doing the rest of the where clauses etc. in the 'outer query' so it is the same overall query. If indexed properly, this can dramatically improve speed, but it doesn't always work. Commented Sep 20, 2022 at 20:36
  • 1
    I suggest you add the CREATE TABLE statements for all 3 tables, including indexes - or the output of \d reservation from psql (again for all 3 tables). Commented Sep 20, 2022 at 21:50

2 Answers 2

1

Some thoughts about the query. If we had the tables structure and indexes, it would be more helpful (and the advice maybe different):

  • The 2nd query is not equivalent to the 1st so it's not unusual that you get different results. So let's stick to the first query:

     select count(*)
     from reservation as r
     JOIN companyclient as cc ON r.companyclientid = cc.companyclientid
     JOIN reservationitem as ri ON r.reservationid = ri.reservationid
     LEFT JOIN billingaccount as bi
     ON (r.reservationid = bi.reservationid
     AND cc.companyclientid = bi.companyclientid
     AND bi.propertyid = '84'
     AND NOT bi.isdeleted
     AND bi.groupkey = bi.billingaccountid
     AND bi.billingaccounttypeid = '3'
     AND bi.reservationid IS NOT NULL
     AND bi.statusid = '1')
     WHERE r.propertyid = '84'
     AND NOT r.isdeleted
     AND r.companyclientid is not null
     AND ri.tenantid = '025aa64f-67fb-4c23-b975-2b0fc3f5d65a'
     AND NOT ri.isdeleted
     AND ri.reservationitemstatusid NOT IN (6, 3, 7, 8);
    
  • The conditions r.companyclientid is not null and bi.reservationid IS NOT NULL are redundant since these columns already participate in joining conditions and any NULL value will not pass the joining conditions. So these two can be safely removed (but still check that you get the same results).

  • The cc.companyclientid = bi.companyclientid condition (inside the subquery) can be replaced with r.companyclientid = bi.companyclientid since you have the equality condition in the outer query: r.companyclientid = cc.companyclientid. This leaves no cc column in the subquery and all the subquery's conditions involve only 2 tables, bi and r and it might help Postgres' planner to get a more simple plan (especially if there are suitable indexes on billingaccount and reservation.

  • If and only if the reservation. companyclientid is not nullable and there is a FOREIGN KEY constraint that REFERENCES companyclient (companyclientid), then we can completely remove the JOIN companyclient as cc ON r.companyclientid = cc.companyclientid since for any row in reservation, the foreign guarantees that there is a single row in companyclient and the count would not be affected.

  • use count(1) instead of count(*). This is just a minor improvement in some versions.

If all the above are applied, the query would become
(and don't forget to compare results with the original correct query after every modification!):

 select count(1)
 from reservation as r
 JOIN reservationitem as ri ON r.reservationid = ri.reservationid
 LEFT JOIN billingaccount as bi
 ON (r.reservationid = bi.reservationid
 AND r.companyclientid = bi.companyclientid
 AND bi.propertyid = '84'
 AND NOT bi.isdeleted
 AND bi.groupkey = bi.billingaccountid
 AND bi.billingaccounttypeid = '3'
 AND bi.statusid = '1')
 WHERE r.propertyid = '84'
 AND NOT r.isdeleted
 AND ri.tenantid = '025aa64f-67fb-4c23-b975-2b0fc3f5d65a'
 AND NOT ri.isdeleted
 AND ri.reservationitemstatusid NOT IN (6, 3, 7, 8);

Now let's suggest some indexes, specifically for this query and one for each table:

CREATE INDEX suggested_index_a ON
reservation
 (propertyid, reservationid, companyclientid)
WHERE (NOT isdeleted) ;
CREATE INDEX suggested_index_b ON
reservationitem
 (tenantid, reservationid) 
INCLUDE (reservationitemstatusid)
WHERE (NOT isdeleted) ;
CREATE INDEX suggested_index_c ON
billingaccount
 (propertyid, billingaccounttypeid, statusid,
 reservationid, companyclientid)
WHERE (groupkey = billingaccountid 
 AND NOT isdeleted) ;

Please try with the above indexes added. Have in mind that any index consumes disk space (and memory space when used) so it is a trade off.

4
  • Thanks for your amazing answer. The result was slightly larger than expected, it returned 29968 rows. I will check all the points you mentioned. Commented Sep 21, 2022 at 15:53
  • The result returned different because the join with companyclient is necessary, but your query is around 60% faster, now I will check the indexes. Commented Sep 21, 2022 at 16:38
  • so there is no FK :( Commented Sep 21, 2022 at 17:22
  • Using the indices, the query runs in 1.1 seconds! Thanks! Commented Sep 22, 2022 at 16:25
1

While that surely is a oddly written query, using the plan we can see there is a missing index on billingaccount, without having to understand what the design of the overall query is.

You shouldn't need to scan and reject 7649 rows just to return 84, so some of the columns in the filter should be part of the index. We don't know how selective each part of the filter is, but based on the column names and constants, I am guessing you need a multicolumn index over both companyclientid and propertyid. (And should probably have billingaccounttypeid and statusid in it as well, it shouldn't hurt much and might help)

answered Sep 20, 2022 at 21:57
1
  • Ok, thanks for the reply! I'll have a look if any more indexes can be added. Commented Sep 21, 2022 at 15:48

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.